The following class library scans a form's (any form, not just MDI's) control collection for MenuStrips, ToolStrips, and ToolStripItem containers for ToolStripItems, and groups those items by command name into a command dictionary within instances of the class MDICommandInfo.
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 enable MDICommandHandler
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 parent MDICommandHander
class--so that whenever an instance of the former class has its StatusLabelText
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 instantiated MDICommandInfo
(as opposed to MDICommandHandler
) in your host code. (If so, then insert the instance of the parent class into each "New
" constructor statement.) This fix ensures that changes to StatusLabelText
made, say, using MDICommandHandler
's CommandInfo
(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 the CommandDictionary
, and, if the indicated command is the currently-selected one, any supplied tool-strip label. - As of 7/17/2023, the
CommandSelected
and CommandDeselected
events are suppressed while any host-program event code for the CommandClicked
event is running--and they're guaranteed to fire respectively before and after CommandClicked
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 the StatusLabel
parameter (as well as the following parameter) to be optional. (One can specify StatusTextSource
while omitting StatusLabel
if one wants to have default status-text but determine where it's displayed manually. BTW, the new constructor should not
break any existing code as the third paraneter is an optional Object
which is accepted if it's Nothing
[omitted], ResourceManager
type, or Dictionary(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
's CommandDictionary
returns a copy of the MDICommandInfo
dictionary in order to prevent the host program from adding and removing entries; host program can still modify MDICommandInfo
properties for entries. - As of 11/3/2019, the class now plugs in any
ContextMenuStrip
encountered automatically. Previously, one had to use AddChildItems
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
and IsCommandVisible
properties are now Nullable, so that internal (and external) code that sets them to Nothing (some yes, some no) doesn't set them to False
(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 the ResourceManager
property no longer exists! The list of command names and associated status texts is specified by either a ResourceManager
or a Dictionary(Of String, String)
--through the constructor or through the respective GetCommandsFromResources
or GetCommandsFromDictionary
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 a RemoveHandler
statement so that successive calls to AddChildItems
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 parent MDICommandHandler
class using this helper class CommandName
is aString
for type of program action (Tag.ToString
of corresponding ToolStripItem
s) StatusLabelText
is an optional String
for status-bar text for this command
Properties
CommandItems
gets List
of ToolStripItem
s corresponding to this command CommandName
gets name of command (specified in constructor) StatusLabelText
gets or sets status-bar text (If CommandName
corresponds to the currently selected item in the parent class, then any change to StatusLabelText
is reflected in any tool-strip label of the Parent
class; this includes initialization in the constructor above.) IsCommandEnabled
(Nullable Boolean) gets or sets the Enabled
property of all the ToolStripItem
s (when getting, Nothing
indicates that some are enabled, some disabled) IsCommandVisible
(Nullable Boolean) gets or sets the Visible
property of all the items (once again, Nothing
indicates mixed information) Parent
gets parent MDICommandHandler
class instance
Methods
SetProperty
sets an arbitrary property (specified by PropertyName
String
) of the items to a given NewValue
--with optional
index()
information in the event that the property has parameters--using reflection. GetProperty
gets a Dictionary(Of ToolstripItem, Object)
set of values of an arbitrary property (specified by PropertyName
String
)----with optional
index()
information in the event that the property has parameters--using reflection. The ToolStripItem
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 control ToolStripItem
is in the CommandItems
List
. Add
and Remove
allow you to insert or delete a ToolStripItem
item, respectively.
MDICommandHandler Class (Main Class)
Constructor
MDIParentForm
is any WinForms Form
(does not have to be an MDI parent form) StatusLabel
is an optional ToolStripStatusLabel
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 or Dictionary(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; resource string
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 the String
s which will be assigned to the MDICommandInfo
instances' StatusLabelText
property and to StatusLabel
(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 its Tag.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 to Nothing
to handle label control and/or description text manually.) SelectedItem
gets the ToolStripItem
corresponding to the currently selected menu or toolbar item (Nothing if none is currently selected). SelectedCommand
gets the command-name of the currently selected ToolStripItem
(null string if none is selected or it menu/toolbar item doesn't have a value assigned to its Tag.ToString
) CommandNameForItem
gets the command-name String
for the specified ToolStripItem--
basically, the item's Tag.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-name String
or ToolStripItem
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 the CommandDictionary
for the given command, then a null string is returned. When setting, the new status-text String
replaces the current one for the given command's CommandDictionary
entry--along with the current contents of the StatusLabel
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) a ToolStripItem
is the parameter but it has no command name (Tag.ToString
is null
). CommandInfo
gets instance of MDICommandInfo
corresponding to action specified by a command-name String
or a ToolStripItem
instance. This is the default property. CommandDictionary
gets a Dictionary
of MDICommandInfo
instances, representing all participating commands. The key is the command-name String
; the value is the status-text String
corresponding 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 a ToolStripItem
to the command dictionary; if the item is a ToolStripDrownDownItem
, then AddChildItems
is called to handle the drop-down list. AddChildItems
adds all ToolStripItem
s inside a Control
, ControlCollection
, or ToolStripItemCollection
; this method is recursive, and is automatically invoked by the constructor for the entire form. GetCommandsFromResources
or GetCommandsFromDictionary
gets a list of command-name/status-text associations using either a ResourceManager
instance or a Dictionary(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, either ResourceManager
or StatusTextDictionary
, is the resource-manager/dictionary instance, the optional second parameter, Clear
(defaults to False
), 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
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-null ContextMenuStrip
will be included in the search. - If you want an individual
ToolStrip
item to be omitted from the dictionary, then leave its Tag.ToString
value null
. - Any showing or hiding of status-bar text occurs before the
CommandClicked
, CommandSelected
, or CommandDeselected
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 name
specified by key-string "This_Command_Tag
". This rule does not apply when specifying associations via a Dictionary
(that is, "This Command
" has a dictionary-key value "This Command
"). - If the
StatusLabelText
property of a command's MDICommandInfo
instance is set to a non-null String
after MDICommandHandler
has been instantiated or the most recent call to GetCommandsFromResources
/ GetCommandsFromDictionary
, then the StatusLabelText
value overrides any pre-defined text for that command. - Whenever the constructor,
AddItem
, or add AddChildItems
is invoked, all ToolStripItem
s whose Tag.ToString
values are not Nothing
nor null strings are placed in the CommandDictionary
under the command-name keys specified by Tag.ToString
--whether or not those commands are actually covered by a resource-manager/dictionary object in the constructor, GetCommandsFromResources
, or GetCommandsFromDictionary
. 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 the StatusTextSource
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 of CommandClicked
for it, then its deselection will only be handled after the CommandClick
event code is finished. These new rules apply even when the CommandClick
event code executes DoEvents
! Finally, if an item is invoked without being formally "selected"--i.e., a menu item's shortcut key is pressed--then the CommandClick
event will still be preceded by CommandSelected
and followed by CommandDeselected
for that item. All in all, a given menu/toolbar item's event sequence is now always CommandSelected
, then CommandClicked
(if it's chosen by the user), and finally CommandDeselected
. This new behavior ensures that an item's selection/deselection triggered by code inside CommandClicked
'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
Dim mch As MDICommandHandler = _
New MDICommandHandler(Me, StatusLabel, Resource3)
Dim mch As MDICommandHandler = _
New MDICommandHandler(Me, StatusLabel, Dictionary1)
mch.CommandInfo("Save").IsCommandEnabled = ShouldWeSave
Dim CanWeSave As Boolean? = mch.CommandInfo("Save").IsCommandEnabled
mch("Open").SetProperty("ForeColor", Color.Red)
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
mch.GetCommandsFromDictionary(Dictionary2, False)
mch.AddChildItems(Me) : mch.AddChildItems(Me.ContextMenuStrip)
AddHandler mch.CommandClicked, AddressOf mch_CommandClicked
Private Sub mch_CommandClicked(sender As Object, e As MDICommandHandlerEventArgs)
SomeBeginningCode()
Select Case e.CommandName
Case "Open"
OpenFileProc()
Case "New"
NewFileProc()
Case "Close"
CloseFileProc()
Case "Save"
SaveFileProc()
End Select
SomeEndingCode()
End Sub