11
\$\begingroup\$

The next version of is going to include a pretty nifty "goto anything" / find symbol navigation tool that lets the user enter an identifier name in a combobox:

navigate to any identifier in any module


UI

To achieve the templated dropdown I had to introduce some WPF interop. Here's the markup:

<UserControl.CommandBindings>
    <CommandBinding Command="local:FindSymbolControl.GoCommand" 
                    Executed="CommandBinding_OnExecuted"
                    CanExecute="CommandBinding_OnCanExecute"/>
</UserControl.CommandBindings>

<Grid>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="32" />
    </Grid.ColumnDefinitions>

    <ComboBox IsEditable="True"
              ItemsSource="{Binding MatchResults}"
              SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}"
              IsTextSearchCaseSensitive="False"
              IsTextSearchEnabled="True"
              TextSearch.Text="{Binding SearchString, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
              TextSearch.TextPath="IdentifierName">

        <ComboBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel />
            </ItemsPanelTemplate>
        </ComboBox.ItemsPanel>

        <ComboBox.ItemTemplate>
            <DataTemplate DataType="local:SearchResult">
                <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                    <Image Height="16" Width="16" Margin="2,0,2,0" Source="{Binding Icon}" />
                    <TextBlock Margin="2,0,2,0" Text="{Binding IdentifierName}" FontWeight="Bold" MinWidth="160" VerticalAlignment="Center" />
                    <TextBlock Margin="2,0,2,0" Text="{Binding Location}" VerticalAlignment="Center" />
                </StackPanel>
            </DataTemplate>
        </ComboBox.ItemTemplate>
    </ComboBox>

    <Button Grid.Column="1"
            Command="local:FindSymbolControl.GoCommand">
        <Image Height="16" Source="pack://application:,,,/Rubberduck;component/Resources/arrow.png" />
    </Button>

</Grid>

...and the code-behind:

namespace Rubberduck.UI.FindSymbol
{
    /// <summary>
    /// Interaction logic for FindSymbolControl.xaml
    /// </summary>
    public partial class FindSymbolControl : UserControl
    {
        public FindSymbolControl()
        {
            InitializeComponent();
        }

        private FindSymbolViewModel ViewModel { get { return (FindSymbolViewModel)DataContext; } }

        private static readonly ICommand _goCommand = new RoutedCommand();
        public static ICommand GoCommand { get { return _goCommand; } }

        private void CommandBinding_OnExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            ViewModel.Execute();
        }

        private void CommandBinding_OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            if (ViewModel == null)
            {
                return;
            }

            e.CanExecute = ViewModel.CanExecute();
            e.Handled = true;
        }
    }
}

The WPF control is embedded inside a WinForms host, which is basically only responsible for setting the ViewModel:

namespace Rubberduck.UI.FindSymbol
{
    public partial class FindSymbolDialog : Form
    {
        public FindSymbolDialog(FindSymbolViewModel viewModel)
            : this()
        {
            findSymbolControl1.DataContext = viewModel;
        }

        public FindSymbolDialog()
        {
            InitializeComponent();
        }
    }
}

Logic

I implemented the logic inside the ViewModel class:

namespace Rubberduck.UI.FindSymbol
{
    public class FindSymbolViewModel : INotifyPropertyChanged
    {
        private static readonly DeclarationType[] ExcludedTypes =
        {
            DeclarationType.Control, 
            DeclarationType.ModuleOption
        };

        public FindSymbolViewModel(IEnumerable<Declaration> declarations, SearchResultIconCache cache)
        {
            _declarations = declarations;
            _cache = cache;
            var initialResults = _declarations
                .Where(declaration => !ExcludedTypes.Contains(declaration.DeclarationType))
                .OrderBy(declaration => declaration.IdentifierName.ToLowerInvariant())
                .Select(declaration => new SearchResult(declaration, cache[declaration]))
                .ToList();

            MatchResults = new ObservableCollection<SearchResult>(initialResults);
        }

        public event EventHandler<NavigateCodeEventArgs> Navigate;

        public bool CanExecute()
        {
            return _selectedItem != null;
        }

        public void Execute()
        {
            OnNavigate();
        }

        public void OnNavigate()
        {
            var handler = Navigate;
            if (handler != null && _selectedItem != null)
            {
                var arg = new NavigateCodeEventArgs(_selectedItem.Declaration);
                handler(this, arg);
            }
        }

        private readonly IEnumerable<Declaration> _declarations;
        private readonly SearchResultIconCache _cache;

        private void Search(string value)
        {
            var lower = value.ToLowerInvariant();
            var results = _declarations
                .Where(declaration => !ExcludedTypes.Contains(declaration.DeclarationType)
                                        && (string.IsNullOrEmpty(value) || declaration.IdentifierName.ToLowerInvariant().Contains(lower)))
                .OrderBy(declaration => declaration.IdentifierName.ToLowerInvariant())
                .Select(declaration => new SearchResult(declaration, _cache[declaration]))
                .ToList();

            MatchResults = new ObservableCollection<SearchResult>(results);
        }

        private string _searchString;

        public string SearchString
        {
            get { return _searchString; }
            set
            {
                _searchString = value; 
                Search(value);
            }
        }

        private SearchResult _selectedItem;

        public SearchResult SelectedItem
        {
            get { return _selectedItem; }
            set 
            { 
                _selectedItem = value; 
                OnPropertyChanged();
            }
        }

        private ObservableCollection<SearchResult> _matchResults;

        public ObservableCollection<SearchResult> MatchResults
        {
            get { return _matchResults; }
            set { _matchResults = value; OnPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

A SearchResult is simply wrapping up a Declaration for presentation purposes:

namespace Rubberduck.UI.FindSymbol
{
    public class SearchResult
    {
        private readonly Declaration _declaration;
        private readonly BitmapImage _icon;

        public SearchResult(Declaration declaration, BitmapImage icon)
        {
            _declaration = declaration;
            _icon = icon;
        }

        public Declaration Declaration { get { return _declaration; } }
        public string IdentifierName { get { return _declaration.IdentifierName; } }
        public string Location { get { return _declaration.Scope; } }

        public BitmapImage Icon { get { return _icon; } }

    }
}

I originally had an IValueConverter to convert a Declaration into a BitmapImage at the XAML level, but that meant creating an instance of a bitmap for every single search result, and felt inefficient.

So I implemented this "cache" instead:

namespace Rubberduck.UI.FindSymbol
{
    public class SearchResultIconCache
    {
        private readonly IDictionary<Tuple<DeclarationType, Accessibility>, BitmapImage> _images;

        public SearchResultIconCache()
        {
            var types = Enum.GetValues(typeof (DeclarationType)).Cast<DeclarationType>();
            var accessibilities = Enum.GetValues(typeof (Accessibility)).Cast<Accessibility>();

            _images = types.SelectMany(t => accessibilities.Select(a => Tuple.Create(t, a)))
                .ToDictionary(key => key, key => new BitmapImage(GetIconUri(key.Item1, key.Item2)));
        }

        public BitmapImage this[Declaration declaration]
        {
            get
            {
                var key = Tuple.Create(declaration.DeclarationType, declaration.Accessibility);
                return _images[key];
            }
        }

        private Uri GetIconUri(DeclarationType declarationType, Accessibility accessibility)
        {
            const string baseUri = @"../../Resources/Microsoft/PNG/";

            string path;
            switch (declarationType)
            {
                case DeclarationType.Module:
                    path = "VSObject_Module.png";
                    break;

                case DeclarationType.Document | DeclarationType.Class: 
                    path = "document.png";
                    break;

                case DeclarationType.UserForm | DeclarationType.Class | DeclarationType.Control:
                    path = "VSProject_Form.png";
                    break;

                case DeclarationType.Class | DeclarationType.Module:
                    path = "VSProject_Class.png";
                    break;

                case DeclarationType.Procedure | DeclarationType.Member:
                case DeclarationType.Function | DeclarationType.Member:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Method_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Method_Friend.png";
                        break;
                    }

                    path = "VSObject_Method.png";
                    break;

                case DeclarationType.PropertyGet | DeclarationType.Property | DeclarationType.Function:
                case DeclarationType.PropertyLet | DeclarationType.Property | DeclarationType.Procedure:
                case DeclarationType.PropertySet | DeclarationType.Property | DeclarationType.Procedure:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Properties_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Properties_Friend.png";
                        break;
                    }

                    path = "VSObject_Properties.png";
                    break;

                case DeclarationType.Parameter:
                    path = "VSObject_Field_Shortcut.png";
                    break;

                case DeclarationType.Variable:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Field_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Field_Friend.png";
                        break;
                    }

                    path = "VSObject_Field.png";
                    break;

                case DeclarationType.Constant:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Constant_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Constant_Friend.png";
                        break;
                    }

                    path = "VSObject_Constant.png";
                    break;

                case DeclarationType.Enumeration:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Enum_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Enum_Friend.png";
                        break;
                    }

                    path = "VSObject_Enum.png";
                    break;

                case DeclarationType.EnumerationMember | DeclarationType.Constant:
                    path = "VSObject_EnumItem.png";
                    break;

                case DeclarationType.Event:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_Event_Private.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_Event_Friend.png";
                        break;
                    }

                    path = "VSObject_Event.png";
                    break;

                case DeclarationType.UserDefinedType:
                    if (accessibility == Accessibility.Private)
                    {
                        path = "VSObject_ValueTypePrivate.png";
                        break;
                    }
                    if (accessibility == Accessibility.Friend)
                    {
                        path = "VSObject_ValueType_Friend.png";
                        break;
                    }

                    path = "VSObject_ValueType.png";
                    break;

                case DeclarationType.UserDefinedTypeMember | DeclarationType.Variable:
                    path = "VSObject_Field.png";
                    break;

                case DeclarationType.LibraryProcedure | DeclarationType.Procedure:
                case DeclarationType.LibraryFunction | DeclarationType.Function:
                    path = "VSObject_Method_Shortcut.png";
                    break;

                case DeclarationType.LineLabel:
                    path = "VSObject_Constant_Shortcut.png";
                    break;

                case DeclarationType.Project:
                    path = "VSObject_Library.png";
                    break;

                default:
                    path = "VSObject_Structure.png";
                    break;
            }

            return new Uri(baseUri + path, UriKind.Relative);
        }

    }
}

Usage

The calling code (a context menu button) uses it like this:

private void FindSymbolContextMenuClick(CommandBarButton Ctrl, ref bool CancelDefault)
{
    var declarations = _parser.Parse(IDE.ActiveVBProject, this).Declarations;
    var vm = new FindSymbolViewModel(declarations.Items.Where(item => !item.IsBuiltIn), _iconCache);
    vm.Navigate += vm_Navigate;
    using (var view = new FindSymbolDialog(vm))
    {
        view.ShowDialog();
    }

    vm.Navigate -= vm_Navigate;
}

private void vm_Navigate(object sender, NavigateCodeEventArgs e)
{
    if (e.QualifiedName.Component == null)
    {
        return;
    }

    try
    {
        e.QualifiedName.Component.CodeModule.CodePane.SetSelection(e.Selection);
    }
    catch (COMException)
    {
    }
}

For these bitwise-or operations to work I had to modify the DeclarationType enum a ...bit (pun not intended), by making it a [Flags] enum:

namespace Rubberduck.Parsing.Symbols
{
    [Flags]
    public enum DeclarationType
    {
        Project = 1 << 0,
        Module = 1 << 1,
        Class = 1 << 2,
        Control = 1 << 3,
        UserForm = 1 << 4,
        Document = 1 << 5,
        ModuleOption = 1 << 6,
        Member = 1 << 7,
        Procedure = 1 << 8 | Member,
        Function = 1 << 9 | Member,
        Property = 1 << 10 | Member,
        PropertyGet = 1 << 11 | Property | Function,
        PropertyLet = 1 << 12 | Property | Procedure,
        PropertySet = 1 << 13 | Property | Procedure,
        Parameter = 1 << 14,
        Variable = 1 << 15,
        Constant = 1 << 16,
        Enumeration = 1 << 17,
        EnumerationMember = 1 << 18 | Constant,
        Event = 1 << 19,
        UserDefinedType = 1 << 20,
        UserDefinedTypeMember = 1 << 21 | Variable,
        LibraryFunction = 1 << 22 | Function,
        LibraryProcedure = 1 << 23 | Procedure,
        LineLabel = 1 << 24
    }
}
\$\endgroup\$
4
\$\begingroup\$

It looks like you can do the filtering and sorting of _declarations just once in the view-model constructor (parse results don't change, and ExcludedTypes doesn't change):

_declarations = declarations
    .Where(declaration => !ExcludedTypes.Contains(declaration.DeclarationType))
    .OrderBy(declaration => declaration.IdentifierName.ToLowerInvariant())
    .ToList();

While we're at it, it should be more efficient to use the overload of OrderBy that takes an IComparer<T>, so we're not making lots of throw-away strings:

_declarations = declarations
    .Where(declaration => !ExcludedTypes.Contains(declaration.DeclarationType))
    .OrderBy(declaration => declaration.IdentifierName, StringComparer.OrdinalIgnoreCase)
    .ToList();

This also lets us simplify Search:

private void Search(string value)
{
    var declarations = _declarations;
    if (!string.IsNullOrEmpty(value))
    {
        declarations = declarations
            .Where(declaration => declaration.IdentifierName.IndexOf(value, StringComparison.OrdinalIgnoreCase) != -1);
    }

    var results = declarations
        .Select(declaration => new SearchResult(declaration, _cache[declaration]));

    MatchResults = new ObservableCollection<SearchResult>(results);
}

Here I've used IndexOf instead of Contains so we can pass it a StringComparer, again saving on the creation of string objects.

(Consider using string.IsNullOrWhiteSpace instead of string.IsNullOrEmpty depending on the behaviour you want.)

We can save a call to Search if _searchString hasn't changed. Not sure if it will help much in this case, but it's something to consider.

set
{
    if (_searchString == value)
    {
        return;
    }

    _searchString = value; 
    Search(value);
}
\$\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.