Rubberduck's Code Explorer was recently redesigned from scratch:
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.
Dispose()
es of the various commands that implementIDisposable
? \$\endgroup\$