4
\$\begingroup\$

Continuing the series of Code Explorer posts, here is the collection of view models for the tree nodes:

Code Explorer

This is the interface for nodes with a declaration.

public interface ICodeExplorerDeclarationViewModel
{
    Declaration Declaration { get; }
}

And here is the abstract view model they are all based on:

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

    public bool IsExpanded { get; set; }

    public abstract string Name { get; }
    public abstract string NameWithSignature { get; }
    public abstract BitmapImage CollapsedIcon { get; }
    public abstract BitmapImage ExpandedIcon { get; }
    public abstract CodeExplorerItemViewModel Parent { get; }

    public abstract QualifiedSelection? QualifiedSelection { 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 Declaration GetSelectedDeclaration()
    {
        return this is ICodeExplorerDeclarationViewModel
            ? ((ICodeExplorerDeclarationViewModel)this).Declaration
            : null;
    }

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

    public void ReorderItems(bool sortByName, bool sortByType)
    {
        if (sortByType)
        {
            Items = sortByName
                ? Items.OrderBy(o => o, new CompareByType()).ThenBy(t => t, new CompareByName()).ToList()
                : Items.OrderBy(o => o, new CompareByType()).ThenBy(t => t, new CompareBySelection()).ToList();

            return;
        }

        Items = sortByName
            ? Items.OrderBy(t => t, new CompareByName()).ToList()
            : Items.OrderBy(t => t, new CompareBySelection()).ToList();
    }
}

This VM is for project nodes:

public class CodeExplorerProjectViewModel : CodeExplorerItemViewModel, ICodeExplorerDeclarationViewModel
{
    private readonly Declaration _declaration;
    public Declaration Declaration { get { return _declaration; } }
    private readonly CodeExplorerCustomFolderViewModel _folderTree;

    private static readonly DeclarationType[] ComponentTypes =
    {
        DeclarationType.ClassModule, 
        DeclarationType.Document, 
        DeclarationType.ProceduralModule, 
        DeclarationType.UserForm, 
    };

    public CodeExplorerProjectViewModel(FolderHelper folderHelper, Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _declaration = declaration;
        _name = _declaration.IdentifierName;
        IsExpanded = true;
        _folderTree = folderHelper.GetFolderTree(declaration);

        try
        {
            FillFolders(declarations.ToList());
            Items = _folderTree.Items.ToList();

            _icon = _declaration.Project.Protection == vbext_ProjectProtection.vbext_pp_locked
                ? GetImageSource(resx.lock__exclamation)
                : GetImageSource(resx.VSObject_Library);
        }
        catch (NullReferenceException e)
        {
            Console.WriteLine(e);
        }
    }

    private void FillFolders(IEnumerable<Declaration> declarations)
    {
        var items = declarations.ToList();
        var groupedItems = items.Where(item => ComponentTypes.Contains(item.DeclarationType))
                           .GroupBy(item => item.CustomFolder)
                           .OrderBy(item => item.Key);

        foreach (var grouping in groupedItems)
        {
            AddNodesToTree(_folderTree, items, grouping);
        }
    }

    private bool AddNodesToTree(CodeExplorerCustomFolderViewModel tree, List<Declaration> items, IGrouping<string, Declaration> grouping)
    {
        foreach (var folder in tree.Items.OfType<CodeExplorerCustomFolderViewModel>())
        {
            if (grouping.Key.Replace("\"", string.Empty) != folder.FullPath)
            {
                continue;
            }

            if (folder.Parent.Name == string.Empty)
            {
                folder.SetParent(this);
            }

            var parents = grouping.Where(
                    item => ComponentTypes.Contains(item.DeclarationType) &&
                        item.CustomFolder.Replace("\"", string.Empty) == folder.FullPath)
                    .ToList();

            folder.AddNodes(items.Where(item => parents.Contains(item) || parents.Any(parent =>
                (item.ParentDeclaration != null && item.ParentDeclaration.Equals(parent)) ||
                item.ComponentName == parent.ComponentName)).ToList());

            return true;
        }

        return tree.Items.OfType<CodeExplorerCustomFolderViewModel>().Any(node => AddNodesToTree(node, items, grouping));
    }

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

    // projects are always at the top of the tree
    public override CodeExplorerItemViewModel Parent { get { return null; } }

    private string _name;
    public override string Name { get { return _name; } }
    public override string NameWithSignature { get { return _name; } }
    public override QualifiedSelection? QualifiedSelection { get { return _declaration.QualifiedSelection; } }

    public void SetParenthesizedName(string parenthesizedName)
    {
        _name += " (" + parenthesizedName + ")";
    }
}

The folders' VM:

public class CodeExplorerCustomFolderViewModel : CodeExplorerItemViewModel
{
    private readonly string _fullPath;
    private readonly string _name;
    private readonly string _folderAttribute;
    private static readonly DeclarationType[] ComponentTypes =
    {
        DeclarationType.ClassModule, 
        DeclarationType.Document, 
        DeclarationType.ProceduralModule, 
        DeclarationType.UserForm, 
    };

    public CodeExplorerCustomFolderViewModel(CodeExplorerItemViewModel parent, string name, string fullPath)
    {
        _parent = parent;
        _fullPath = fullPath;
        _name = name.Replace("\"", string.Empty);
        _folderAttribute = string.Format("@Folder(\"{0}\")", fullPath.Replace("\"", string.Empty));

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

    public void AddNodes(List<Declaration> declarations)
    {
        var parents = declarations.GroupBy(item => item.ComponentName).OrderBy(item => item.Key).ToList();
        foreach (var component in parents)
        {
            try
            {
                var moduleName = component.Key;
                var parent = declarations.Single(item =>
                    ComponentTypes.Contains(item.DeclarationType) && item.ComponentName == moduleName);
                var members = declarations.Where(item =>
                    !ComponentTypes.Contains(item.DeclarationType) && item.ComponentName == moduleName);

                AddChild(new CodeExplorerComponentViewModel(this, parent, members));
            }
            catch (InvalidOperationException exception)
            {
                Console.WriteLine(exception);
            }
        }
    }

    public string FolderAttribute { get { return _folderAttribute; } }

    public string FullPath { get { return _fullPath; } }

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

    public override QualifiedSelection? QualifiedSelection { get { return null; } }

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

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

    // I have to set the parent from a different location than
    // the node is created because of the folder helper
    internal void SetParent(CodeExplorerItemViewModel parent)
    {
        _parent = parent;
    }

    private CodeExplorerItemViewModel _parent;
    public override CodeExplorerItemViewModel Parent { get { return _parent; } }
}

The VM for components:

public class CodeExplorerComponentViewModel : CodeExplorerItemViewModel, ICodeExplorerDeclarationViewModel
{
    private readonly Declaration _declaration;
    public Declaration Declaration { get { return _declaration; } }

    private readonly CodeExplorerItemViewModel _parent;
    public override CodeExplorerItemViewModel Parent { get { return _parent; } }

    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(CodeExplorerItemViewModel parent, Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _parent = parent;
        _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(this, item, grouping)))
                            .ToList<CodeExplorerItemViewModel>();

        _name = _declaration.IdentifierName;

        var component = declaration.QualifiedName.QualifiedModuleName.Component;
        if (component.Type == vbext_ComponentType.vbext_ct_Document)
        {
            try
            {
                var parenthesizedName = component.Properties.Item("Name").Value.ToString();

                if (ContainsBuiltinDocumentPropertiesProperty())
                {
                    CodeExplorerItemViewModel node = this;
                    while (node.Parent != null)
                    {
                        node = node.Parent;
                    }

                    ((CodeExplorerProjectViewModel) node).SetParenthesizedName(parenthesizedName);
                }
                else
                {
                    _name += " (" + parenthesizedName + ")";
                }
            }
            catch
            {
                // gotcha! (this means that the property either doesn't exist or we weren't able to get it for some reason)
            }
        }
    }

    private bool ContainsBuiltinDocumentPropertiesProperty()
    {
        var component = _declaration.QualifiedName.QualifiedModuleName.Component;

        try
        {
            component.Properties.Item("BuiltinDocumentProperties");
        }
        catch
        {
            // gotcha! (this means that the property either doesn't exist or we weren't able to get it for some reason)
            return false;
        }

        return true;
    }

    private bool _isErrorState;
    public bool IsErrorState
    {
        get { return _isErrorState; }
        set
        {
            _isErrorState = value;
            _icon = GetImageSource(resx.Error);


            foreach (var item in Items)
            {
                ((CodeExplorerMemberViewModel) item).ParentComponentHasError();
            }

            OnPropertyChanged();
            OnPropertyChanged("CollapsedIcon");
            OnPropertyChanged("ExpandedIcon");
        }
    }

    public bool IsTestModule
    {
        get
        {
            return _declaration.DeclarationType == DeclarationType.ProceduralModule
                   && _declaration.Annotations.Any(annotation => annotation.AnnotationType == AnnotationType.TestModule);
        }
    }

    private readonly string _name;
    public override string Name { get { return _name; } }
    public override string NameWithSignature { get { return _name; } }

    public override QualifiedSelection? QualifiedSelection { get { return _declaration.QualifiedSelection; } }

    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.ClassModule },
        { vbext_ComponentType.vbext_ct_StdModule, DeclarationType.ProceduralModule },
        { vbext_ComponentType.vbext_ct_Document, DeclarationType.Document },
        { vbext_ComponentType.vbext_ct_MSForm, DeclarationType.UserForm }
    };

    private DeclarationType DeclarationType
    {
        get
        {
            var result = DeclarationType.ClassModule;
            try
            {
                DeclarationTypes.TryGetValue(ComponentType, out result);
            }
            catch (COMException exception)
            {
                Console.WriteLine(exception);
            }
            return result;
        }
    }

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

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

And the VM for members:

public class CodeExplorerMemberViewModel : CodeExplorerItemViewModel, ICodeExplorerDeclarationViewModel
{
    private readonly Declaration _declaration;
    public Declaration Declaration { get { return _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_Constant)},
            { 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(CodeExplorerItemViewModel parent, Declaration declaration, IEnumerable<Declaration> declarations)
    {
        _parent = parent;

        _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(this, item, null))
                                .ToList<CodeExplorerItemViewModel>();
        }

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

        _name = DetermineMemberName(declaration);
        _icon = Mappings[key];
    }

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

    private readonly CodeExplorerItemViewModel _parent;
    public override CodeExplorerItemViewModel Parent { get { return _parent; } }

    private string _signature = null;
    public override string NameWithSignature
    {
        get
        {
            if (_signature != null)
            {
                return _signature;
            }

            var context =
                _declaration.Context.children.FirstOrDefault(d => d is VBAParser.ArgListContext) as VBAParser.ArgListContext;

            if (context == null)
            {
                _signature = Name;
            }
            else if (_declaration.DeclarationType == DeclarationType.PropertyGet 
                  || _declaration.DeclarationType == DeclarationType.PropertyLet 
                  || _declaration.DeclarationType == DeclarationType.PropertySet)
            {
                // 6 being the three-letter "get/let/set" + parens + space
                _signature = Name.Insert(Name.Length - 6, context.GetText()); 
            }
            else
            {
                _signature = Name + context.GetText();
            }
            return _signature;
        }
    }

    public override QualifiedSelection? QualifiedSelection { get { return _declaration.QualifiedSelection; } }

    private static string DetermineMemberName(Declaration declaration)
    {
        var type = declaration.DeclarationType;
        switch (type)
        {
            case DeclarationType.PropertyGet:
                return declaration.IdentifierName + " (Get)";
            case DeclarationType.PropertyLet:
                return declaration.IdentifierName + " (Let)";
            case DeclarationType.PropertySet:
                return declaration.IdentifierName + " (Set)";
            case DeclarationType.Variable:
                if (declaration.IsArray)
                {
                    return declaration.IdentifierName + "()";
                }
                return declaration.IdentifierName;
            case DeclarationType.Constant:
                var valuedDeclaration = (ConstantDeclaration)declaration;
                return valuedDeclaration.IdentifierName + " = " + valuedDeclaration.Expression;

            default:
                return declaration.IdentifierName;
        }
    }

    public void ParentComponentHasError()
    {
        _icon = GetImageSource(resx.Warning);
        OnPropertyChanged("CollapsedIcon");
        OnPropertyChanged("ExpandedIcon");
    }

    private BitmapImage _icon;
    public override BitmapImage CollapsedIcon { get { return _icon; } }
    public override BitmapImage ExpandedIcon { get { return _icon; } }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

GetSelectedDeclaration is ugly and a little bit uncomprehensive. CodeExplorerItemViewModel does not implement ICodeExplorerDeclarationViewModel, so this method can only return something on a subclass of it like CodeExplorerProjectViewModel. I would prefer to have a virtual method and override it.

//on `CodeExplorerItemViewModel`
public virtual Declaration GetSelectedDeclaration()
{
    return null;
}

//on `CodeExplorerProjectViewModel `
public override Declaration GetSelectedDeclaration()
{
    return Declaration;
}

ReorderItems is ok but there's room for improvement. What you know is that the OrderBy may be performed by any comparer and it may be needed a ThenBy if sortByType is selected. You also always need one instance of CompareByName or CompareBySelection. I sugest an implementation where you start by instatiating the right comparer:

public void ReorderItems(bool sortByName, bool sortByType)
{
    var comparer = sortByName ? new CompareByName() : new CompareBySelection();

    if(sortByType){
        Items = Items
            .OrderBy(o => o, new CompareByType())
            .ThenBy(comparer)
            .ToList();

    }else{
        Items = Items
            .OrderBy(o => o, comparer))
            .ToList();
    }
}

Your AddNodesToTree could use a variable to store the nodes before adding them to folder.

var nodes = items.Where(item => parents.Contains(item) || 
                parents.Any(parent =>
                    (item.ParentDeclaration != null && item.ParentDeclaration.Equals(parent)) ||
                    item.ComponentName == parent.ComponentName)
                ).ToList()

folder.AddNodes(nodes);

CodeExplorerItemViewModel could have a Root property that would return the first node, sou you wouldn't have to write the while on CodeExplorerComponentViewModel-

\$\endgroup\$

Your Answer

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