6
\$\begingroup\$

Rubberduck's Code Explorer was recently redesigned from scratch:

Rubberduck Code Explorer

Anything from modern features, such as virtual folders (limitation of the VBE--it doesn't support real folders), to ancient features in the original VBE, such as printing.

Here are some of the commands I wrote that I'd like to get reviewed:

The Indent command. This command calls our Smart Indenter port and works on any node. On member nodes, procedures are indented (nothing happens if the node is a field); for component nodes, the selected component is indented; on folders, all components in the folder are indented; and on project nodes, all components in the project are indented.

public class CodeExplorer_IndentCommand : CommandBase
{
    private readonly RubberduckParserState _state;
    private readonly IIndenter _indenter;
    private readonly INavigateCommand _navigateCommand;

    public CodeExplorer_IndentCommand(RubberduckParserState state, IIndenter indenter, INavigateCommand navigateCommand)
    {
        _state = state;
        _indenter = indenter;
        _navigateCommand = navigateCommand;
    }

    public override bool CanExecute(object parameter)
    {
        if (parameter is CodeExplorerComponentViewModel)
        {
            var node = (CodeExplorerComponentViewModel)parameter;
            if (node.Declaration.Annotations.Any(a => a.AnnotationType == AnnotationType.NoIndent))
            {
                return false;
            }
        }

        if (parameter is CodeExplorerProjectViewModel)
        {
            if (_state.Status != ParserState.Ready)
            {
                return false;
            }

            var declaration = ((ICodeExplorerDeclarationViewModel)parameter).Declaration;
            return _state.AllUserDeclarations
                        .Any(c => c.DeclarationType.HasFlag(DeclarationType.Module) &&
                        c.Annotations.All(a => a.AnnotationType != AnnotationType.NoIndent) &&
                        c.Project == declaration.Project);
        }

        if (parameter is CodeExplorerCustomFolderViewModel)
        {
            if (_state.Status != ParserState.Ready)
            {
                return false;
            }

            var node = (CodeExplorerCustomFolderViewModel) parameter;
            return node.Items.OfType<CodeExplorerComponentViewModel>()
                    .Select(s => s.Declaration)
                    .Any(d => d.Annotations.All(a => a.AnnotationType != AnnotationType.NoIndent));
        }

        return _state.Status == ParserState.Ready && !(parameter is CodeExplorerErrorNodeViewModel);
    }

    public override void Execute(object parameter)
    {
        var node = (CodeExplorerItemViewModel)parameter;

        if (!node.QualifiedSelection.HasValue && !(node is CodeExplorerCustomFolderViewModel))
        {
            return;
        }

        if (node is CodeExplorerProjectViewModel)
        {
            var declaration = ((ICodeExplorerDeclarationViewModel)node).Declaration;

            var components = _state.AllUserDeclarations.Where(c => 
                        c.DeclarationType.HasFlag(DeclarationType.Module) &&
                        c.Annotations.All(a => a.AnnotationType != AnnotationType.NoIndent) &&
                        c.Project == declaration.Project);

            foreach (var component in components)
            {
                _indenter.Indent(component.QualifiedName.QualifiedModuleName.Component);
            }
        }

        if (node is CodeExplorerCustomFolderViewModel)
        {
            var components = node.Items.OfType<CodeExplorerComponentViewModel>()
                    .Select(s => s.Declaration)
                    .Where(d => d.Annotations.All(a => a.AnnotationType != AnnotationType.NoIndent))
                    .Select(d => d.QualifiedName.QualifiedModuleName.Component);

            foreach (var component in components)
            {
                _indenter.Indent(component);
            }
        }

        if (node is CodeExplorerComponentViewModel)
        {
            _indenter.Indent(node.QualifiedSelection.Value.QualifiedName.Component);
        }

        if (node is CodeExplorerMemberViewModel)
        {
            _navigateCommand.Execute(node.QualifiedSelection.Value.GetNavitationArgs());

            _indenter.IndentCurrentProcedure();
        }
    }
}

The Import command. This command replaces the Project Explorer's Import command, only it allows you to select multiple files at once, and will import them all. Currently, it aborts if any of the selected files is invalid--should I make it so it imports all valid files and pops a message with a list of any invalid files (if there are any) instead?

public class CodeExplorer_ImportCommand : CommandBase, IDisposable
{
    private readonly IOpenFileDialog _openFileDialog;

    public CodeExplorer_ImportCommand(IOpenFileDialog openFileDialog)
    {
        _openFileDialog = openFileDialog;

        _openFileDialog.AddExtension = true;
        _openFileDialog.AutoUpgradeEnabled = true;
        _openFileDialog.CheckFileExists = true;
        _openFileDialog.Multiselect = true;
        _openFileDialog.ShowHelp = false;   // we don't want 1996's file picker.
        _openFileDialog.Filter = @"VB Files|*.cls;*.bas;*.frm";
        _openFileDialog.CheckFileExists = true;
    }

    public override bool CanExecute(object parameter)
    {
        // I could import to a folder as well, if I had a
        // MoveToFolder refactoring to call
        return parameter is ICodeExplorerDeclarationViewModel;
    }

    public override void Execute(object parameter)
    {
        // I know this will never be null because of the CanExecute
        var project = ((ICodeExplorerDeclarationViewModel)parameter).Declaration.QualifiedName.QualifiedModuleName.Project;

        if (_openFileDialog.ShowDialog() == DialogResult.OK)
        {
            var fileExts = _openFileDialog.FileNames.Select(s => s.Split('.').Last());
            if (fileExts.Any(fileExt => !new[] {"bas", "cls", "frm"}.Contains(fileExt)))
            {
                return;
            }

            foreach (var filename in _openFileDialog.FileNames)
            {
                project.VBComponents.Import(filename);
            }
        }
    }

    public void Dispose()
    {
        if (_openFileDialog != null)
        {
            _openFileDialog.Dispose();
        }
    }
}

The Export command. This is a full replacement for the Project Explorer's Export command:

public class CodeExplorer_ExportCommand : CommandBase, IDisposable
{
    private readonly ISaveFileDialog _saveFileDialog;
    private readonly Dictionary<vbext_ComponentType, string> _exportableFileExtensions = new Dictionary<vbext_ComponentType, string>
    {
        { vbext_ComponentType.vbext_ct_StdModule, ".bas" },
        { vbext_ComponentType.vbext_ct_ClassModule, ".cls" },
        { vbext_ComponentType.vbext_ct_Document, ".cls" },
        { vbext_ComponentType.vbext_ct_MSForm, ".frm" }
    };

    public CodeExplorer_ExportCommand(ISaveFileDialog saveFileDialog)
    {
        _saveFileDialog = saveFileDialog;
        _saveFileDialog.OverwritePrompt = true;
    }

    public override bool CanExecute(object parameter)
    {
        if (!(parameter is CodeExplorerComponentViewModel))
        {
            return false;
        }

        var node = (CodeExplorerComponentViewModel)parameter;
        var componentType = node.Declaration.QualifiedName.QualifiedModuleName.Component.Type;
        return _exportableFileExtensions.Select(s => s.Key).Contains(componentType);
    }

    public override void Execute(object parameter)
    {
        var node = (CodeExplorerComponentViewModel)parameter;
        var component = node.Declaration.QualifiedName.QualifiedModuleName.Component;

        string ext;
        _exportableFileExtensions.TryGetValue(component.Type, out ext);

        _saveFileDialog.FileName = component.Name + ext;
        var result = _saveFileDialog.ShowDialog();

        if (result == DialogResult.OK)
        {
            component.Export(_saveFileDialog.FileName);
        }
    }

    public void Dispose()
    {
        if (_saveFileDialog != null)
        {
            _saveFileDialog.Dispose();
        }
    }
}

The Remove command. This is a full replacement for the Project Explorer's Remove command.

public class CodeExplorer_RemoveCommand : CommandBase, IDisposable
{
    private readonly ISaveFileDialog _saveFileDialog;
    private readonly IMessageBox _messageBox;

    private readonly Dictionary<vbext_ComponentType, string> _exportableFileExtensions = new Dictionary<vbext_ComponentType, string>
    {
        { vbext_ComponentType.vbext_ct_StdModule, ".bas" },
        { vbext_ComponentType.vbext_ct_ClassModule, ".cls" },
        { vbext_ComponentType.vbext_ct_Document, ".cls" },
        { vbext_ComponentType.vbext_ct_MSForm, ".frm" }
    };

    public CodeExplorer_RemoveCommand(ISaveFileDialog saveFileDialog, IMessageBox messageBox)
    {
        _saveFileDialog = saveFileDialog;
        _saveFileDialog.OverwritePrompt = true;

        _messageBox = messageBox;
    }

    public override bool CanExecute(object parameter)
    {
        if (!(parameter is CodeExplorerComponentViewModel))
        {
            return false;
        }

        var node = (CodeExplorerComponentViewModel)parameter;
        var componentType = node.Declaration.QualifiedName.QualifiedModuleName.Component.Type;
        return _exportableFileExtensions.Select(s => s.Key).Contains(componentType);
    }

    public override void Execute(object parameter)
    {
        var message = string.Format("Do you want to export '{0}' before removing?", ((CodeExplorerComponentViewModel)parameter).Name);
        var result = _messageBox.Show(message, "Rubberduck Export Prompt", MessageBoxButtons.YesNoCancel,
                MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);

        if (result == DialogResult.Cancel)
        {
            return;
        }

        if (result == DialogResult.Yes && !ExportFile((CodeExplorerComponentViewModel)parameter))
        {
            return;
        }

        // No file export or file successfully exported--now remove it

        // I know this will never be null because of the CanExecute
        var declaration = ((CodeExplorerComponentViewModel)parameter).Declaration;

        var project = declaration.QualifiedName.QualifiedModuleName.Project;
        project.VBComponents.Remove(declaration.QualifiedName.QualifiedModuleName.Component);
    }

    private bool ExportFile(CodeExplorerComponentViewModel node)
    {
        var component = node.Declaration.QualifiedName.QualifiedModuleName.Component;

        string ext;
        _exportableFileExtensions.TryGetValue(component.Type, out ext);

        _saveFileDialog.FileName = component.Name + ext;
        var result = _saveFileDialog.ShowDialog();

        if (result == DialogResult.OK)
        {
            component.Export(_saveFileDialog.FileName);
        }

        return result == DialogResult.OK;
    }

    public void Dispose()
    {
        if (_saveFileDialog != null)
        {
            _saveFileDialog.Dispose();
        }
    }
}

The Print command. This is a full replacement for the Project Explorer's Print command, down to not having any margins on the printed page. Should I extract the print logic, or is it OK the way I have it embedded like this? Does it make a difference that this is the only place RD ever prints?

public class CodeExplorer_PrintCommand : CommandBase
{
    public override bool CanExecute(object parameter)
    {
        var node = parameter as CodeExplorerComponentViewModel;
        if (node == null)
        {
            return false;
        }

        return node.Declaration.QualifiedName.QualifiedModuleName.Component.CodeModule.CountOfLines != 0;
    }

    public override void Execute(object parameter)
    {
        var node = (CodeExplorerComponentViewModel)parameter;
        var component = node.Declaration.QualifiedName.QualifiedModuleName.Component;

        var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Rubberduck",
            component.Name + ".txt");

        var text = component.CodeModule.Lines[1, component.CodeModule.CountOfLines].Split(new[] {Environment.NewLine}, StringSplitOptions.None).ToList();

        var printDoc = new PrintDocument { DocumentName = path };
        using (var pd = new PrintDialog
        {
            Document = printDoc,
            AllowCurrentPage = true,
            AllowSelection = true,
            AllowPrintToFile = true,
            AllowSomePages = true,
            UseEXDialog = true
        })
        {
            if (pd.ShowDialog() != DialogResult.OK)
            {
                return;
            }

            var offsetY = 0;
            var pageHeight = pd.PrinterSettings.PaperSizes[0].Height;

            var index = 0;

            printDoc.PrintPage += (sender, printPageArgs) =>
            {
                while (index < text.Count)
                {
                    var font = new Font(new FontFamily("Consolas"), 10, FontStyle.Regular);
                    printPageArgs.Graphics.DrawString(text[index++], font, Brushes.Black, 0, offsetY, new StringFormat());

                    offsetY += font.Height;

                    if (offsetY >= pageHeight)
                    {
                        printPageArgs.HasMorePages = true;
                        offsetY = 0;
                        return;
                    }

                    printPageArgs.HasMorePages = false;
                }
            };

            printDoc.Print();
        }
    }
}

The Undo command. This command uses our very own Source Control feature, and undoes any changes on the selected component:

public class CodeExplorer_UndoCommand : CommandBase
{
    private readonly SourceControlDockablePresenter _presenter;
    private readonly IMessageBox _messageBox;

    public CodeExplorer_UndoCommand(SourceControlDockablePresenter presenter, IMessageBox messageBox)
    {
        _presenter = presenter;
        _messageBox = messageBox;
    }

    public override bool CanExecute(object parameter)
    {
        var node = parameter as CodeExplorerComponentViewModel;
        if (node == null)
        {
            return false;
        }

        var panel = _presenter.Window() as SourceControlPanel;
        if (panel == null)
        {
            return false;
        }

        var panelVM = panel.ViewModel as SourceControlViewViewModel;
        if (panelVM == null)
        {
            return false;
        }

        panelVM.SetTab(SourceControlTab.Changes);
        var changesVM = panelVM.SelectedItem.ViewModel as ChangesViewViewModel;

        return changesVM != null && changesVM.IncludedChanges != null &&
               changesVM.IncludedChanges.Select(s => s.FilePath).Contains(GetFileName(node));
    }

    public override void Execute(object parameter)
    {
        var panel = _presenter.Window() as SourceControlPanel;
        if (panel == null)
        {
            return;
        }

        var panelVM = panel.ViewModel as SourceControlViewViewModel;
        if (panelVM == null)
        {
            return;
        }

        panelVM.SetTab(SourceControlTab.Changes);
        var changesVM = panelVM.SelectedItem.ViewModel as ChangesViewViewModel;
        if (changesVM == null)
        {
            return;
        }

        var fileName = GetFileName((CodeExplorerComponentViewModel) parameter);
        var result = _messageBox.Show(string.Format(RubberduckUI.SourceControl_UndoPrompt, fileName),
            RubberduckUI.SourceControl_UndoTitle, System.Windows.Forms.MessageBoxButtons.OKCancel,
            System.Windows.Forms.MessageBoxIcon.Warning, System.Windows.Forms.MessageBoxDefaultButton.Button2);

        if (result != System.Windows.Forms.DialogResult.OK)
        {
            return;
        }

        changesVM.UndoChangesToolbarButtonCommand.Execute(new FileStatusEntry(fileName, FileStatus.Modified));
        _presenter.Show();
    }

    private string GetFileName(CodeExplorerComponentViewModel node)
    {
        var component = node.Declaration.QualifiedName.QualifiedModuleName.Component;

        var fileExtensions = new Dictionary<vbext_ComponentType, string>
        {
            { vbext_ComponentType.vbext_ct_StdModule, ".bas" },
            { vbext_ComponentType.vbext_ct_ClassModule, ".cls" },
            { vbext_ComponentType.vbext_ct_Document, ".cls" },
            { vbext_ComponentType.vbext_ct_MSForm, ".frm" }
        };

        string ext;
        fileExtensions.TryGetValue(component.Type, out ext);
        return component.Name + ext;
    }
}

This is written using C# 5, so please don't mention any C# 6 enhancements. All other comments are welcome.

\$\endgroup\$
2
  • \$\begingroup\$ What Dispose()es of the various commands that implement IDisposable? \$\endgroup\$ May 16, 2016 at 18:34
  • 1
    \$\begingroup\$ Ninject disposes them. \$\endgroup\$
    – user34073
    May 16, 2016 at 18:53

1 Answer 1

5
\$\begingroup\$

CodeExplorer_ImportCommand

public override void Execute(object parameter)
{
    // I know this will never be null because of the CanExecute
    var project = ((ICodeExplorerDeclarationViewModel)parameter).Declaration.QualifiedName.QualifiedModuleName.Project;

If you know it, you can Assert it. Instead of an InvalidCastException, you get a very clear "The original developer thought this should never happen" message.

CodeExplorer_PrintCommand

Your print command is creating a new Font instance once for each iteration of the loop. Creating it outside the loop would save on some memory and reduce garbage collection. I'm sure RD is beginning to be a memory hog. It'd be prudent to pay attention to such things.

CanExecute / Execute

You're doing all of the parameter validation in both of these methods. You could DRY this up by calling CanExecute from inside of the Execute method, then doing direct casts instead of safe ones. I would also consider throwing an InvalidOperationException if Execute is called while it's not in a valid state to be called.

public override void Execute(object parameter)
{
    if ( !CanExecute(parameter) )
    {
        throw new InvalidOperationException("Be sure to check to see if we CanExecute before calling Execute!");
     }

    // now it's safe to use direct casts instead, at least in a single threaded environment. 

}

Just had a thought. The command could be made thread safe by putting a lock on the parameter object, making it as good as your original implementation.

public override void Execute(object parameter)
{
    lock(parameter)
    {
        if ( !CanExecute(parameter) )
        {
            throw new InvalidOperationException("Be sure to check to see if we CanExecute before calling Execute!");
         }

    // now it's really safe to do the direct casts

     } // exit lock
}
\$\endgroup\$
3
  • \$\begingroup\$ Sorry. I got pulled away mid answer. I added a small addition. \$\endgroup\$
    – RubberDuck
    May 14, 2016 at 21:23
  • \$\begingroup\$ By definition, Execute will never run unless CanExecute returns true (the menu item is disabled automatically). \$\endgroup\$
    – user34073
    May 14, 2016 at 21:24
  • \$\begingroup\$ Not entirely true. Yes, the framework will never call it, but nothing stops a developer from creating and executing a command outside of the GUI & WPF. \$\endgroup\$
    – RubberDuck
    May 14, 2016 at 21:26

Your Answer

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