11
\$\begingroup\$

The Code Explorer is a dockable toolwindow that displays a TreeView that shows all opened VBA projects and their respective modules, but unlike the "native" Project Explorer, Rubberduck's explorer also drills down to individual module members, and allows quickly navigating them:

Rubberduck 1.3 Code Explorer

While it looks pretty, the code behind it is among the most painful code in the project - clearly, it needs to be refactored and optimized. I'm particularly interested in ways to improve the async void RefreshExplorerTreeView() method and the whole node-generation code.

namespace Rubberduck.UI.CodeExplorer
{
    public class CodeExplorerDockablePresenter : DockablePresenterBase
    {
        private readonly IRubberduckParser _parser;
        private CodeExplorerWindow Control { get { return UserControl as CodeExplorerWindow; } }

        public CodeExplorerDockablePresenter(IRubberduckParser parser, VBE vbe, AddIn addIn, CodeExplorerWindow view)
            : base(vbe, addIn, view)
        {
            _parser = parser;
            RegisterControlEvents();
        }

        public override void Show()
        {
            base.Show();
            Task.Run(() => RefreshExplorerTreeView()); 
        }

        private void RegisterControlEvents()
        {
            if (Control == null)
            {
                return;
            }

            Control.RefreshTreeView += RefreshExplorerTreeView;
            Control.NavigateTreeNode += NavigateExplorerTreeNode;
            Control.AddComponent += AddComponent;
            Control.AddTestModule += AddTestModule;
            Control.ToggleFolders += ToggleFolders;
            Control.ShowDesigner += ShowDesigner;
            Control.DisplayStyleChanged += DisplayStyleChanged;
            Control.RunAllTests += ContextMenuRunAllTests;
            Control.RunInspections += ContextMenuRunInspections;
            Control.SelectionChanged += SelectionChanged;
            Control.Rename += RenameSelection;
            Control.FindAllReferences += FindAllReferencesForSelection;
        }

        public event EventHandler<NavigateCodeEventArgs> FindAllReferences;
        private void FindAllReferencesForSelection(object sender, NavigateCodeEventArgs e)
        {
            var handler = FindAllReferences;
            if (handler != null)
            {
                handler(sender, e);
            }
        }

        public event EventHandler<TreeNodeNavigateCodeEventArgs> Rename;
        private void RenameSelection(object sender, TreeNodeNavigateCodeEventArgs e)
        {
            if (e.Node == null || e.Selection.Equals(default(Selection)) && e.QualifiedName == default(QualifiedModuleName))
            {
                return;
            }

            var handler = Rename;
            if (handler != null)
            {
                handler(this, e);
                RefreshExplorerTreeView();
                e.Node.EnsureVisible();
            }
        }

        private void SelectionChanged(object sender, TreeNodeNavigateCodeEventArgs e)
        {
            if (e.Node == null || e.Node.Tag == null)
            {
                return;
            }

            try
            {
                VBE.ActiveVBProject = e.QualifiedName.Project;
            }
            catch (COMException)
            {
                // swallow "catastrophic failure"
            }
        }

        public event EventHandler RunInspections;
        private void ContextMenuRunInspections(object sender, EventArgs e)
        {
            var handler = RunInspections;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        public event EventHandler RunAllTests;
        private void ContextMenuRunAllTests(object sender, EventArgs e)
        {
            var handler = RunAllTests;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        private void DisplayStyleChanged(object sender, EventArgs e)
        {
            RefreshExplorerTreeView();
        }

        private void AddTestModule(object sender, EventArgs e)
        {
            NewUnitTestModuleCommand.NewUnitTestModule(VBE);
            RefreshExplorerTreeView();
        }

        private void ShowDesigner(object sender, EventArgs e)
        {
            var node = Control.SolutionTree.SelectedNode;
            if (node != null && node.Tag != null)
            {
                var selection = (Declaration)node.Tag;
                var module = selection.QualifiedName.QualifiedModuleName.Component.CodeModule;
                if (module == null)
                {
                    return;
                }

                try
                {
                    module.Parent.DesignerWindow().Visible = true;
                }
                catch
                {
                    Control.ShowDesignerButton.Enabled = false;
                }
            }
        }

        private bool _showFolders = true;
        private void ToggleFolders(object sender, EventArgs e)
        {
            _showFolders = !_showFolders;
            RefreshExplorerTreeView();
        }

        private void AddComponent(object sender, AddComponentEventArgs e)
        {
            var project = VBE.ActiveVBProject;
            if (project == null)
            {
                return;
            }

            project.VBComponents.Add(e.ComponentType);
            RefreshExplorerTreeView();
        }

        private void NavigateExplorerTreeNode(object sender, TreeNodeNavigateCodeEventArgs e)
        {
            var declaration = e.Declaration;
            if (declaration != null)
            {
                VBE.SetSelection(new QualifiedSelection(declaration.QualifiedName.QualifiedModuleName, declaration.Selection));
            }
        }

        private void RefreshExplorerTreeView(object sender, EventArgs e)
        {
            Task.Run(() => RefreshExplorerTreeView());
        }

        private async void RefreshExplorerTreeView()
        {
            Control.Invoke((MethodInvoker) delegate
            {
                Control.SolutionTree.Nodes.Clear();
                Control.ShowDesignerButton.Enabled = false;
            });

            var projects = VBE.VBProjects.Cast<VBProject>();
            foreach (var vbProject in projects)
            {
                var project = vbProject;
                await Task.Run(() =>
                {
                    var node = new TreeNode(project.Name + " (parsing...)");
                    node.ImageKey = "Hourglass";
                    node.SelectedImageKey = node.ImageKey;

                    Control.Invoke((MethodInvoker)delegate
                    {
                        Control.SolutionTree.Nodes.Add(node);
                        Control.SolutionTree.Refresh();
                        AddProjectNodes(project, node);
                    });
                });
            }
        }

        private void AddProjectNodes(VBProject project, TreeNode root)
        {
            Control.Invoke((MethodInvoker)async delegate
            {
                if (project.Protection == vbext_ProjectProtection.vbext_pp_locked)
                {
                    root.ImageKey = "Locked";
                }
                else
                {
                    var nodes = (await CreateModuleNodesAsync(project)).ToArray();
                    AddProjectFolders(project, root, nodes);
                    root.ImageKey = "ClosedFolder";
                    root.Expand();
                }

                root.Text = project.Name;
            });
        }

        private static readonly IDictionary<vbext_ComponentType, string> ComponentTypeIcons =
            new Dictionary<vbext_ComponentType, string>
            {
                { vbext_ComponentType.vbext_ct_StdModule, "StandardModule"},
                { vbext_ComponentType.vbext_ct_ClassModule, "ClassModule"},
                { vbext_ComponentType.vbext_ct_Document, "OfficeDocument"},
                { vbext_ComponentType.vbext_ct_ActiveXDesigner, "ClassModule"},
                { vbext_ComponentType.vbext_ct_MSForm, "Form"}
            };

        private void AddProjectFolders(VBProject project, TreeNode root, TreeNode[] components)
        {
            var documentNodes = components.Where(node => node.ImageKey == "OfficeDocument")
                                          .OrderBy(node => node.Text)
                                          .ToArray();
            if (project.VBComponents.Cast<VBComponent>()
                       .Any(component => component.Type == vbext_ComponentType.vbext_ct_Document))
            {
                AddFolderNode(root, "Documents", "ClosedFolder", documentNodes);
            }

            var formsNodes = components.Where(node => node.ImageKey == "Form")
                                       .OrderBy(node => node.Text)
                                       .ToArray();
            if (project.VBComponents.Cast<VBComponent>()
                       .Any(component => component.Type == vbext_ComponentType.vbext_ct_MSForm))
            {
                AddFolderNode(root, "Forms", "ClosedFolder", formsNodes);
            }

            var stdModulesNodes = components.Where(node => node.ImageKey == "StandardModule")
                                            .OrderBy(node => node.Text)
                                            .ToArray();
            if (project.VBComponents.Cast<VBComponent>()
                       .Any(component => component.Type == vbext_ComponentType.vbext_ct_StdModule))
            {
                AddFolderNode(root, "Standard Modules", "ClosedFolder", stdModulesNodes);
            }

            var classModulesNodes = components.Where(node => node.ImageKey == "ClassModule")
                                              .OrderBy(node => node.Text)
                                              .ToArray();
            if (project.VBComponents.Cast<VBComponent>()
                       .Any(component => component.Type == vbext_ComponentType.vbext_ct_ClassModule
                                      || component.Type == vbext_ComponentType.vbext_ct_ActiveXDesigner))
            {
                AddFolderNode(root, "Class Modules", "ClosedFolder", classModulesNodes);
            }
        }

        private void AddFolderNode(TreeNode root, string text, string imageKey, TreeNode[] nodes)
        {
            if (_showFolders)
            {
                var node = root.Nodes.Add(text);
                node.ImageKey = imageKey;
                node.SelectedImageKey = imageKey;
                node.Nodes.AddRange(nodes);
                node.Expand();
            }
            else
            {
                root.Nodes.AddRange(nodes);
            }
        }

        private async Task<IEnumerable<TreeNode>> CreateModuleNodesAsync(VBProject project)
        {
            var result = new List<TreeNode>();
            var parseResult = _parser.Parse(project);
            foreach (var componentParseResult in parseResult.ComponentParseResults)
            {
                var component = componentParseResult.Component;
                var members = parseResult.Declarations.Items
                    .Where(declaration => declaration.ParentScope == component.Collection.Parent.Name + "." + component.Name
                        && declaration.DeclarationType != DeclarationType.Control
                        && declaration.DeclarationType != DeclarationType.ModuleOption);

                var node = new TreeNode(component.Name);
                node.ImageKey = ComponentTypeIcons[component.Type];
                node.SelectedImageKey = node.ImageKey;
                node.Tag = parseResult.Declarations.Items.SingleOrDefault(item => 
                    item.IdentifierName == component.Name 
                    && item.Project == component.Collection.Parent
                    && (item.DeclarationType == DeclarationType.Class || item.DeclarationType == DeclarationType.Module));

                foreach (var declaration in members)
                {
                    if (declaration.DeclarationType == DeclarationType.UserDefinedTypeMember
                        || declaration.DeclarationType == DeclarationType.EnumerationMember)
                    {
                        // these ones are handled by their respective parent
                        continue;
                    }

                    var text = GetNodeText(declaration);
                    var child = new TreeNode(text);
                    child.ImageKey = GetImageKeyForDeclaration(declaration);
                    child.SelectedImageKey = child.ImageKey;
                    child.Tag = declaration;

                    if (declaration.DeclarationType == DeclarationType.UserDefinedType
                        || declaration.DeclarationType == DeclarationType.Enumeration)
                    {
                        var subDeclaration = declaration;
                        var subMembers = parseResult.Declarations.Items.Where(item => 
                            (item.DeclarationType == DeclarationType.EnumerationMember || item.DeclarationType == DeclarationType.UserDefinedTypeMember)
                            && item.Context != null && subDeclaration.Context.Equals(item.Context.Parent));

                        foreach (var subMember in subMembers)
                        {
                            var subChild = new TreeNode(subMember.IdentifierName);
                            subChild.ImageKey = GetImageKeyForDeclaration(subMember);
                            subChild.SelectedImageKey = subChild.ImageKey;
                            subChild.Tag = subMember;
                            child.Nodes.Add(subChild);
                        }
                    }

                    node.Nodes.Add(child);
                }

                result.Add(node);
            }

            return result;
        }

        private string GetNodeText(Declaration declaration)
        {
            if (Control.DisplayStyle == TreeViewDisplayStyle.MemberNames)
            {
                var result = declaration.IdentifierName;
                if (declaration.DeclarationType == DeclarationType.PropertyGet)
                {
                    result += " (" + Tokens.Get + ")";
                }
                else if (declaration.DeclarationType == DeclarationType.PropertyLet)
                {
                    result += " (" + Tokens.Let + ")";
                }
                else if (declaration.DeclarationType == DeclarationType.PropertySet)
                {
                    result += " (" + Tokens.Set + ")";
                }

                return result;
            }

            if (declaration.DeclarationType == DeclarationType.Procedure)
            {
                return ((VBAParser.SubStmtContext) declaration.Context).Signature();
            }

            if (declaration.DeclarationType == DeclarationType.Function)
            {
                return ((VBAParser.FunctionStmtContext)declaration.Context).Signature();
            }

            if (declaration.DeclarationType == DeclarationType.PropertyGet)
            {
                return ((VBAParser.PropertyGetStmtContext)declaration.Context).Signature();
            }

            if (declaration.DeclarationType == DeclarationType.PropertyLet)
            {
                return ((VBAParser.PropertyLetStmtContext)declaration.Context).Signature();
            }

            if (declaration.DeclarationType == DeclarationType.PropertySet)
            {
                return ((VBAParser.PropertySetStmtContext)declaration.Context).Signature();
            }

            return declaration.IdentifierName;
        }

        private string GetImageKeyForDeclaration(Declaration declaration)
        {
            var result = string.Empty;
            switch (declaration.DeclarationType)
            {
                case DeclarationType.Module:
                    break;
                case DeclarationType.Class:
                    break;
                case DeclarationType.Procedure:
                case DeclarationType.Function:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateMethod";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendMethod";
                        break;
                    }
                    result = "PublicMethod";
                    break;

                case DeclarationType.PropertyGet:
                case DeclarationType.PropertyLet:
                case DeclarationType.PropertySet:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateProperty";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendProperty";
                        break;
                    }
                    result = "PublicProperty";
                    break;

                case DeclarationType.Parameter:
                    break;
                case DeclarationType.Variable:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateField";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendField";
                        break;
                    }
                    result = "PublicField";
                    break;

                case DeclarationType.Constant:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateConst";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendConst";
                        break;
                    }
                    result = "PublicConst";
                    break;

                case DeclarationType.Enumeration:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateEnum";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendEnum";
                        break;
                    }
                    result = "PublicEnum";
                    break;

                case DeclarationType.EnumerationMember:
                    result = "EnumItem";
                    break;

                case DeclarationType.Event:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateEvent";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendEvent";
                        break;
                    }
                    result = "PublicEvent";
                    break;

                case DeclarationType.UserDefinedType:
                    if (declaration.Accessibility == Accessibility.Private)
                    {
                        result = "PrivateType";
                        break;
                    }
                    if (declaration.Accessibility == Accessibility.Friend)
                    {
                        result = "FriendType";
                        break;
                    }
                    result = "PublicType";
                    break;

                case DeclarationType.UserDefinedTypeMember:
                    result = "PublicField";
                    break;

                case DeclarationType.LibraryProcedure:
                case DeclarationType.LibraryFunction:
                    result = "Identifier";
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }

            return result;
        }
    }
}

One thing that bothers me above all, is the fact that this presenter is calling into the parser - which means the parse results can only be used in this class... but there are many other features that would love to know the VBA code has just been re-parsed. Part of me is starting to accept the idea that maybe perhaps a Singleton might be in order here. Is there another way?

\$\endgroup\$
8
\$\begingroup\$

(Not a full review, just focusing on one method.)

I think private string GetImageKeyForDeclaration(Declaration declaration) contains a lot of repeated code that can be significantly reduced.

First of all I'm not a fan of the "use a switch to set a value that you then return"-approach: just do the return directly, especially if there are a lot of cases.

I'd group all of these:

case DeclarationType.Module:
    break;
case DeclarationType.Class:
    break;
case DeclarationType.Parameter:
    break;

So that would become:

case DeclarationType.Module:
case DeclarationType.Class:
case DeclarationType.Parameter:
    return string.Empty;

Each of these I'd change to a method call:

if (declaration.Accessibility == Accessibility.Private)
{
    result = "PrivateMethod";
    break;
}
if (declaration.Accessibility == Accessibility.Friend)
{
    result = "FriendMethod";
    break;
}
result = "PublicMethod";

So that would become something along these lines:

case DeclarationType.Function:
    return GetImageKeyForDeclaration(declaration, "Method");

case DeclarationType.PropertyGet:
case DeclarationType.PropertyLet:
case DeclarationType.PropertySet:
    return GetImageKeyForDeclaration(declaration, "Property");


private string GetImageKeyForDeclaration(Declaration declaration, string suffix)
{
    if (declaration.Accessibility == Accessibility.Private)
    {
        return "Private" + suffix;
    }
    if (declaration.Accessibility == Accessibility.Friend)
    {
        return "Friend" + suffix;
    }
    return "Public" + suffix;
}

Which means this method is now half its original length:

private string GetImageKeyForDeclaration(Declaration declaration)
{
    switch (declaration.DeclarationType)
    {
        case DeclarationType.Module:
        case DeclarationType.Class:
        case DeclarationType.Parameter:
            return string.Empty;

        case DeclarationType.Procedure:
        case DeclarationType.Function:
            return GetImageKeyForDeclaration(declaration, "Method");

        case DeclarationType.PropertyGet:
        case DeclarationType.PropertyLet:
        case DeclarationType.PropertySet:
            return GetImageKeyForDeclaration(declaration, "Property");

        case DeclarationType.Variable:
            return GetImageKeyForDeclaration(declaration, "Field");

        case DeclarationType.Constant:
            return GetImageKeyForDeclaration(declaration, "Const");

        case DeclarationType.Enumeration:
            return GetImageKeyForDeclaration(declaration, "Enum");

        case DeclarationType.Event:
            return GetImageKeyForDeclaration(declaration, "Event");

        case DeclarationType.UserDefinedType:
            return GetImageKeyForDeclaration(declaration, "Type");

        case DeclarationType.EnumerationMember:
            return "EnumItem";

        case DeclarationType.UserDefinedTypeMember:
            return "PublicField";

        case DeclarationType.LibraryProcedure:
        case DeclarationType.LibraryFunction:
            return "Identifier";

        default:
            throw new ArgumentOutOfRangeException();
    }
}

I'm still inclined to move this to a helper class though, considering that CodeExplorerDockablePresenter is 500+ lines long.

\$\endgroup\$
5
\$\begingroup\$

I'm not going to make a full review either - I'll leave that to other eyes.

One thing I just have to mention is this part of the async void RefreshTreeView method:

foreach (var vbProject in projects)
{
    var project = vbProject;
    await Task.Run(() =>
    {
        var node = new TreeNode(project.Name + " (parsing...)");
        node.ImageKey = "Hourglass";
        node.SelectedImageKey = node.ImageKey;

        Control.Invoke((MethodInvoker)delegate
        {
            Control.SolutionTree.Nodes.Add(node);
            Control.SolutionTree.Refresh();
            AddProjectNodes(project, node);
        });
    });
}

And then the first thing AddProjectNodes does...

Control.Invoke((MethodInvoker)async delegate
{

...but it's already running on the UI thread. And since that code ultimately ends up calling AddModuleNodesAsync which calls _parser.Parse(project), you have very, very, very convoluted code here that looks like it's running async (hard to tell really, with all the Invokeing and Task-nesting going on), but in reality the parsing and the actual CPU-intensive processing occurs on the UI thread, which locks up the IDE while all this is happening.

For larger VBA projects this looks like it could be a problem.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.