Handling Menus and Toolbars Using MDICommandSupport Class Library






4.71/5 (16 votes)
Simplifies working with ToolStrip-type menus and tool bars for MDI-parent and other forms; also works with ToolStrip-type context menus. Also handles help requests for menu and toolbar items.
Most recent update-- September 9, 2024 9:50 PM (EST)
Introduction
Whenever I create a major application--especially an MDI app--I often have commands (program actions) that can be invoked by the user in multiple places--i.e., from a menu and from a toolbar. That could mean two procedures in the MDI module that do the same thing--as well as the need to set similar properties for each ToolStripItem
(menu and toolbar item)--say, whenever a command needs to be enabled/disabled or shown/hidden. Also, the code executed for each command, while different overall, often contains certain common instructions--say, at the beginning and end of the procedure. Finally, I often want to detect when a ToolStripItem
is selected (highlighted) or deselected (unhighlighted)--say, for displaying text in a status bar label; unfortunately, menu items raise no specific events for when they are selected/deselected.
The following class library scans a form's (any form, not just MDI's) control collection for MenuStrips
, ToolStrip
s, and ToolStripItem
containers for ToolStripItem
s, and groups those items by command name (specified by ToString
method of Tag
property) into a "command dictionary" within instances of the class MDICommandInfo
--which allow one to specify status-bar text and get/set properties of all ToolStripItem
s associated with a given command at once.
The MDICommandInfo
instances are managed by another class, MDICommandHandler
--which intercepts the mouse/keyboard events needed in order to detect when commands are invoked (clicked), selected, and deselected--and delegates those situations to three events: CommandClicked
, CommandSelected
, and CommandDeselected
, respectively. The list of command names and associated status texts is supplied by the user using a ResourceManager
, using a Dictionary
, or manually.
It can also be used on ContextMenuStrips
.
(NEW!) It now also processes requests for help when a participating menu/toolbar item is selected using event CommandHelpRequested
.
(NEWER!) Demo program displays notification of help requests in form's status bar as well as in Debug
window.
History of Changes
From most recent to earliest:
- As of 9/9/2024 9:50 PM EST, the demo program displays notification of when a help request is made in status bar of form as well as in
Debug
window (which is now otherwise only used to log selection and deselection of menu/toolbar items). - As of 9/9/2024 1:40 AM EST, a new event,
CommandHelpRequested
, is provided to enableMDICommandHandler
handle requests for application help for any participation menu or toolbar item. Also, the shortcut key for "First Command" is now Shift + F1, in order to allow the standard help key (F1 or fn + F1) to be used to trigger the help request. - (BUG FIX!) As of 7/27/2023 2:40 PM EST, I fixed a bug to guard against re-entrancy issues for the
CommandClicked
event. - (BUG FIX!) As of 7/18/2023 4:35 PM EST, I altered the
MDICommandInfo
class to take an additional paremeter--an instance of its parentMDICommandHander
class--so that whenever an instance of the former class has itsStatusLabelText
property changed, the label in the latter (parent) class is updated if the command-name for the first class matches that of the second class' current menu/toolbar item. This shouldn't break your current code, provided you haven't instantiatedMDICommandInfo
(as opposed toMDICommandHandler
) in your host code. (If so, then insert the instance of the parent class into each "New
" constructor statement.) This fix ensures that changes toStatusLabelText
made, say, usingMDICommandHandler
'sCommandInfo
(default) property, are reflected immediately in the UI. - As of 7/18/2023, the
CommandStatusText
property is now read-write, rather than read-only. When this property is set, the new label string is placed in theCommandDictionary
, and, if the indicated command is the currently-selected one, any supplied tool-strip label. - As of 7/17/2023, the
CommandSelected
andCommandDeselected
events are suppressed while any host-program event code for theCommandClicked
event is running--and they're guaranteed to fire respectively before and afterCommandClicked
even when a menu item is invoked by shortcut key--in order to ensure that the class considers a menu/toolbar item to be effectively "selected" while the host program is carrying out the command associated with it. - Also as of 7/17/2023, the 3 overloads of the
MDICommandHandler
's constructor have been collapsed into 1 in order to allow theStatusLabel
parameter (as well as the following parameter) to be optional. (One can specifyStatusTextSource
while omittingStatusLabel
if one wants to have default status-text but determine where it's displayed manually. BTW, the new constructorshould not
break any existing code as the third paraneter is an optionalObject
which is accepted if it'sNothing
[omitted],ResourceManager
type, orDictionary(Of String, String)
type.) As of 3/6/2023, the constructor-overload comments have been updated to correctly indicate that there are 3 overloads.- As of 12/28/2021, the three events are raised by protected "
On
" methods to facilitate overriding by any derived class. - As of 12/28/2020,
MDICommandHandler
'sCommandDictionary
returns a copy of theMDICommandInfo
dictionary in order to prevent the host program from adding and removing entries; host program can still modifyMDICommandInfo
properties for entries. - As of 11/3/2019, the class now plugs in any
ContextMenuStrip
encountered automatically. Previously, one had to useAddChildItems
to add support for context menus manually. - As of 11/1/2019, the demo project features submenus and a context menu.
- As of 12/12/2018, the
IsCommandEnabled
andIsCommandVisible
properties are now Nullable, so that internal (and external) code that sets them to Nothing (some yes, some no) doesn't set them toFalse
(all no) by default. This is an error I only noticed by chance! - As of 2/5/2018, the main class no longer relies on a life-long access to a
ResourceManager
, and therefore theResourceManager
property no longer exists! The list of command names and associated status texts is specified by either aResourceManager
or aDictionary(Of String, String)
--through the constructor or through the respectiveGetCommandsFromResources
orGetCommandsFromDictionary
methods. - As of 11/28/2017, the code has been updated so as to avoid multiple firings of events. Each
AddHandler
statement is preceded by aRemoveHandler
statement so that successive calls toAddChildItems
don't create redundant event handling.
Using the Code
Root Namespace: MDICommandSupport
Classes: MDICommandHandler
(main class), MDICommandInfo
(helper class)
MDICommandHandlerInfo Class (Helper Class)
Constructor
mch
is an instance of parentMDICommandHandler
class using this helper classCommandName
is aString
for type of program action (Tag.ToString
of correspondingToolStripItem
s)StatusLabelText
is an optionalString
for status-bar text for this command
Properties
CommandItems
getsList
ofToolStripItem
s corresponding to this commandCommandName
gets name of command (specified in constructor)StatusLabelText
gets or sets status-bar text (IfCommandName
corresponds to the currently selected item in the parent class, then any change toStatusLabelText
is reflected in any tool-strip label of theParent
class; this includes initialization in the constructor above.)IsCommandEnabled
(Nullable Boolean) gets or sets theEnabled
property of all theToolStripItem
s (when getting,Nothing
indicates that some are enabled, some disabled)IsCommandVisible
(Nullable Boolean) gets or sets theVisible
property of all the items (once again,Nothing
indicates mixed information)Parent
gets parentMDICommandHandler
class instance
Methods
SetProperty
sets an arbitrary property (specified byPropertyName
String
) of the items to a givenNewValue
--with optionalindex()
information in the event that the property has parameters--using reflection.GetProperty
gets aDictionary(Of ToolstripItem, Object)
set of values of an arbitrary property (specified byPropertyName
String
)----with optionalindex()
information in the event that the property has parameters--using reflection. TheToolStripItem
s in this class represent the keys of the dictionary returned; the respective values of the given property for each item represent the dictionary values.Contains
checks to see if controlToolStripItem
is in theCommandItems
List
.Add
andRemove
allow you to insert or delete aToolStripItem
item, respectively.
MDICommandHandler Class (Main Class)
Constructor
MDIParentForm
is any WinFormsForm
(does not have to be an MDI parent form)StatusLabel
is an optionalToolStripStatusLabel
used to display status bar text. If parameter is omitted, then you'll have to assign text to a label or other control manually- StatusTextSource is an optional
ResourceManager
object orDictionary(Of String, String)
object, respectively, which contains a list of descriptive command-name/status-text associations--using either resources (resource name specifies command name, with underscores where spaces are intended and "_Tag
" appended; resourcestring
specifies status text), or a dictionary (key specifies command name; value specifies status text)--for each command's status-bar text. If parameter is omitted, you'll have to manually generate theString
s which will be assigned to theMDICommandInfo
instances'StatusLabelText
property and toStatusLabel
(or your own status control). (You'll also have to manually generate descriptive status-text for any menu/toolbar item with a non-null command name specified in itsTag.ToString
value but for which the command string isn't present as an entry in the resource/dictionary object.)
Properties
MDIParentForm
gets the form (specified in the constructor)StatusLabel
gets or sets status bar control (Set toNothing
to handle label control and/or description text manually.)SelectedItem
gets theToolStripItem
corresponding to the currently selected menu or toolbar item (Nothing if none is currently selected).SelectedCommand
gets the command-name of the currently selectedToolStripItem
(null string if none is selected or it menu/toolbar item doesn't have a value assigned to itsTag.ToString
)CommandNameForItem
gets the command-nameString
for the specifiedToolStripItem--
basically, the item'sTag.ToString
value. If no such value exists (indicating that the item isn't meant to be handled by this class), then a null string is returned.CommandStatusText
gets or sets the descriptive status-text for the menu/toolbar item indicated by an optional command-nameString
orToolStripItem
instance; if neither is specified, then the status-text for the currently selected item (if any) is returned when reading. If no status-text is specified in theCommandDictionary
for the given command, then a null string is returned. When setting, the new status-textString
replaces the current one for the given command'sCommandDictionary
entry--along with the current contents of theStatusLabel
if the given command is the currently-selected one;--an exception results if 1) no parameter is given and no menu/toolbar item is currently selected, or 2) aToolStripItem
is the parameter but it has no command name (Tag.ToString
isnull
).CommandInfo
gets instance ofMDICommandInfo
corresponding to action specified by a command-nameString
or aToolStripItem
instance. This is the default property.CommandDictionary
gets aDictionary
ofMDICommandInfo
instances, representing all participating commands. The key is the command-nameString
; the value is thestatus-textcorrespondingString
MDICommandInfo
instance. Additions and removals from the dictionary returned by this property will not affect the internal dictionary; invoking members of entries will.
Methods
AddItem
adds aToolStripItem
to the command dictionary; if the item is aToolStripDrownDownItem
, thenAddChildItems
is called to handle the drop-down list.AddChildItems
adds allToolStripItem
s inside aControl
,ControlCollection
, orToolStripItemCollection
; this method is recursive, and is automatically invoked by the constructor for the entire form.GetCommandsFromResources
orGetCommandsFromDictionary
gets a list of command-name/status-text associations using either aResourceManager
instance or aDictionary(Of String, String)
instance, respectively. Resource-name/dictionary-key specifies command name, and resource-string/dictionary-value specifies status text for a command. The first parameter, eitherResourceManager
orStatusTextDictionary
, is the resource-manager/dictionary instance, the optional second parameter,Clear
(defaults toFalse
), specifies whether to initially clear out command dictionary (True
) or to simply change status text when an item is found to be pre-existing in it (False
).
Events
CommandClicked
--fired when a participatingToolStripItem
(Tag.ToString
is notnull
) is clickedCommandSelected
--fired when it's selected (highlighted).CommandDeselected
--fired when it's deselectedCommandHelpRequested
(NEW!) -- fired when a help request (say, using F1 or fn + F1) is made on itMDICommandHandlerEventArg
Parameters (CommandClicked
,CommandSelected
, andCommandDeselected
events):e.CommandName
= name of commande.CommandItem
= specificToolStripItem
in question (e.CommandName = e.CommandItem.Tag.ToString
)
-
MDICommandHandlerHelpEventArg
(NEW!) Parameters (CommandHelpRequested
event):e.CommandName
= name of commande.CommandItem
= specificToolStripItem
in question (e.CommandName = e.CommandItem.Tag.ToString
)e.MousePos
= position of mousee.Handled
=True
if event is to be handled exclusively by event procedure,False
(default) if Windows is to process it further; this property is read-write
NOTES
- When a form or control is searched for menus and tooltips, context menus are no longer skipped. Previously, one needs to use the main class'
AddChildItems
method with a context-menu instance as its argument. Now any form/control, and any of its child controls, featuring a non-nullContextMenuStrip
will be included in the search. - If you want an individual
ToolStrip
item to be omitted from the dictionary, then leave itsTag.ToString
valuenull
. - Any showing or hiding of status-bar text occurs before the
CommandClicked
,CommandSelected
, orCommandDeselected
event is fired. - If the associations of command names and status texts are specified via a
ResourceManager
, then the key-names of the resources must echo the command names (Tag.ToString
) with all spaces replaced with underscores ("_
") and with "_Tag
" appended--i.e., command "This Command
" must have a resource nameThis_Command_Tag
". This rule does not apply when specifying associations via aDictionary
(that is, "This Command
" has a dictionary-key value "This Command
"). - If the
StatusLabelText
property of a command'sMDICommandInfo
instance is set to a non-nullString
afterMDICommandHandler
has been instantiated or the most recent call toGetCommandsFromResources
/GetCommandsFromDictionary
, then theStatusLabelText
value overrides any pre-defined text for that command. - Whenever the constructor,
AddItem
, or addAddChildItems
is invoked, allToolStripItem
s whoseTag.ToString
values are notNothing
nor null strings are placed in theCommandDictionary
under the command-name keys specified byTag.ToString
--whether or not those commands are actually covered by a resource-manager/dictionary object in the constructor,GetCommandsFromResources
, orGetCommandsFromDictionary
. If a command is found which is not specified in a resource list/dictionary, then its initial status-text value is a null string. - If the form specified in the constructor is
Nothing
, or if theStatusTextSource
is included but of the wrong type, then an exception is thrown. - If a new item is selected while the host-program's event code for the current item's
CommandClicked
event is still being processed, the new selection won't be noticed until after the event code is finished and the existing item's deselection is handled; if an item is deselected while the host event code ofCommandClicked
for it, then its deselection will only be handled after theCommandClick
event code is finished. These new rules apply even when theCommandClick
event code executesDoEvents
! Finally, if an item is invoked without being formally "selected"--i.e., a menu item's shortcut key is pressed--then theCommandClick
event will still be preceded byCommandSelected
and followed byCommandDeselected
for that item. All in all, a given menu/toolbar item's event sequence is now alwaysCommandSelected
, thenCommandClicked
(if it's chosen by the user), and finallyCommandDeselected
. This new behavior ensures that an item's selection/deselection triggered by code insideCommandClicked
's host event code--i.e., displaying another form--doesn't invalidate the info about the current command, and that an item's descriptive status-text displays even when it's triggered by a shortcut key.
Imports MDICommandSupport
' constructor
Dim mch As MDICommandHandler = _
New MDICommandHandler(Me, StatusLabel, Resource3)
Dim mch As MDICommandHandler = _
New MDICommandHandler(Me, StatusLabel, Dictionary1)
' properties
mch.CommandInfo("Save").IsCommandEnabled = ShouldWeSave
Dim CanWeSave As Boolean? = mch.CommandInfo("Save").IsCommandEnabled
mch("Open").SetProperty("ForeColor", Color.Red) ' CommandInfo is default property
Dim BackColors As Dictionary(Of ToolStripItem, Object) = _
mch("Open").GetProperty("BackColor")
mch("New").StatusLabelText = "THIS COMMAND has manually set text"
Dim CurrentCommand As String = mch.SelectedCommand
' methods
mch.GetCommandsFromDictionary(Dictionary2, False)
mch.AddChildItems(Me) : mch.AddChildItems(Me.ContextMenuStrip) ' more controls
' events
AddHandler mch.CommandClicked, AddressOf mch_CommandClicked
Private Sub mch_CommandClicked(sender As Object, e As MDICommandHandlerEventArgs)
' preliminary stuff
SomeBeginningCode()
' command-specific stuff
Select Case e.CommandName
Case "Open"
OpenFileProc()
Case "New"
NewFileProc()
Case "Close"
CloseFileProc()
Case "Save"
SaveFileProc()
End Select
' final stuff
SomeEndingCode()
End Sub