One of the biggest pain point in the entire rubberduck code base, is the way commandbar menus and menu items are created and wired up - something needs to be done to straighten that up, and while I was refactoring the App
class and got to the point where I needed to address the dependencies of RubberduckMenu
, I had an idea: instead of creating menu buttons and wiring up event handlers, I could be creating commands and associating them with as many buttons as needed!
For example, I could have a RefactorRenameCommand
that could be called from the Rubberduck/Refactorings menu, or from the code pane context menu, or from the form designer's context menu, or whatever.
So I created a Rubberduck.UI.Commands
namespace, and wrote an abstract class to encapsulate the concept:
namespace Rubberduck.UI.Commands
{
/// <summary>
/// Base class to derive all menu commands from.
/// </summary>
public abstract class RubberduckCommandBase
{
private readonly IRubberduckMenuCommand _command;
protected RubberduckCommandBase(IRubberduckMenuCommand command)
{
_command = command;
_command.RequestExecute += command_RequestExecute;
}
private void command_RequestExecute(object sender, EventArgs e)
{
ExecuteAction();
}
protected IRubberduckMenuCommand Command { get { return _command; } }
/// <summary>
/// A method that uses the <see cref="Command"/> helper to wire up as many UI controls as needed.
/// </summary>
public abstract void Initialize();
/// <summary>
/// The method that is executed when either wired-up UI control is clicked.
/// </summary>
public abstract void ExecuteAction();
public void Release()
{
_command.Release();
}
}
}
The IRubberduckMenuCommand
interface defines everything I need to be able to associate "buttons" with a command, to destroy the created COM objects on demand (changing the app's language from the Settings dialog causes a reload of the entire UI), and of course a mechanism to communicate that the command needs to be executed:
namespace Rubberduck.UI.Commands
{
/// <summary>
/// An object that encapsulates the logic to wire up a number of CommandBarControl instances to a specific command.
/// </summary>
public interface IRubberduckMenuCommand
{
/// <summary>
/// Associates a new <see cref="CommandBarButton"/> to the command.
/// </summary>
/// <param name="parent">The parent control collection to add the button to.</param>
/// <param name="caption">The localized caption for the command.</param>
/// <param name="beginGroup">Optionally specifies that the UI element begins a command group.</param>
/// <param name="beforeIndex">Optionally specifies the index of the UI element in the parent collection.</param>
/// <param name="image">An optional icon to represent the command.</param>
/// <param name="mask">A transparency mask for the command's icon. Required if <see cref="image"/> is not null.</param>
void AddCommandBarButton(CommandBarControls parent, string caption, bool beginGroup = false, int beforeIndex = -1, Image image = null, Image mask = null);
/// <summary>
/// Destroys all UI elements associated to the command.
/// </summary>
void Release();
/// <summary>
/// Requests execution of the command.
/// </summary>
event EventHandler RequestExecute;
}
}
At first I had a plain Execute
method there and it looked nice on paper, but the simple AboutCommand
implementation was getting pretty complicated, so I changed it a bit - this is how the interface ended up implemented:
namespace Rubberduck.UI.Commands
{
public class RubberduckMenuCommand : IRubberduckMenuCommand
{
private readonly IList<CommandBarButton> _buttons;
/// <summary>
/// Creates a new menu command.
/// </summary>
public RubberduckMenuCommand()
{
_buttons = new List<CommandBarButton>();
}
public void AddCommandBarButton(CommandBarControls parent, string caption, bool beginGroup = false, int beforeIndex = -1, Image image = null, Image mask = null)
{
if (image != null && mask == null)
{
throw new ArgumentNullException("'image' cannot be null if 'mask' is non-null.");
}
if (image == null && mask != null)
{
throw new ArgumentNullException("'mask' cannot be null if 'image' is non-null.");
}
var button = (CommandBarButton) (beforeIndex == -1
? parent.Add(MsoControlType.msoControlButton, Temporary: true)
: parent.Add(MsoControlType.msoControlButton, Before: beforeIndex, Temporary: true));
button.BeginGroup = beginGroup;
button.Caption = caption;
if (image != null)
{
SetButtonImage(button, image, mask);
}
button.Click += button_Click;
_buttons.Add(button);
}
public void Release()
{
foreach (var button in _buttons)
{
button.Click -= button_Click;
button.Delete();
Marshal.ReleaseComObject(button);
}
_buttons.Clear();
}
private void button_Click(CommandBarButton Ctrl, ref bool CancelDefault)
{
OnRequestExecute();
}
public event EventHandler RequestExecute;
public void OnRequestExecute()
{
var handler = RequestExecute;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
private void SetButtonImage(CommandBarButton button, Image image, Image mask)
{
button.FaceId = 0;
button.Picture = AxHostConverter.ImageToPictureDisp(image);
button.Mask = AxHostConverter.ImageToPictureDisp(mask);
}
private class AxHostConverter : AxHost
{
private AxHostConverter() : base("") { }
static public IPictureDisp ImageToPictureDisp(Image image)
{
return (IPictureDisp)GetIPictureDispFromPicture(image);
}
static public Image PictureDispToImage(IPictureDisp pictureDisp)
{
return GetPictureFromIPicture(pictureDisp);
}
}
}
}
I only need this RubberduckMenuCommand
implementation. In fact, I'm not even sure I need the abstraction in the first place: would it hurt to couple RubberduckCommandBase
with RubberduckMenuCommand
?
Here's the "prototype" command:
namespace Rubberduck.UI.Commands
{
/// <summary>
/// A command that displays the "About" dialog.
/// </summary>
public class AboutCommand : RubberduckCommandBase
{
private readonly VBE _vbe;
public AboutCommand(IRubberduckMenuCommand command, VBE vbe)
: base(command)
{
_vbe = vbe;
}
public override void Initialize()
{
var parent = _vbe.CommandBars[1].Controls.OfType<CommandBarPopup>()
.SingleOrDefault(control => control.Caption == RubberduckUI.RubberduckMenu);
if (parent == null)
{
throw new InvalidOperationException("Parent menu not found. Cannot create child menu item.");
}
Command.AddCommandBarButton(parent.Controls, RubberduckUI.RubberduckMenu_About, true);
}
public override void ExecuteAction()
{
using (var window = new _AboutWindow())
{
window.ShowDialog();
}
}
}
}
As you can see, all the class is doing is calling Command.AddCommandBarButton
in the Initialize
method, and doing its thing in the ExecuteAction
method.
If I implement all menu commands like this, I'll have a class for each one and the RubberduckMenu
class will no longer need to care about handling & dispatching any of the clicks; in fact, all it will be responsible for, will be to create the CommandBarPopup
objects that each command will use as parents. And I can constructor-inject some IEnumerable<RubberduckCommandBase>
and simply iterate all commands and call their Initialize
method to create the menu.
This looks too good to be true. Do you see anything that's coming to bite me?