8
\$\begingroup\$

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 DelegateCommands - 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>(),
            };
        }
    }
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

These four methods contain a lot of duplication:

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);
}

Let's start by introducing a method ConfigureContextMenu:

private void ConfigureCodePaneContextMenu()
{
    const int listMembersMenuId = 2529;
    ConfigureContextMenu<RubberduckParentMenu>(listMembersMenuId, "Code Window", GetCodePaneContextMenuItems());
}

private void ConfigureFormDesignerContextMenu()
{
    const int viewCodeMenuId = 2558;
    ConfigureContextMenu<FormDesignerContextParentMenu>(viewCodeMenuId, "MSForms", GetFormDesignerContextMenuItems());
}

private void ConfigureFormDesignerControlContextMenu()
{
    const int viewCodeMenuId = 2558;
    ConfigureContextMenu<FormDesignerControlContextParentMenu>(viewCodeMenuId, "MSForms Control", GetFormDesignerContextMenuItems());
}

private void ConfigureProjectExplorerContextMenu()
{
    const int projectPropertiesMenuId = 2578;
    ConfigureContextMenu<ProjectWindowContextParentMenu>(projectPropertiesMenuId, "Project Window", GetProjectWindowContextMenuItems());
}

private void ConfigureContextMenu<T>(int menuId, string commandBarId, IEnumerable<IMenuItem> menuItems)
{
    var parent = _kernel.Get<VBE>().CommandBars[commandBarId].Controls;
    var beforeIndex = parent.Cast<CommandBarControl>().First(control => control.Id == menuId).Index;
    BindParentMenuItem<T>(parent, beforeIndex, menuItems);
}

By doing this we can see that they aren't really so much different methods, as they are the same method operating on different data.

You could take it a step further and introduce a private class ContextMenuConfiguration:

const int listMembersMenuId = 2529;
const int viewCodeMenuId = 2558;
const int projectPropertiesMenuId = 2578;

var codePaneContextMenuConfiguration =
    new ContextMenuConfiguration(listMembersMenuId, "Code Window", GetCodePaneContextMenuItems(), typeof(RubberduckParentMenu));

var formDesignerContextMenuConfiguration =
    new ContextMenuConfiguration(viewCodeMenuId, "MSForms", GetFormDesignerContextMenuItems(), typeof(FormDesignerContextParentMenu));

var formDesignerControlContextMenuConfiguration =
    new ContextMenuConfiguration(viewCodeMenuId, "MSForms Control", GetFormDesignerContextMenuItems(), typeof(FormDesignerControlContextParentMenu));

var projectExplorerContextMenuConfiguration =
    new ContextMenuConfiguration(projectPropertiesMenuId, "Project Window", GetProjectWindowContextMenuItems(), typeof(ProjectWindowContextParentMenu));

var contextMenuConfigurations = new[]
{
    codePaneContextMenuConfiguration,
    formDesignerContextMenuConfiguration,
    formDesignerControlContextMenuConfiguration,
    projectExplorerContextMenuConfiguration
};

foreach (var configuration in contextMenuConfigurations)
{
    ConfigureContextMenu(configuration);
}
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.