10
\$\begingroup\$

Next release of Rubberduck will introduce a very cool @Folder annotation system that the Code Explorer will use to organize modules into - you guessed it - folders:

RD 2.0 Code Explorer showcasing custom folders and '@Folder annotations

...which is tremendously awesome, because VBA doesn't have any concept of namespace, and the VBE doesn't offer anything better than a "Classes" folder to organize your class modules - this feature will make writing OOP VBA code much more appealing.

The Code Explorer WPF user control binds to a CodeExplorerViewModel class, which isn't completed yet - I have a bunch of commands to wire up still, so I'm not including it here.

I'm interested in feedback specifically about the way the node hierarchy is done and rendered, so here's the root-level CodeExplorerProjectViewModel class:

public class CodeExplorerProjectViewModel : CodeExplorerItemViewModel
{
    private readonly Declaration _declaration;
    private static readonly DeclarationType[] ComponentTypes =
    {
        DeclarationType.Class, 
        DeclarationType.Document, 
        DeclarationType.Module, 
        DeclarationType.UserForm, 
    };

    public CodeExplorerProjectViewModel(Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _declaration = declaration;
        Items = FindFolders(declarations.ToList(), '.');

        _icon = _declaration.Project.Protection == vbext_ProjectProtection.vbext_pp_locked
            ? GetImageSource(resx.lock__exclamation)
            : GetImageSource(resx.VSObject_Library);
    }

    private static IEnumerable<CodeExplorerItemViewModel> FindFolders(IEnumerable<Declaration> declarations, char delimiter)
    {
        var root = new CodeExplorerCustomFolderViewModel(string.Empty, new List<Declaration>());

        var items = declarations.ToList();
        var folders = items.Where(item => ComponentTypes.Contains(item.DeclarationType))
                           .GroupBy(item => item.CustomFolder)
                           .OrderBy(item => item.Key);
        foreach (var grouping in folders)
        {
            CodeExplorerItemViewModel node = root;
            var parts = grouping.Key.Split(delimiter);
            var path = new StringBuilder();
            foreach (var part in parts)
            {
                if (path.Length != 0)
                {
                    path.Append(delimiter);
                }

                path.Append(part);
                var next = node.GetChild(part);
                if (next == null)
                {
                    var currentPath = path.ToString();
                    var parents = grouping.Where(item => ComponentTypes.Contains(item.DeclarationType) && item.CustomFolder == currentPath).ToList();

                    next = new CodeExplorerCustomFolderViewModel(part, items.Where(item => 
                        parents.Contains(item) || parents.Any(parent => 
                            (item.ParentDeclaration != null && item.ParentDeclaration.Equals(parent)) || item.ComponentName == parent.ComponentName)));
                    node.AddChild(next);
                }

                node = next;
            }
        }

        return root.Items;
    }

    private readonly BitmapImage _icon;
    public override BitmapImage CollapsedIcon { get { return _icon; } }
    public override BitmapImage ExpandedIcon { get { return _icon; } }

    public override string Name { get { return _declaration.CustomFolder; } }
}

If none of the project components have a @Folder annotation, a folder is still created, named after the project. Here's the CodeExplorerCustomFolderViewModel class:

public class CodeExplorerCustomFolderViewModel : CodeExplorerItemViewModel
{
    private readonly string _name;
    private static readonly DeclarationType[] ComponentTypes =
    {
        DeclarationType.Class, 
        DeclarationType.Document, 
        DeclarationType.Module, 
        DeclarationType.UserForm, 
    };

    public CodeExplorerCustomFolderViewModel(string name, IEnumerable<Declaration> declarations)
    {
        _name = name;

        _collapsedIcon = GetImageSource(resx.folder_horizontal);
        _expandedIcon = GetImageSource(resx.folder_horizontal_open);

        var items = declarations.ToList();

        var parents = items.GroupBy(item => item.ComponentName).OrderBy(item => item.Key).ToList();
        foreach (var component in parents)
        {
            var moduleName = component.Key;
            var parent = items.Single(item =>
                ComponentTypes.Contains(item.DeclarationType) && item.ComponentName == moduleName);
            var members = items.Where(item =>
                !ComponentTypes.Contains(item.DeclarationType) && item.ComponentName == moduleName);

            AddChild(new CodeExplorerComponentViewModel(parent, members));
        }
    }

    public override string Name { get { return _name; } }

    private readonly BitmapImage _collapsedIcon;
    public override BitmapImage CollapsedIcon { get { return _collapsedIcon; } }

    private readonly BitmapImage _expandedIcon;
    public override BitmapImage ExpandedIcon { get { return _expandedIcon; } }
}

Walking down the tree, the CodeExplorerComponentViewModel class represents a project component / module:

public class CodeExplorerComponentViewModel : CodeExplorerItemViewModel
{
    private readonly Declaration _declaration;

    private static readonly DeclarationType[] MemberTypes =
    {
        DeclarationType.Constant, 
        DeclarationType.Enumeration, 
        DeclarationType.Event, 
        DeclarationType.Function, 
        DeclarationType.LibraryFunction, 
        DeclarationType.LibraryProcedure, 
        DeclarationType.Procedure,
        DeclarationType.PropertyGet, 
        DeclarationType.PropertyLet, 
        DeclarationType.PropertySet, 
        DeclarationType.UserDefinedType, 
        DeclarationType.Variable, 
    };

    public CodeExplorerComponentViewModel(Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _declaration = declaration;
        _icon = Icons[DeclarationType];
        Items = declarations.GroupBy(item => item.Scope).SelectMany(grouping =>
                        grouping.Where(item => item.ParentDeclaration != null
                                            && item.ParentScope == declaration.Scope                                                && MemberTypes.Contains(item.DeclarationType))
                            .OrderBy(item => item.QualifiedSelection.Selection.StartLine)
                            .Select(item => new CodeExplorerMemberViewModel(item, grouping)));
        
    }

    private bool _isErrorState;
    public bool IsErrorState { get { return _isErrorState; } set { _isErrorState = value; OnPropertyChanged(); } }

    public bool IsTestModule
    {
        get
        {
            return _declaration.DeclarationType == DeclarationType.Module 
                   && _declaration.Annotations.Split('\n').Contains(Parsing.Grammar.Annotations.TestModule);
        }
    }

    public override string Name { get { return _declaration.IdentifierName; } }


    private vbext_ComponentType ComponentType { get { return _declaration.QualifiedName.QualifiedModuleName.Component.Type; } }

    private static readonly IDictionary<vbext_ComponentType, DeclarationType> DeclarationTypes = new Dictionary<vbext_ComponentType, DeclarationType>
    {
        { vbext_ComponentType.vbext_ct_ClassModule, DeclarationType.Class },
        { vbext_ComponentType.vbext_ct_StdModule, DeclarationType.Module },
        { vbext_ComponentType.vbext_ct_Document, DeclarationType.Document },
        { vbext_ComponentType.vbext_ct_MSForm, DeclarationType.UserForm }
    };

    private DeclarationType DeclarationType
    {
        get
        {
            DeclarationType result;
            if (!DeclarationTypes.TryGetValue(ComponentType, out result))
            {
                result = DeclarationType.Class;
            }

            return result;
        }
    }

    private static readonly IDictionary<DeclarationType,BitmapImage> Icons = new Dictionary<DeclarationType, BitmapImage>
    {
        { DeclarationType.Class, GetImageSource(resx.VSObject_Class) },
        { DeclarationType.Module, GetImageSource(resx.VSObject_Module) },
        { DeclarationType.UserForm, GetImageSource(resx.VSProject_form) },
        { DeclarationType.Document, GetImageSource(resx.document_office) }
    };

    private readonly BitmapImage _icon;
    public override BitmapImage CollapsedIcon { get { return _icon; } }
    public override BitmapImage ExpandedIcon { get { return _icon; } }
}

The last level in the tree represents members (and sub-members too - e.g. an enum node will have its enum members under it in the Code Explorer):

public class CodeExplorerMemberViewModel : CodeExplorerItemViewModel
{
    private readonly Declaration _declaration;

    private static readonly DeclarationType[] SubMemberTypes =
    {
        DeclarationType.EnumerationMember, 
        DeclarationType.UserDefinedTypeMember            
    };

    private static readonly IDictionary<Tuple<DeclarationType,Accessibility>,BitmapImage> Mappings =
        new Dictionary<Tuple<DeclarationType, Accessibility>, BitmapImage>
        {
            { Tuple.Create(DeclarationType.Constant, Accessibility.Private), GetImageSource(resx.VSObject_Constant_Private)},
            { Tuple.Create(DeclarationType.Constant, Accessibility.Public), GetImageSource(resx.VSObject_Field)},
            { Tuple.Create(DeclarationType.Enumeration, Accessibility.Public), GetImageSource(resx.VSObject_Enum)},
            { Tuple.Create(DeclarationType.Enumeration, Accessibility.Private ), GetImageSource(resx.VSObject_EnumPrivate)},
            { Tuple.Create(DeclarationType.EnumerationMember, Accessibility.Public), GetImageSource(resx.VSObject_EnumItem)},
            { Tuple.Create(DeclarationType.Event, Accessibility.Public), GetImageSource(resx.VSObject_Event)},
            { Tuple.Create(DeclarationType.Event, Accessibility.Private ), GetImageSource(resx.VSObject_Event_Private)},
            { Tuple.Create(DeclarationType.Function, Accessibility.Public), GetImageSource(resx.VSObject_Method)},
            { Tuple.Create(DeclarationType.Function, Accessibility.Friend ), GetImageSource(resx.VSObject_Method_Friend)},
            { Tuple.Create(DeclarationType.Function, Accessibility.Private ), GetImageSource(resx.VSObject_Method_Private)},
            { Tuple.Create(DeclarationType.LibraryFunction, Accessibility.Public), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.LibraryProcedure, Accessibility.Public), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.LibraryFunction, Accessibility.Private), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.LibraryProcedure, Accessibility.Private), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.LibraryFunction, Accessibility.Friend), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.LibraryProcedure, Accessibility.Friend), GetImageSource(resx.VSObject_Method_Shortcut)},
            { Tuple.Create(DeclarationType.Procedure, Accessibility.Public), GetImageSource(resx.VSObject_Method)},
            { Tuple.Create(DeclarationType.Procedure, Accessibility.Friend ), GetImageSource(resx.VSObject_Method_Friend)},
            { Tuple.Create(DeclarationType.Procedure, Accessibility.Private ), GetImageSource(resx.VSObject_Method_Private)},
            { Tuple.Create(DeclarationType.PropertyGet, Accessibility.Public), GetImageSource(resx.VSObject_Properties)},
            { Tuple.Create(DeclarationType.PropertyGet, Accessibility.Friend ), GetImageSource(resx.VSObject_Properties_Friend)},
            { Tuple.Create(DeclarationType.PropertyGet, Accessibility.Private ), GetImageSource(resx.VSObject_Properties_Private)},
            { Tuple.Create(DeclarationType.PropertyLet, Accessibility.Public), GetImageSource(resx.VSObject_Properties)},
            { Tuple.Create(DeclarationType.PropertyLet, Accessibility.Friend ), GetImageSource(resx.VSObject_Properties_Friend)},
            { Tuple.Create(DeclarationType.PropertyLet, Accessibility.Private ), GetImageSource(resx.VSObject_Properties_Private)},
            { Tuple.Create(DeclarationType.PropertySet, Accessibility.Public), GetImageSource(resx.VSObject_Properties)},
            { Tuple.Create(DeclarationType.PropertySet, Accessibility.Friend ), GetImageSource(resx.VSObject_Properties_Friend)},
            { Tuple.Create(DeclarationType.PropertySet, Accessibility.Private ), GetImageSource(resx.VSObject_Properties_Private)},
            { Tuple.Create(DeclarationType.UserDefinedType, Accessibility.Public), GetImageSource(resx.VSObject_ValueType)},
            { Tuple.Create(DeclarationType.UserDefinedType, Accessibility.Private ), GetImageSource(resx.VSObject_ValueTypePrivate)},
            { Tuple.Create(DeclarationType.UserDefinedTypeMember, Accessibility.Public), GetImageSource(resx.VSObject_Field)},
            { Tuple.Create(DeclarationType.Variable, Accessibility.Private), GetImageSource(resx.VSObject_Field_Private)},
            { Tuple.Create(DeclarationType.Variable, Accessibility.Public ), GetImageSource(resx.VSObject_Field)},
        };

    public CodeExplorerMemberViewModel(Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _declaration = declaration;
        if (declarations != null)
        {
            Items = declarations.Where(item => SubMemberTypes.Contains(item.DeclarationType) && item.ParentDeclaration.Equals(declaration))
                                .OrderBy(item => item.Selection.StartLine)
                                .Select(item => new CodeExplorerMemberViewModel(item, null));
        }

        var modifier = declaration.Accessibility == Accessibility.Global || declaration.Accessibility == Accessibility.Implicit
            ? Accessibility.Public
            : declaration.Accessibility;
        var key = Tuple.Create(declaration.DeclarationType, modifier);
        _icon = Mappings[key];
    }

    public override string Name { get { return _declaration.IdentifierName; } }

    private readonly BitmapImage _icon;
    public override BitmapImage CollapsedIcon { get { return _icon; } }
    public override BitmapImage ExpandedIcon { get { return _icon; } }
}

You might have noticed that every single one of these classes (except the root level) is derived from the CodeExplorerItemViewModel class. Here's this base class:

public abstract class CodeExplorerItemViewModel : ViewModelBase
{
    private IList<CodeExplorerItemViewModel> _items = new List<CodeExplorerItemViewModel>();
    public IEnumerable<CodeExplorerItemViewModel> Items { get { return _items; } protected set { _items = value.ToList(); } }

    public abstract string Name { get; }
    public abstract BitmapImage CollapsedIcon { get; }
    public abstract BitmapImage ExpandedIcon { get; }

    public CodeExplorerItemViewModel GetChild(string name)
    {
        foreach (var item in _items)
        {
            if (item.Name == name)
            {
                return item;
            }
            var result = item.GetChild(name);
            if (result != null)
            {
                return result;
            }
        }

        return null;
    }

    public void AddChild(CodeExplorerItemViewModel item)
    {
        _items.Add(item);
    }
}

Here's how it's rendered in the XAML (note: this isn't the full markup):

    <Style x:Key="IconStyle" TargetType="Image">
        <Setter Property="Height" Value="16" />
        <Setter Property="Width" Value="16" />
        <Setter Property="Margin" Value="2,0,2,0" />
    </Style>
    
    <Style x:Key="TreeViewItemStyle" TargetType="TextBlock">
        <Setter Property="Text" Value="{Binding Name}" />
        <Setter Property="Margin" Value="2,0,2,0" />
    </Style>
    
    <Style x:Key="TreeViewIconStyle" TargetType="Image" BasedOn="{StaticResource IconStyle}">
        <Setter Property="Source" Value="{Binding CollapsedIcon}" />
        <Style.Triggers>
            <!-- thanks to H.B. on http://stackoverflow.com/a/5797323/1188513 -->
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsExpanded}" Value="True">
                <Setter Property="Source" Value="{Binding ExpandedIcon}" />
            </DataTrigger>
        </Style.Triggers>
    </Style>

    <HierarchicalDataTemplate DataType="codeExplorer:CodeExplorerItemViewModel"
                              ItemsSource="{Binding Items}">
        <StackPanel Orientation="Horizontal">
            <Image Style="{StaticResource TreeViewIconStyle}" />
            <TextBlock Style="{StaticResource TreeViewItemStyle}" />
        </StackPanel>
    </HierarchicalDataTemplate>
    
    <HierarchicalDataTemplate x:Key="CodeExplorerTemplate" 
                              DataType="codeExplorer:CodeExplorerProjectViewModel"
                              ItemsSource="{Binding Items}">
        <StackPanel Orientation="Horizontal">
            <Image Style="{StaticResource TreeViewIconStyle}" />
            <TextBlock Style="{StaticResource TreeViewItemStyle}" />
        </StackPanel>
    </HierarchicalDataTemplate>

    <Style x:Key="CodeExplorerTreeViewStyle" TargetType="TreeView">
        <Setter Property="ItemTemplate" Value="{StaticResource CodeExplorerTemplate}" />
        <Setter Property="ItemsSource" Value="{Binding Projects}" />
    </Style>
    <TreeView Grid.Row="1"
              ItemContainerStyle="{StaticResource ShinyTreeView}"
              HorizontalContentAlignment="Stretch"
              MouseDoubleClick="TreeView_OnMouseDoubleClick"
              Style="{StaticResource CodeExplorerTreeViewStyle}">
        <i:Interaction.Behaviors>
            <resx:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
        </i:Interaction.Behaviors>
    </TreeView>

Everything works as expected, and the code that determines what nodes get what icon is much, much cleaner than it was in v1.x - but I'm sure it's not as good as it gets.. or is it? Anything else jumps at you?

\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Looks pretty good. Some readability improvements:

  1. Magic character '.' should be moved into a named constant like DefaultDelimiter.

  2. Since you need to classify the DeclarationType another way to encapsulate this logic is to create a set of extension methods like this:

    public static class DeclarationTypeExtensions
    {
        public static bool IsComponentType(this DeclarationType declType)
        {
            switch (declType)
            {
                DeclarationType.Class:
                DeclarationType.Document:
                DeclarationType.Module:
                DeclarationType.UserForm:
                    return true;
            }
            return false;
        }
    
        public static bool IsMemberType(this DeclarationType declType)
        {
            ...
        }
    }
    

    which allows you to write:

    item.DeclarationType.IsComponentType()
    

    This encapsulates the classification logic and makes the code easier to read. If you have control over the DeclarationType you could also use attributes to decorate the values instead (plus adding some generic code to obtain classification based on the enum value attributes). The advantage is then it's obvious that you need to add something when a new enum member gets added.

  3. I would consider extracting this condition into its own method:

    parents.Contains(item) || parents.Any(parent => 
        (item.ParentDeclaration != null && item.ParentDeclaration.Equals(parent)) || item.ComponentName == parent.ComponentName)))
    

    because it's fairly complex and it's kind of cluttered. Extracting it into a method (which should probably live in Declaration if I followed the types correctly) means you can give this an expressive name which will make the whole thing easier to read.

  4. I'm not a fan of squeezing property get and set into one line. I find this easier on the eyes:

    public bool IsErrorState 
    { 
        get { return _isErrorState; } 
        set { _isErrorState = value; OnPropertyChanged(); }
    }
    
\$\endgroup\$
2
  • \$\begingroup\$ I've mentioned the arrays of DeclarationTypes to Hosch before @Mat'sMug. We would do well to centralize this kind of logic. Good answer Chris. ++ \$\endgroup\$
    – RubberDuck
    Commented Jan 19, 2016 at 19:19
  • \$\begingroup\$ Yup. I'm going with the extension methods. As for the dot delimiter, I'm going to actually make it user-configurable through some ICodeExplorerSettings interface. \$\endgroup\$ Commented Jan 20, 2016 at 1:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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