4
\$\begingroup\$

The third post in the Code Explorer series is about the UI and main VM.

This is the XAML, except a few hundred lines of styling:

<UserControl x:Class="Rubberduck.UI.CodeExplorer.CodeExplorerControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:codeExplorer="clr-namespace:Rubberduck.Navigation.CodeExplorer"
             xmlns:controls="clr-namespace:Rubberduck.UI.Controls"
             xmlns:themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
             xmlns:converters="clr-namespace:Rubberduck.UI.Converters"
             ResxExtension.DefaultResxName="Rubberduck.UI.RubberduckUI" 
             Language="{UICulture}"
             Name="CodeExplorer"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300" d:DataContext="{d:DesignInstance codeExplorer:CodeExplorerViewModel}">
    <UserControl.Resources>
        <BitmapImage x:Key="RefreshImage" UriSource="../../Resources/arrow-circle-double.png" />
        <BitmapImage x:Key="UndoImage" UriSource="../../Resources/arrow-circle-left.png" />
        <BitmapImage x:Key="PrintImage" UriSource="../../Resources/printer.png" />

        <BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
        <converters:BoolToHiddenVisibilityConverter x:Key="BoolToHiddenVisibility" />
        <converters:InvertBoolValueConverter x:Key="InvertBoolValue" />

        <Style x:Key="ShinyTreeView"
               TargetType="{x:Type TreeViewItem}">
            <Setter Property="BorderThickness" Value="1.5"/>
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
            <Setter Property="HorizontalAlignment" Value="Left" />
            <EventSetter Event="PreviewMouseRightButtonDown" Handler="OnPreviewMouseRightButtonDown" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="BorderBrush" Value="#adc6e5"/>
                </Trigger>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsSelected" Value="True"/>
                        <Condition Property="IsSelectionActive" Value="False"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="BorderBrush" Value="LightGray"/>
                </MultiTrigger>
            </Style.Triggers>
            <Style.Resources>
                <Style TargetType="Border">
                    <Setter Property="CornerRadius" Value="2"/>
                </Style>
            </Style.Resources>
        </Style>

        <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="MaxWidth" Value="200" />
            <Setter Property="Margin" Value="2,0,2,0" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="ToolTip" Value="{Binding Name}" />
            <Setter Property="ToolTipService.InitialShowDelay" Value="500" />
            <Setter Property="TextTrimming" Value="CharacterEllipsis" />
        </Style>

        <Style x:Key="TreeViewItemStyleWithSignatures" TargetType="TextBlock">
            <Setter Property="Text" Value="{Binding NameWithSignature}" />
            <Setter Property="MaxWidth" Value="200" />
            <Setter Property="Margin" Value="2,0,2,0" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="ToolTip" Value="{Binding NameWithSignature}" />
            <Setter Property="ToolTipService.InitialShowDelay" Value="500" />
            <Setter Property="TextTrimming" Value="CharacterEllipsis" />
        </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}" Visibility="{Binding ElementName=DisplaySignatures, Path=IsChecked, Converter={StaticResource BoolToHiddenVisibility}}" />
                <TextBlock Style="{StaticResource TreeViewItemStyleWithSignatures}" Visibility="{Binding ElementName=DisplaySignatures, Path=IsChecked, Converter={StaticResource BoolToVisibility}}" />
            </StackPanel>
        </HierarchicalDataTemplate>

        <HierarchicalDataTemplate x:Key="CodeExplorerTemplate" 
                                  DataType="codeExplorer:CodeExplorerProjectViewModel"
                                  ItemsSource="{Binding Items}">
            <StackPanel Orientation="Horizontal">
                <Image Style="{StaticResource TreeViewIconStyle}" />
                <TextBlock Style="{StaticResource TreeViewItemStyle}" Visibility="{Binding ElementName=DisplaySignatures, Path=IsChecked, Converter={StaticResource BoolToHiddenVisibility}}" />
                <TextBlock Style="{StaticResource TreeViewItemStyleWithSignatures}" Visibility="{Binding ElementName=DisplaySignatures, Path=IsChecked, Converter={StaticResource BoolToVisibility}}" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </UserControl.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="*" MinHeight="64" />
            <RowDefinition Height="5"/>
            <RowDefinition Height="Auto" MinHeight="48"/>
        </Grid.RowDefinitions>

        <Border Grid.Row="0" Grid.RowSpan="3" Background="#FFEEF5FD" />

        <ToolBarTray Grid.Row="0" IsLocked="True">
            <ToolBar Style="{DynamicResource ToolBarWithOverflowOnlyShowingWhenNeededStyle}">

                <Button Command="{Binding RefreshCommand}">
                    <Image Height="16" Source="../../Resources/arrow-circle-double.png" />
                    <Button.ToolTip>
                        <TextBlock Text="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=Refresh}" />
                    </Button.ToolTip>
                </Button>

                <Separator />

                <Menu>
                    <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=Add}">
                        <MenuItem.Icon>
                            <Image Height="16" Source="../../Resources/plus-circle.png" />
                        </MenuItem.Icon>
                        <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_AddTestModuleText}"
                                  Command="{Binding AddTestModuleCommand}"
                                  CommandParameter="{Binding SelectedItem}" />
                        <Separator />
                        <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_AddStdModuleText}"
                                  Command="{Binding AddStdModuleCommand}"
                                  CommandParameter="{Binding SelectedItem}" />
                        <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_AddClassModuleText}"
                                  Command="{Binding AddClassModuleCommand}"
                                  CommandParameter="{Binding SelectedItem}" />
                        <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_AddFormText}"
                                  Command="{Binding AddUserFormCommand}"
                                  CommandParameter="{Binding SelectedItem}" />
                    </MenuItem>
                </Menu>

                <Separator />

                <Menu Margin="0,0,2,0">
                    <MenuItem Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_Sort}">
                        <MenuItem.Icon>
                            <Image Height="16" Source="../../Resources/edit-list-order.png" />
                        </MenuItem.Icon>

                        <MenuItem Name="SortByName"
                                  Style="{DynamicResource MenuItemStyle}" VerticalAlignment="Center" 
                                  Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SortStyle_ByName}"
                                  IsChecked="{Binding SortByName, UpdateSourceTrigger=PropertyChanged}" 
                                  Command="{Binding SetNameSortCommand}"
                                  CommandParameter="{Binding ElementName=SortByName, Path=IsChecked}"
                                  IsCheckable="True" />
                        <MenuItem Name="SortBySelection"
                                  Style="{DynamicResource MenuItemStyle}" VerticalAlignment="Center" 
                                  Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SortStyle_BySelection}"
                                  IsChecked="{Binding SortBySelection, UpdateSourceTrigger=PropertyChanged}" 
                                  Command="{Binding SetSelectionSortCommand}"
                                  CommandParameter="{Binding ElementName=SortBySelection, Path=IsChecked}"
                                  IsCheckable="True" />

                        <Separator />

                        <MenuItem Style="{DynamicResource MenuItemStyle}" VerticalAlignment="Center" 
                                  Header="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SortStyle_ByType}"
                                  IsChecked="{Binding SortByType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                                  IsCheckable="True" />
                    </MenuItem>
                </Menu>
                <ToggleButton Name="DisplaySignatures" IsEnabled="{Binding CanRefresh}" ToolTip="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_ShowSignaturesToolTip}" IsChecked="True">
                    <Image Height="16" Source="../../Resources/Microsoft/PNG/DisplayFullSignature_13393_32.png" />
                </ToggleButton>

                <Separator />

                <Button Command="{Binding OpenDesignerCommand}" CommandParameter="{Binding SelectedItem}">
                    <Image Height="16" Source="../../Resources/Microsoft/PNG/VSProject_form.png" />
                    <Button.ToolTip>
                        <TextBlock Text="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_ShowDesignerToolTip}" />
                    </Button.ToolTip>
                </Button>

                <Separator />

                <Button Command="{Binding CopyResultsCommand}">
                    <Image Height="16" Source="../../Resources/document-copy.png" />
                    <Button.ToolTip>
                        <TextBlock Text="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_CopyToolTip}" />
                    </Button.ToolTip>
                </Button>

            </ToolBar>
        </ToolBarTray>

        <TreeView Grid.Row="1" Background="White"
                  ItemContainerStyle="{StaticResource ShinyTreeView}"
                  HorizontalContentAlignment="Stretch"
                  MouseDoubleClick="TreeView_OnMouseDoubleClick"
                  Style="{StaticResource CodeExplorerTreeViewStyle}" BorderThickness="0,1">
            <i:Interaction.Behaviors>
                <controls:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
            </i:Interaction.Behaviors>
        </TreeView>

        <controls:BusyIndicator Grid.Row="1" Width="36" Height="36" Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />

        <GridSplitter Grid.Row="2" Height="5" ShowsPreview="True" Cursor="SizeNS" HorizontalAlignment="Stretch"/>

        <Border Grid.Row="3" BorderThickness="0,1,0,0" BorderBrush="DimGray">

            <StackPanel Orientation="Vertical" MinHeight="48" Background="WhiteSmoke">

                <Grid Margin="4" HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="20" />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Image Style="{StaticResource IconStyle}" VerticalAlignment="Top"
                           Source="{Binding SelectedItem.CollapsedIcon}" Grid.Column="0"/>
                    <TextBlock Margin="4" Text="{Binding PanelTitle}" FontWeight="Bold"
                               TextWrapping="WrapWithOverflow" Grid.Column="1"/>
                </Grid>

                <TextBlock Margin="4" Text="{Binding Description}" FontSize="10" TextWrapping="WrapWithOverflow"/>

                <WrapPanel>
                    <controls:LinkButton Margin="4"
                            Visibility="{Binding CanExecuteIndenterCommand, Converter={StaticResource BoolToVisibility}, UpdateSourceTrigger=PropertyChanged}"
                            Command="{Binding IndenterCommand}"
                            CommandParameter="{Binding SelectedItem}"
                            Content="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=SmartIndenterMenu}" />
                    <controls:LinkButton Margin="4"
                            Visibility="{Binding CanExecuteRenameCommand, Converter={StaticResource BoolToVisibility}}"
                            Command="{Binding RenameCommand}"
                            CommandParameter="{Binding SelectedItem}"
                            Content="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=Rename}" />
                    <controls:LinkButton Margin="4"
                            Visibility="{Binding CanExecuteFindAllReferencesCommand, Converter={StaticResource BoolToVisibility}}"
                            Command="{Binding FindAllReferencesCommand}"
                            CommandParameter="{Binding SelectedItem}"
                            Content="{Resx ResxName=Rubberduck.UI.RubberduckUI, Key=CodeExplorer_FindAllReferencesText}" />
                </WrapPanel>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

Here is the code-behind:

    private CodeExplorerViewModel ViewModel { get { return DataContext as CodeExplorerViewModel; } }

private void TreeView_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    if (ViewModel != null && ViewModel.SelectedItem != null)
    {
        ViewModel.NavigateCommand.Execute(ViewModel.SelectedItem);
    }
}

private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    ((TreeViewItem)sender).IsSelected = true;
}

And here is the main VM:

public sealed class CodeExplorerViewModel : ViewModelBase, IDisposable
{
    private readonly FolderHelper _folderHelper;
    private readonly RubberduckParserState _state;

    public CodeExplorerViewModel(FolderHelper folderHelper, RubberduckParserState state, List<ICommand> commands)
    {
        _folderHelper = folderHelper;
        _state = state;
        _state.StateChanged += ParserState_StateChanged;

        _refreshCommand = new DelegateCommand(param => _state.OnParseRequested(this),
            param => !IsBusy && _state.IsDirty());

        _refreshComponentCommand = commands.OfType<CodeExplorer_RefreshComponentCommand>().FirstOrDefault();
        _navigateCommand = commands.OfType<CodeExplorer_NavigateCommand>().FirstOrDefault();

        _addTestModuleCommand = commands.OfType<CodeExplorer_AddTestModuleCommand>().FirstOrDefault();
        _addStdModuleCommand = commands.OfType<CodeExplorer_AddStdModuleCommand>().FirstOrDefault();
        _addClassModuleCommand = commands.OfType<CodeExplorer_AddClassModuleCommand>().FirstOrDefault();
        _addUserFormCommand = commands.OfType<CodeExplorer_AddUserFormCommand>().FirstOrDefault();

        _openDesignerCommand = commands.OfType<CodeExplorer_OpenDesignerCommand>().FirstOrDefault();
        _renameCommand = commands.OfType<CodeExplorer_RenameCommand>().FirstOrDefault();
        _indenterCommand = commands.OfType<CodeExplorer_IndentCommand>().FirstOrDefault();

        _findAllReferencesCommand = commands.OfType<CodeExplorer_FindAllReferencesCommand>().FirstOrDefault();
        _findAllImplementationsCommand = commands.OfType<CodeExplorer_FindAllImplementationsCommand>().FirstOrDefault();

        _importCommand = commands.OfType<CodeExplorer_ImportCommand>().FirstOrDefault();
        _exportCommand = commands.OfType<CodeExplorer_ExportCommand>().FirstOrDefault();
        _externalRemoveCommand = commands.OfType<CodeExplorer_RemoveCommand>().FirstOrDefault();
        if (_externalRemoveCommand != null)
        {
            _removeCommand = new DelegateCommand(ExecuteRemoveComand, _externalRemoveCommand.CanExecute);
        }

        _printCommand = commands.OfType<CodeExplorer_PrintCommand>().FirstOrDefault();

        _commitCommand = commands.OfType<CodeExplorer_CommitCommand>().FirstOrDefault();
        _undoCommand = commands.OfType<CodeExplorer_UndoCommand>().FirstOrDefault();

        _copyResultsCommand = commands.OfType<CodeExplorer_CopyResultsCommand>().FirstOrDefault();

        _setNameSortCommand = new DelegateCommand(param =>
        {
            SortByName = (bool)param;
            SortBySelection = !(bool)param;
        });

        _setSelectionSortCommand = new DelegateCommand(param =>
        {
            SortBySelection = (bool)param;
            SortByName = !(bool)param;
        });
    }

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

            if (_selectedItem is CodeExplorerProjectViewModel)
            {
                var vbe = _selectedItem.GetSelectedDeclaration().Project.VBE;
                var project = vbe.VBProjects.Cast<VBProject>().FirstOrDefault(f => f.HelpFile == _selectedItem.GetSelectedDeclaration().Project.HelpFile);

                if (project != null)
                {
                    vbe.ActiveVBProject = project;
                }
            }

            // ReSharper disable ExplicitCallerInfoArgument
            OnPropertyChanged("CanExecuteIndenterCommand");
            OnPropertyChanged("CanExecuteRenameCommand");
            OnPropertyChanged("CanExecuteFindAllReferencesCommand");
            OnPropertyChanged("PanelTitle");
            OnPropertyChanged("Description");
            // ReSharper restore ExplicitCallerInfoArgument
        }
    }

    private bool _sortByName = true;
    public bool SortByName
    {
        get { return _sortByName; }
        set
        {
            if (_sortByName == value)
            {
                return;
            }

            _sortByName = value;
            OnPropertyChanged();

            ReorderChildNodes(Projects);
        }
    }

    private bool _sortBySelection;
    public bool SortBySelection
    {
        get { return _sortBySelection; }
        set
        {
            if (_sortBySelection == value)
            {
                return;
            }

            _sortBySelection = value;
            OnPropertyChanged();

            ReorderChildNodes(Projects);
        }
    }

    private readonly ICommand _copyResultsCommand;
    public ICommand CopyResultsCommand { get { return _copyResultsCommand; } }

    private readonly ICommand _setNameSortCommand;
    public ICommand SetNameSortCommand { get { return _setNameSortCommand; } }

    private readonly ICommand _setSelectionSortCommand;
    public ICommand SetSelectionSortCommand { get { return _setSelectionSortCommand; } }

    private bool _sortByType = true;
    public bool SortByType
    {
        get { return _sortByType; }
        set
        {
            if (_sortByType != value)
            {
                _sortByType = value;
                OnPropertyChanged();

                ReorderChildNodes(Projects);
            }
        }
    }

    private bool _isBusy;
    public bool IsBusy
    {
        get { return _isBusy; }
        set
        {
            _isBusy = value;
            OnPropertyChanged();
        }
    }

    public string PanelTitle
    {
        get
        {
            if (SelectedItem == null)
            {
                return string.Empty;
            }

            if (SelectedItem is CodeExplorerProjectViewModel)
            {
                var node = (CodeExplorerProjectViewModel) SelectedItem;
                return node.Declaration.IdentifierName + string.Format(" - ({0})", node.Declaration.DeclarationType);
            }

            if (SelectedItem is CodeExplorerComponentViewModel)
            {
                var node = (CodeExplorerComponentViewModel) SelectedItem;
                return node.Declaration.IdentifierName + string.Format(" - ({0})", node.Declaration.DeclarationType);
            }

            if (SelectedItem is CodeExplorerMemberViewModel)
            {
                var node = (CodeExplorerMemberViewModel) SelectedItem;
                return node.Declaration.IdentifierName + string.Format(" - ({0})", node.Declaration.DeclarationType);
            }

            return SelectedItem.Name;
        }
    }

    public string Description
    {
        get
        {
            if (SelectedItem is ICodeExplorerDeclarationViewModel)
            {
                return ((ICodeExplorerDeclarationViewModel) SelectedItem).Declaration.DescriptionString;
            }

            if (SelectedItem is CodeExplorerCustomFolderViewModel)
            {
                return ((CodeExplorerCustomFolderViewModel) SelectedItem).FolderAttribute;
            }

            return string.Empty;
        }
    }

    public bool CanExecuteIndenterCommand { get { return IndenterCommand.CanExecute(SelectedItem); } }
    public bool CanExecuteRenameCommand { get { return RenameCommand.CanExecute(SelectedItem); } }
    public bool CanExecuteFindAllReferencesCommand { get { return FindAllReferencesCommand.CanExecute(SelectedItem); } }

    private ObservableCollection<CodeExplorerItemViewModel> _projects;
    public ObservableCollection<CodeExplorerItemViewModel> Projects
    {
        get { return _projects; }
        set
        {
            _projects = new ObservableCollection<CodeExplorerItemViewModel>(value.OrderBy(o => o.NameWithSignature));

            ReorderChildNodes(_projects);
            OnPropertyChanged();
        }
    }

    private void ParserState_StateChanged(object sender, EventArgs e)
    {
        if (Projects == null)
        {
            Projects = new ObservableCollection<CodeExplorerItemViewModel>();
        }

        IsBusy = _state.Status == ParserState.Parsing;
        if (_state.Status != ParserState.Ready)
        {
            return;
        }

        var userDeclarations = _state.AllUserDeclarations
            .GroupBy(declaration => declaration.Project)
            .Where(grouping => grouping.Key != null)
            .ToList();

        if (userDeclarations.Any(
                grouping => grouping.All(declaration => declaration.DeclarationType != DeclarationType.Project)))
        {
            return;
        }

        var newProjects = userDeclarations.Select(grouping =>
            new CodeExplorerProjectViewModel(_folderHelper,
                grouping.SingleOrDefault(declaration => declaration.DeclarationType == DeclarationType.Project),
                grouping)).ToList();

        UpdateNodes(Projects, newProjects);

        Projects = new ObservableCollection<CodeExplorerItemViewModel>(newProjects);
    }

    private void UpdateNodes(IEnumerable<CodeExplorerItemViewModel> oldList,
        IEnumerable<CodeExplorerItemViewModel> newList)
    {
        foreach (var item in newList)
        {
            CodeExplorerItemViewModel oldItem;

            if (item is CodeExplorerCustomFolderViewModel)
            {
                oldItem = oldList.FirstOrDefault(i => i.Name == item.Name);
            }
            else
            {
                oldItem = oldList.FirstOrDefault(i =>
                    item.QualifiedSelection != null && i.QualifiedSelection != null &&
                    i.QualifiedSelection.Value.QualifiedName.ProjectId ==
                    item.QualifiedSelection.Value.QualifiedName.ProjectId &&
                    i.QualifiedSelection.Value.QualifiedName.ComponentName ==
                    item.QualifiedSelection.Value.QualifiedName.ComponentName &&
                    i.QualifiedSelection.Value.Selection == item.QualifiedSelection.Value.Selection);
            }

            if (oldItem != null)
            {
                item.IsExpanded = oldItem.IsExpanded;

                if (oldItem.Items.Any() && item.Items.Any())
                {
                    UpdateNodes(oldItem.Items, item.Items);
                }
            }
        }
    }

    private void ParserState_ModuleStateChanged(object sender, Parsing.ParseProgressEventArgs e)
    {
        if (e.State != ParserState.Error)
        {
            return;
        }

        var componentProject = e.Component.Collection.Parent;
        var projectNode = Projects.OfType<CodeExplorerProjectViewModel>()
            .FirstOrDefault(p => p.Declaration.Project == componentProject);

        if (projectNode == null)
        {
            return;
        }

        SetErrorState(projectNode, e.Component);

        if (_errorStateSet) { return; }

        // at this point, we know the node is newly added--we have to add a new node, not just change the icon of the old one.

        var folderNode = projectNode.Items.FirstOrDefault(f => f is CodeExplorerCustomFolderViewModel && f.Name == componentProject.Name);

        UiDispatcher.Invoke(() =>
        {
            if (folderNode == null)
            {
                folderNode = new CodeExplorerCustomFolderViewModel(projectNode, componentProject.Name,
                    componentProject.Name);
                projectNode.AddChild(folderNode);
            }

            var declaration = CreateDeclaration(e.Component);
            var newNode = new CodeExplorerComponentViewModel(folderNode, declaration, new List<Declaration>())
            {
                IsErrorState = true
            };

            folderNode.AddChild(newNode);

            // Force a refresh. OnPropertyChanged("Projects") didn't work.
            Projects = Projects;
        });
    }

    private Declaration CreateDeclaration(VBComponent component)
    {
        var projectDeclaration =
            _state.AllUserDeclarations.FirstOrDefault(item =>
                    item.DeclarationType == DeclarationType.Project &&
                    item.Project.VBComponents.Cast<VBComponent>().Contains(component));

        if (component.Type == vbext_ComponentType.vbext_ct_StdModule)
        {
            return new ProceduralModuleDeclaration(
                    new QualifiedMemberName(new QualifiedModuleName(component), component.Name), projectDeclaration,
                    component.Name, false, new List<IAnnotation>(), null);
        }

        return new ClassModuleDeclaration(new QualifiedMemberName(new QualifiedModuleName(component), component.Name),
                projectDeclaration, component.Name, false, new List<IAnnotation>(), null);
    }

    private void ReorderChildNodes(IEnumerable<CodeExplorerItemViewModel> nodes)
    {
        foreach (var node in nodes)
        {
            node.ReorderItems(SortByName, SortByType);
            ReorderChildNodes(node.Items);
        }
    }

    private bool _errorStateSet;
    private void SetErrorState(CodeExplorerItemViewModel itemNode, VBComponent component)
    {
        _errorStateSet = false;

        foreach (var node in itemNode.Items)
        {
            if (node is CodeExplorerCustomFolderViewModel)
            {
                SetErrorState(node, component);
            }

            if (_errorStateSet)
            {
                return;
            }

            if (node is CodeExplorerComponentViewModel)
            {
                var componentNode = (CodeExplorerComponentViewModel) node;
                if (componentNode.GetSelectedDeclaration().QualifiedName.QualifiedModuleName.Component == component)
                {
                    componentNode.IsErrorState = true;
                    _errorStateSet = true;
                }
            }
        }
    }

    private readonly ICommand _refreshCommand;
    public ICommand RefreshCommand { get { return _refreshCommand; } }

    private readonly ICommand _refreshComponentCommand;
    public ICommand RefreshComponentCommand { get { return _refreshComponentCommand; } }

    private readonly ICommand _navigateCommand;
    public ICommand NavigateCommand { get { return _navigateCommand; } }

    private readonly ICommand _addTestModuleCommand;
    public ICommand AddTestModuleCommand { get { return _addTestModuleCommand; } }

    private readonly ICommand _addStdModuleCommand;
    public ICommand AddStdModuleCommand { get { return _addStdModuleCommand; } }

    private readonly ICommand _addClassModuleCommand;
    public ICommand AddClassModuleCommand { get { return _addClassModuleCommand; } }

    private readonly ICommand _addUserFormCommand;
    public ICommand AddUserFormCommand { get { return _addUserFormCommand; } }

    private readonly ICommand _openDesignerCommand;
    public ICommand OpenDesignerCommand { get { return _openDesignerCommand; } }

    private readonly ICommand _renameCommand;
    public ICommand RenameCommand { get { return _renameCommand; } }

    private readonly ICommand _indenterCommand;
    public ICommand IndenterCommand { get { return _indenterCommand; } }

    private readonly ICommand _findAllReferencesCommand;
    public ICommand FindAllReferencesCommand { get { return _findAllReferencesCommand; } }

    private readonly ICommand _findAllImplementationsCommand;
    public ICommand FindAllImplementationsCommand { get { return _findAllImplementationsCommand; } }

    private readonly ICommand _importCommand;
    public ICommand ImportCommand { get { return _importCommand; } }

    private readonly ICommand _exportCommand;
    public ICommand ExportCommand { get { return _exportCommand; } }

    private readonly ICommand _removeCommand;
    public ICommand RemoveCommand { get { return _removeCommand; } }

    private readonly ICommand _printCommand;
    public ICommand PrintCommand { get { return _printCommand; } }

    private readonly ICommand _commitCommand;
    public ICommand CommitCommand { get { return _commitCommand; } }

    private readonly ICommand _undoCommand;
    public ICommand UndoCommand { get { return _undoCommand; } }

    private readonly ICommand _externalRemoveCommand;

    // this is a special case--we have to reset SelectedItem to prevent a crash
    private void ExecuteRemoveComand(object param)
    {
        var node = (CodeExplorerComponentViewModel) SelectedItem;
        SelectedItem = Projects.First(p => ((CodeExplorerProjectViewModel) p).Declaration.Project == node.Declaration.Project);

        _externalRemoveCommand.Execute(param);
    }

    public void Dispose()
    {
        if (_state != null)
        {
            _state.StateChanged -= ParserState_StateChanged;
            _state.ModuleStateChanged -= ParserState_ModuleStateChanged;
        }
    }
}

I'm pretty satisfied with this, but I'm sure there are things that could be improved.

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

I don't like this:

_refreshComponentCommand = commands.OfType<CodeExplorer_RefreshComponentCommand>().FirstOrDefault();
_navigateCommand = commands.OfType<CodeExplorer_NavigateCommand>().FirstOrDefault();

_addTestModuleCommand = commands.OfType<CodeExplorer_AddTestModuleCommand>().FirstOrDefault();
_addStdModuleCommand = commands.OfType<CodeExplorer_AddStdModuleCommand>().FirstOrDefault();
_addClassModuleCommand = commands.OfType<CodeExplorer_AddClassModuleCommand>().FirstOrDefault();
_addUserFormCommand = commands.OfType<CodeExplorer_AddUserFormCommand>().FirstOrDefault();

_openDesignerCommand = commands.OfType<CodeExplorer_OpenDesignerCommand>().FirstOrDefault();
_renameCommand = commands.OfType<CodeExplorer_RenameCommand>().FirstOrDefault();
_indenterCommand = commands.OfType<CodeExplorer_IndentCommand>().FirstOrDefault();

_findAllReferencesCommand = commands.OfType<CodeExplorer_FindAllReferencesCommand>().FirstOrDefault();
_findAllImplementationsCommand = commands.OfType<CodeExplorer_FindAllImplementationsCommand>().FirstOrDefault();

_importCommand = commands.OfType<CodeExplorer_ImportCommand>().FirstOrDefault();
_exportCommand = commands.OfType<CodeExplorer_ExportCommand>().FirstOrDefault();
_externalRemoveCommand = commands.OfType<CodeExplorer_RemoveCommand>().FirstOrDefault();

The use of .FirstOrDefault makes it look like commands could possibly have more than one instance of each command which, if that's the case, would indicate a bug with whoever is injecting that List<ICommand>, which could really be an IReadOnlyList<ICommand>, since you're iterating it multiple times, but you're not interested in adding or removing items.

Using .SingleOrDefault would not only better convey the idea that it's only supposed to find one, it would also throw an InvalidOperationException if that's not the case, and that would put a problem under the spotlight - a problem that may or may not exist right now, that using FirstOrDefault potentially hides.


When you're determining the PanelTitle value...

var node = (CodeExplorerMemberViewModel) SelectedItem;
return node.Declaration.IdentifierName + string.Format(" - ({0})", node.Declaration.DeclarationType);

You're using the DeclarationType enum's string representation, which is its name as it appears in code. This should be a localized string instead.


In ExecuteRemoveCommand, you're comparing Project objects:

SelectedItem = Projects.First(p => ((CodeExplorerProjectViewModel) p).Declaration.Project == node.Declaration.Project);

Comparing VBProject object instances like this is known to be unreliable, you should be comparing ProjectId values instead. And again, why First when you only expect to match one?

\$\endgroup\$
2
\$\begingroup\$

You could demerge common setters of the TreeViewItemStyle into a base style:

    <Style x:Key="TreeViewItemStyleBase" TargetType="TextBlock">
        <Setter Property="MaxWidth" Value="200" />
        <Setter Property="Margin" Value="2,0,2,0" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="ToolTipService.InitialShowDelay" Value="500" />
        <Setter Property="TextTrimming" Value="CharacterEllipsis" />
    </Style>    

    <Style x:Key="TreeViewItemStyle" TargetType="TextBlock" BasedOn="{StaticResource TreeViewItemStyleBase}">
        <Setter Property="Text" Value="{Binding Name}" />
        <Setter Property="ToolTip" Value="{Binding Name}" />
    </Style>

    <Style x:Key="TreeViewItemStyleWithSignatures" TargetType="TextBlock" BasedOn="{StaticResource TreeViewItemStyleBase}">
        <Setter Property="Text" Value="{Binding NameWithSignature}" />
        <Setter Property="ToolTip" Value="{Binding NameWithSignature}" />
    </Style>
\$\endgroup\$

Your Answer

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