I have been working my way through Jeremy Miller’s excellent Build Your Own CAB Series (which would be even better if he felt like finishing!) and was very interested by the article on controlling menus with Microcontrollers.
After reading it and writing a version of it myself, I came to the conclusion that some parts of it seem to be wrong. All of the permissioning is done based on the menu items which fire ICommands, and several menu items could use the same ICommand. This means that you need to use the interface something like this:
MenuController.MenuItem(mnuFileNew).Executes(Commands.Open).IsAvailableToRoles("normal", "editor", "su");
MenuController.MenuItem(tsbStandardNew).Executes(Commands.Open).IsAvailableToRoles("normal", "editor", "su");
Now to me this seems somewhat wrong, I would rather have something like this:
MenuController.Command(new MenuCommands.New).IsAttachedTo(mnuFileNew, tsbStandardNew).IsAvailableToRoles("normal", "editor", "su");
So I decided to have a go at re-working it to my liking. To start with we have the mandatory ICommand interface:
Public Interface ICommand
Sub Execute()
End Interface
Then a class that manages the actual ICommand and its menuitem(s):
Public NotInheritable Class CommandItem(Of T) Implements IDisposable ‘used to remove handlers that we dont want to leave lying around Private ReadOnly _command As ICommand Private ReadOnly _id As T Private _roles As New List(Of String) Private _menuItems As New List(Of ToolStripItem) Private _alwaysEnabled As Boolean = False Private _disposed As Boolean = False Public Property AlwaysEnabled() As Boolean Get Return _alwaysEnabled End Get Set(ByVal value As Boolean) _alwaysEnabled = value End Set End Property Public Property Roles() As List(Of String) Get Return _roles End Get Set(ByVal value As List(Of String)) _roles = value End Set End Property Public ReadOnly Property MenuItems() As ToolStripItem() Get Return _menuItems.ToArray End Get End Property Public ReadOnly Property IsDisposed() As Boolean Get Return _disposed End Get End Property Public Sub New(ByVal cmd As ICommand, ByVal id As T) _command = cmd _id = id End Sub Public Sub AddMenuItem(ByVal menuItem As ToolStripItem) _menuItems.Add(menuItem) AddHandler menuItem.Click, AddressOf _item_Click End Sub Public Sub RemoveMenuItem(ByVal menuItem As ToolStripItem) RemoveHandler menuItem.Click, AddressOf _item_Click _menuItems.Remove(menuItem) End Sub Public Function IsEnabled(ByVal state As CommandState(Of T)) As Boolean If _alwaysEnabled Then Return True If Not state.IsEnabled(_id) Return False For i As Integer = 0 To _roles.Count – 1 If Thread.CurrentPrincipal.IsInRole(_roles(i)) Then Return True Next Return False End Function Public Sub SetState(ByVal state As CommandState(Of T)) Dim enabled As Boolean = IsEnabled(state) For Each ts As ToolStripItem In _menuItems ts.Enabled = enabled Next End Sub Public Sub Dispose(ByVal disposing As Boolean) If Not _disposed AndAlso disposing Then For Each menuItem As ToolStripItem In _menuItems RemoveMenuItem(menuItem) Next End If _disposed = True End Sub Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub Private Sub _item_Click(ByVal sender As Object, ByVal e As EventArgs) _command.Execute() End SubEnd Class
As you can see, the Dispose Method is used to allow for handlers to be removed, otherwise the objects might be hanging around longer than they should be. We also have a list of menu items that this command controls, and a list of roles that the command is available to.
Next we have the class that holds the state of each menu item, which is generic to allow the end user to use whatever they wish to identify each menu item:
Public NotInheritable Class CommandState(Of T) Private _enabledCommands As New List(Of T) Public Function Enable(ByVal id As T) As CommandState(Of T) If Not _enabledCommands.Contains(id) Then _enabledCommands.Add(id) End If Return Me End Function Public Function Disable(ByVal id As T) As CommandState(Of T) If _enabledCommands.Contains(id) Then _enabledCommands.Remove(id) End If Return Me End Function Public Function IsEnabled(ByVal id As T) As Boolean Return _enabledCommands.Contains(id) End FunctionEnd Class
Finally we have the Manager class which stitches the whole lot together with a health dollop of Fluent Interfaces. We have a unique list of Commands (as I wrote this in VS2005, I just had to make a unique List class, rather than use a dictionary of CommmandItem and Null) and a sub class which provides the Fluent Interface to the manager. (IDisposeable parts have been trimmed out for brevity, it’s just contains a loop that disposes all child objects).
Public NotInheritable Class Manager(Of T) Private _commands As New UniqueList(Of CommandItem(Of T)) Public Function Command(ByVal cmd As ICommand, ByVal id As T) As CommandExpression Return New CommandExpression(Me, cmd, id) End Function Public Sub SetState(ByVal state As CommandState(Of T)) For Each ci As CommandItem(Of T) In _commands ci.SetState(state) Next End Sub Public NotInheritable Class CommandExpression Private ReadOnly _manager As Manager(Of T) Private ReadOnly _commandItem As CommandItem(Of T) Friend Sub New(ByVal mgr As Manager(Of T), ByVal cmd As ICommand, ByVal id As T) _manager = mgr _commandItem = New CommandItem(Of T)(cmd, id) _manager._commands.Add(_commandItem) End Sub Public Function IsAttachedTo(ByVal menuItem As ToolStripItem) As CommandExpression _commandItem.AddMenuItem(menuItem) Return Me End Function Public Function IsInRole(ByVal ParamArray roles() As String) As CommandExpression _commandItem.Roles.AddRange(roles) Return Me End Function Public Function IsAlwaysEnabled() As CommandExpression _commandItem.AlwaysEnabled = True Return Me End Function End Class Private Class UniqueList(Of TKey) Inherits List(Of TKey) Public Shadows Sub Add(ByVal item As TKey) If Not MyBase.Contains(item) Then MyBase.Add(item) End If End Sub End ClassEnd Class
In my test application I have a file containing my menuCommands and an Enum used for identification:
Namespace MenuCommands Public Enum Commands [New] Open Save Close End Enum Public Class Open Implements ICommand Public Sub Execute() Implements ICommand.Execute MessageBox.Show(“Open”) End Sub End Class End Namespace
And in the main form I have this code. The Thread Principle is used for the roles, and the actual roles could (should) be loaded from a database or anywhere other than hard coded constants of course.
Private _menuManager As New Manager(Of MenuCommands.Commands)
Private _state As New CommandState(Of MenuCommands.Commands)Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Thread.CurrentPrincipal = New GenericPrincipal(Thread.CurrentPrincipal.Identity, New String() {“normal”}) _menuManager.Command(New MenuCommands.[New], MenuCommands.Commands.[New]) _ .IsAttachedTo(mnuFileNew) _ .IsAttachedTo(tsbNew) _ .IsInRole(“normal”) _menuManager.Command(New MenuCommands.Open, MenuCommands.Commands.Open) _ .IsAttachedTo(mnuFileOpen) _ .IsAttachedTo(tsbOpen) _ .IsInRole(“normal”, “reviewer”, “viewer”) _menuManager.Command(New MenuCommands.Save, MenuCommands.Commands.Save) _ .IsAttachedTo(mnuFileSave) _ .IsAttachedTo(tsbSave) _ .IsInRole(“normal”, “reviewer”) _menuManager.Command(New MenuCommands.Close, MenuCommands.Commands.Close) _ .IsAttachedTo(mnuFileExit) _ .IsAlwaysEnabled() _state.Enable(MenuCommands.Commands.Open) _ .Enable(MenuCommands.Commands.Save) _ .Enable(MenuCommands.Commands.Close) _menuManager.SetState(_state)End Sub
The state object is used to enable and disable menu items and could be wrapped in another object if it needed to be exposed further than the form.