Click here to Skip to main content
15,884,986 members
Articles / Programming Languages / Visual Basic
Tip/Trick

Handling Menus and Toolbars Using MDICommandSupport Class Library

Rate me:
Please Sign up or sign in to vote.
4.71/5 (16 votes)
6 Mar 2023CPOL11 min read 32.3K   848   33   4
Simplifies working with ToolStrip-type menus and tool bars for MDI-parent and other forms; also works with ToolStrip-type context menus
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-- July 27, 2023 2:40 PM (EST)

Image 1

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, ToolStrips, and ToolStripItem containers for ToolStripItems, 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 ToolStripItems 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.

History of Changes

From most recent to earliest:

  1. (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.
  2. (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.
  3. 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.
  4. 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.
  5. 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.)
  6. As of 3/6/2023, the constructor-overload comments have been updated to correctly indicate that there are 3 overloads.
  7. As of 12/28/2021, the three events are raised by protected "On" methods to facilitate overriding by any derived class.
  8. 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.
  9. 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.
  10. As of 11/1/2019, the demo project features submenus and a context menu.
  11. 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!
  12. 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.
  13. 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 (NEW!) is an instance of parent MDICommandHandler class using this helper class
  • CommandName is  aString for type of program action (Tag.ToString of corresponding ToolStripItems)
  • StatusLabelText is an optional String for status-bar text for this command
Properties
  • CommandItems gets List of ToolStripItems corresponding to this command
  • CommandName gets name of command (specified in constructor)
  • StatusLabelText gets or sets status-bar text (NEW behavior! 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 ToolStripItems (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 (NEW!) 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 ToolStripItems 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 (optional status NEW!) ToolStripStatusLabel used to display status bar text. If parameter is pmitted, then you'll have to assign text to a label or other control manually
  • StatusTextSource (name NEW!) 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 Strings 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 (NEW!) gets the ToolStripItem corresponding to the currently selected menu or toolbar item (Nothing if none is currently selected).
  • SelectedCommand (NEW!) 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 (NEW!) 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 (NEW!) gets or sets (NEW!) 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 ToolStripItems 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
  • CommandClicked--fired when a participating ToolStripItem (Tag.ToString is not null) is clicked
  • CommandSelected--fired when it's selected (highlighted). 
  • CommandDeselected--fired when it's deselected.

    MDICommandHandlerEventArg Parameters (all three events):

    • e.CommandName = name of command
    • e.CommandItem = specific ToolStripItem in question (e.CommandName = e.CommandItem.Tag.ToString)

NEW behavior!: 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.

NOTES

  1. 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.
  2. If you want an individual ToolStrip item to be omitted from the dictionary, then leave its Tag.ToString value null.
  3. Any showing or hiding of status-bar text occurs before the CommandClicked, CommandSelected, or CommandDeselected event is fired.
  4. 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").
  5. 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.
  6. Whenever the constructor, AddItem, or add AddChildItems is invoked, all ToolStripItems 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.
  7. 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 (behavior NEW!).
VB.NET
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionRobert you are a very brave person, much braver than me :) Pin
NG_AU23-Jul-20 21:21
NG_AU23-Jul-20 21:21 
AnswerRe: Robert you are a very brave person, much braver than me :) Pin
Robert Gustafson6-Aug-20 11:17
Robert Gustafson6-Aug-20 11:17 
Generalany C# version available? Pin
Southmountain1-Nov-19 8:59
Southmountain1-Nov-19 8:59 
thank you for this useful article!
diligent hands rule....

GeneralRe: any C# version available? Pin
phil.o2-Nov-19 21:20
professionalphil.o2-Nov-19 21:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.