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\$
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, privacy policy and cookie policy

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