Following up on CommandBars, Buttons and Commands: Take 2, I've refactored quite a lot of the Rubberduck code base (like, a whole third of the entire code base has been affected), and successfully untangled the RubberduckMenu
and gave each command its own place to be.
So, "Shared" commands that can be executed from the main "Rubberduck" menu, or from the code pane context menu, or a COM CommandBar
, WinForms Toolstrip
or WPF Button
, can be easily reused. These commands derive from the CommandBase
abstract class, which implements the WPF ICommand
interface:
CommandBase abstract class
using System;
using System.Runtime.InteropServices;
using System.Windows.Input;
namespace Rubberduck.UI.Command
{
[ComVisible(false)]
public abstract class CommandBase : ICommand
{
public virtual bool CanExecute(object parameter)
{
return true;
}
public abstract void Execute(object parameter);
public event EventHandler CanExecuteChanged;
protected void OnCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler.Invoke(this, EventArgs.Empty);
}
}
}
}
Here is one such "shared" command:
RunAllTestsCommand implementation
using Rubberduck.UI.UnitTesting;
using Rubberduck.UnitTesting;
namespace Rubberduck.UI.Command
{
/// <summary>
/// A command that runs all Rubberduck unit tests in the VBE.
/// </summary>
public class RunAllTestsCommand : CommandBase
{
private readonly ITestEngine _engine;
private readonly TestExplorerModelBase _model;
public RunAllTestsCommand(ITestEngine engine, TestExplorerModelBase model)
{
_engine = engine;
_model = model;
}
public override void Execute(object parameter)
{
_model.Refresh();
_model.ClearLastRun();
_engine.Run(_model.Tests);
}
}
}
There are many, may other commands (quite a bunch yet to be actually implemented), but the idea is the same: "shared" commands get their dependencies in their constructor, and can be called from anywhere; commands that are specific to a given ViewModel (say, the "export inspection results" command in the CodeInspectionResultsViewModel
), are implemented in that ViewModel as DelegateCommand
s - the result is a decoupled architecture, that the IoC container brings together.
The IoC container being Ninject, I've implemented a NinjectModule
dedicated to configuring the menus and command bars - works great, but I'd like it cleaner; any ideas?
CommandBarsModule class
namespace Rubberduck.Root
{
public class CommandBarsModule : NinjectModule
{
private readonly IKernel _kernel;
public CommandBarsModule(IKernel kernel)
{
_kernel = kernel;
}
public override void Load()
{
BindCommandsToMenuItems();
ConfigureRubberduckMenu();
ConfigureCodePaneContextMenu();
ConfigureFormDesignerContextMenu();
ConfigureFormDesignerControlContextMenu();
ConfigureProjectExplorerContextMenu();
}
private void ConfigureRubberduckMenu()
{
const int windowMenuId = 30009;
var parent = _kernel.Get<VBE>().CommandBars["Menu Bar"].Controls;
var beforeIndex = FindRubberduckMenuInsertionIndex(parent, windowMenuId);
var items = GetRubberduckMenuItems();
BindParentMenuItem<RubberduckParentMenu>(parent, beforeIndex, items);
}
private void ConfigureCodePaneContextMenu()
{
const int listMembersMenuId = 2529;
var parent = _kernel.Get<VBE>().CommandBars["Code Window"].Controls;
var beforeIndex = parent.Cast<CommandBarControl>().First(control => control.Id == listMembersMenuId).Index;
var items = GetCodePaneContextMenuItems();
BindParentMenuItem<RubberduckParentMenu>(parent, beforeIndex, items);
}
private void ConfigureFormDesignerContextMenu()
{
const int viewCodeMenuId = 2558;
var parent = _kernel.Get<VBE>().CommandBars["MSForms"].Controls;
var beforeIndex = parent.Cast<CommandBarControl>().First(control => control.Id == viewCodeMenuId).Index;
var items = GetFormDesignerContextMenuItems();
BindParentMenuItem<FormDesignerContextParentMenu>(parent, beforeIndex, items);
}
private void ConfigureFormDesignerControlContextMenu()
{
const int viewCodeMenuId = 2558;
var parent = _kernel.Get<VBE>().CommandBars["MSForms Control"].Controls;
var beforeIndex = parent.Cast<CommandBarControl>().First(control => control.Id == viewCodeMenuId).Index;
var items = GetFormDesignerContextMenuItems();
BindParentMenuItem<FormDesignerControlContextParentMenu>(parent, beforeIndex, items);
}
private void ConfigureProjectExplorerContextMenu()
{
const int projectPropertiesMenuId = 2578;
var parent = _kernel.Get<VBE>().CommandBars["Project Window"].Controls;
var beforeIndex = parent.Cast<CommandBarControl>().First(control => control.Id == projectPropertiesMenuId).Index;
var items = GetProjectWindowContextMenuItems();
BindParentMenuItem<ProjectWindowContextParentMenu>(parent, beforeIndex, items);
}
private void BindParentMenuItem<TParentMenu>(CommandBarControls parent, int beforeIndex, IEnumerable<IMenuItem> items)
{
_kernel.Bind<IParentMenuItem>().To(typeof(TParentMenu))
.InSingletonScope()
.WithConstructorArgument("items", items)
.WithConstructorArgument("beforeIndex", beforeIndex)
.WithPropertyValue("Parent", parent);
}
private static int FindRubberduckMenuInsertionIndex(CommandBarControls controls, int beforeId)
{
for (var i = 1; i <= controls.Count; i++)
{
if (controls[i].BuiltIn && controls[i].Id == beforeId)
{
return i;
}
}
return controls.Count;
}
private void BindCommandsToMenuItems()
{
//_kernel.Bind<ICommand>().To<NavigateCommand>().InSingletonScope();
_kernel.Bind<IDeclarationNavigator>().To<NavigateAllImplementations>().WhenTargetHas<FindImplementationsAttribute>().InSingletonScope();
_kernel.Bind<IDeclarationNavigator>().To<NavigateAllReferences>().WhenTargetHas<FindReferencesAttribute>().InSingletonScope();
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.Namespace != null && type.Namespace.StartsWith(typeof(CommandBase).Namespace ?? string.Empty))
.ToList();
// note: ICommand naming convention: [Foo]Command
var commands = types.Where(type => type.IsClass && type.BaseType == typeof(CommandBase) && type.Name.EndsWith("Command"));
foreach (var command in commands)
{
var commandName = command.Name.Substring(0, command.Name.Length - "Command".Length);
try
{
// note: ICommandMenuItem naming convention for [Foo]Command: [Foo]CommandMenuItem
var item = types.SingleOrDefault(type => type.Name == commandName + "CommandMenuItem");
if (item != null)
{
_kernel.Bind(item).ToSelf().InSingletonScope();
_kernel.Bind<ICommand>().To(command).WhenInjectedInto(item).InSingletonScope();
}
}
catch (InvalidOperationException exception)
{
// rename one of the classes, "FooCommand" is expected to match exactly 1 "FooBarXyzCommandMenuItem"
}
}
}
private IEnumerable<IMenuItem> GetRubberduckMenuItems()
{
return new IMenuItem[]
{
_kernel.Get<AboutCommandMenuItem>(),
_kernel.Get<OptionsCommandMenuItem>(),
_kernel.Get<RunCodeInspectionsCommandMenuItem>(),
_kernel.Get<ShowSourceControlPanelCommandMenuItem>(),
GetUnitTestingParentMenu(),
GetRefactoringsParentMenu(),
GetNavigateParentMenu(),
};
}
private IMenuItem GetUnitTestingParentMenu()
{
var items = new IMenuItem[]
{
_kernel.Get<RunAllTestsCommandMenuItem>(),
_kernel.Get<TestExplorerCommandMenuItem>(),
_kernel.Get<AddTestModuleCommandMenuItem>(),
_kernel.Get<AddTestMethodCommandMenuItem>(),
_kernel.Get<AddTestMethodExpectedErrorCommandMenuItem>(),
};
return new UnitTestingParentMenu(items);
}
private IMenuItem GetRefactoringsParentMenu()
{
var items = new IMenuItem[]
{
_kernel.Get<RefactorRenameCommandMenuItem>(),
_kernel.Get<RefactorExtractMethodCommandMenuItem>(),
_kernel.Get<RefactorReorderParametersCommandMenuItem>(),
_kernel.Get<RefactorRemoveParametersCommandMenuItem>(),
};
return new RefactoringsParentMenu(items);
}
private IMenuItem GetNavigateParentMenu()
{
var items = new IMenuItem[]
{
_kernel.Get<CodeExplorerCommandMenuItem>(),
_kernel.Get<ToDoExplorerCommandMenuItem>(),
_kernel.Get<FindSymbolCommandMenuItem>(),
_kernel.Get<FindAllReferencesCommandMenuItem>(),
_kernel.Get<FindAllImplementationsCommandMenuItem>(),
};
return new NavigateParentMenu(items);
}
private IEnumerable<IMenuItem> GetCodePaneContextMenuItems()
{
return new IMenuItem[]
{
GetRefactoringsParentMenu(),
_kernel.Get<FindSymbolCommandMenuItem>(),
_kernel.Get<FindAllReferencesCommandMenuItem>(),
_kernel.Get<FindAllImplementationsCommandMenuItem>(),
};
}
private IEnumerable<IMenuItem> GetFormDesignerContextMenuItems()
{
return new IMenuItem[]
{
_kernel.Get<RefactorRenameCommandMenuItem>(),
};
}
private IEnumerable<IMenuItem> GetProjectWindowContextMenuItems()
{
return new IMenuItem[]
{
_kernel.Get<RefactorRenameCommandMenuItem>(),
_kernel.Get<FindSymbolCommandMenuItem>(),
_kernel.Get<FindAllReferencesCommandMenuItem>(),
_kernel.Get<FindAllImplementationsCommandMenuItem>(),
};
}
}
}