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:
...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?