Making Reusable Context Menus for Windows Applications






4.57/5 (4 votes)
This article provides a solution for applications that want to reuse the same context menu (and corresponding functionality) in many forms.
Introduction
This article provides a solution for applications that want to reuse the same context menu (and corresponding functionality) in many forms. This is intended to reduce the headache of maintaining the very same menu in many places, allowing for a changing set of menu items to be maintained in one location. My approach was to develop a solution which would let me conceive of the context menu as an XML resource.
In our case, a calendaring application allows users to add five types of time records from a monthly view, weekly view, or daily view. That leads to 15 menu items to maintain, spread over different context menus on three forms. Further, if a user clicks on an existing time record in one of these views, the context menu should also present options for editing or deleting the selected record, leading to an additional 6 menu items to maintain. Below is a screen snap of the context menu that a right mouse click on an existing record brings up over a daily view of time records.
Context menus are commonly added to Windows Forms by dragging ContextMenuStrips
from the designer onto the form, and using the properties explorer to build out their actual menu items as ToolStripMenuItems
. Event handlers can be declared using the designer in each form to be associated with menu item clicks. The result of this, of course, is a different instance of ContextMenuStrip
for each one dragged onto a form, plus, crucially, duplicates of the same ToolStripMenuItems
per context menu, and replicated code to handle mouse clicks. The real maintenance headache comes when changes must be matched across all instances of the menu.
Behind the scenes, Visual Studio creates designer .CS files which hold the menu properties, and one could do find-and-replace actions to change properties like names—if that were what needed changing. Adding or deleting menu items, however, requires visiting all forms just for the purpose of changing the exact same menu in every location.
I could not find a built-in way to consolidate context menu items as resources within a VS project. In our case, the menu items in all three views—monthly, weekly, and daily—functioned as exact duplicates. That is, regardless of which view they were invoked from, they performed one of the add time functions, followed by refreshing the current view. Or, they invoked either edit or delete on the selected record, followed by refreshing the current view. Therefore, the functionality could be coded once, and presumably the menu items themselves could also be designed only once for display to different forms. This seemed in line with how resources are collected and used in C# Windows applications.
I defined an XML construct to hold MenuItem
XML nodes, the attributes of which supply three essential context menu item properties:
Title
: theToolStripMenuItem.Text
ControlName
: theToolStripItem.Name
EventHandlerName
: the name of the function to be invoked when theToolStripMenuItem
is clicked. Here follows the XML definition for our calendaring application.
<?xml version="1.0" encoding="utf-8" ?>
<ContextMenu>
<AddTimeMenu>
<MenuItem Title="Add &Project Time" ControlName="AddProjectTime"
EventHandlerName="AddProjectTimeClicked"/>
<MenuItem Title="Add S&upport Time" ControlName="AddSupportTime"
EventHandlerName="AddSupportTimeClicked"/>
<MenuItem Title="Add &Vacation Time" ControlName="AddVacationTime"
EventHandlerName="AddVacationTimeClicked"/>
<MenuItem Title="Add &Sick Time" ControlName="AddSickTime"
EventHandlerName="AddSickTimeClicked"/>
<MenuItem Title="Add &Lunch Time" ControlName="AddLunchTime"
EventHandlerName="AddLunchTimeClicked"/>
<MenuItem Title="Add &Other Out of Office Time"
ControlName="AddOtherTime" EventHandlerName="AddOtherTimeClicked"/>
</AddTimeMenu>
<RecordSpecificMenu>
<MenuItem Title="&Edit Record"
ControlName="EditRecord" EventHandlerName="EditRecordClicked"/>
</RecordSpecificMenu>
</ContextMenu>
<!--
Title: what user sees as actual menu item text
ControlName: must be unique, is the .net control's name and might be used
to access the control dynamically for showing/hiding it
EventHandlerName: name of method that is invoked when user clicks on menu item,
will be same for all Forms that want to handle this click
Additional menu items to add later
<MenuItem Title="&Delete Record" ControlName="DeleteRecord"
EventHandlerName="DeleteRecordClicked"/>
<MenuItem Title="&Copy Record to Date" ControlName="CopyRecordToDate"
EventHandlerName="CopyRecordToDateClicked"/>
<MenuItem Title="Add from &Record" ControlName="AddFromRecord"
EventHandlerName="AddFromRecordClicked"/>
-->
I sub-classed ContextMenuStrip
in a class called ContextMenuStripReplacement
, and added to its constructor the functionality to build and configure its menu items from the XML. Because it is of type ContextMenuStrip
, this context menu with its menu items could be displayed in the standard fashion by invoking Show()
on it. Constructing an instance of this class requires at least one argument, the target object on which methods named in the XML (like AddProjectTimeClicked
) are defined. These methods will be invoked on the target when menu items are clicked.
Thus, if one examines the code of the class serving as target of these invocations, one must find methods named exactly as the XML EventHandlerName
attributes. In fact, these methods will need to serve as delegates of the void MenuItemClicked(object sender, EventArgs e)
sort in order to be used. During construction of the ContextMenuStripReplacement
instance, reflection is used to look up the method by name on the target and then add the handler to the menu item. This is covered later. In any UI class where I have defined event handler methods for use by the context menu items, I create an instance of ContextMenuStripReplacement
, passing the "this
" pointer in to the constructor in order to designate that "this
" is the target of the method invocations:
this.contextMenuStrip = new ContextMenuStripReplacement(this);
The XML package of our calendaring example defines two sub-menus of a context menu, one for adding new time records, and one for interacting with already-existing specific time records. This builds some flexibility into the context menu, and the visibility of the record-specific portion is governed by a method called ShowRecordSpecificMenuItems
. The record-specific sub-menu should only be shown if the user right-clicks on a control that contains an already-existing record. In our case, these records are shown in a DataGridView
. Our logic hides or shows the record-specific context menu items based on what the user clicks on.
private void DataGridView_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
int rowIndex = -1;
if (sender is DataGridView)
{
rowIndex = this.DataGridView.HitTest(e.X, e.Y).RowIndex;
}
if (rowIndex > -1)
{
this.DataGridView.ClearSelection();
this.DataGridView.Rows[rowIndex].Selected = true;
this.contextMenuStrip.ShowRecordSpecificMenuItems(true);
}
else
{
this.contextMenuStrip.ShowRecordSpecificMenuItems(false);
}
this.contextMenuStrip.Show(this, e.Location, ToolStripDropDownDirection.Right);
}
}
Other flexibility that might be desired would be to disable or enable certain menu items. This is facilitated by the fact that the ToolStripItem.Name
, which is a required Winform Control property, is an XML attribute, thus allowing us to access menu items by name within our application. At present, we keep a list of the names of controls that need to be hidden or shown, but a list of controls to be enabled or disabled could easily be added. It is easy to iterate over a list of controls by name and make attribute changes:
public void ShowRecordSpecificMenuItems(bool state)
{
// hide or show the record-specific menu items
foreach (var menuItemControlName in this.recordSpecificMenuItemControlNames)
{
this.Items[menuItemControlName].Visible = state;
}
}
To complete the code discussion, let’s examine the ContextMenuStripReplacement
class. The constructor converts the XML string into an XML document, then walks through the MenuItem
nodes to create the menu. If the record-specific menu should also be built, it adds the sub-menu ToolStripSeparator
’s Control
name property to a list of control names that can be used to manage the sub-menu’s visibility, adds the separator to its own ToolStripItemCollection
Items property, and then builds the sub-menu.
public ContextMenuStripReplacement(bool fullMenu, object target)
{
string xmlStr = this.GetContextMenuXMLString();
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlStr);
// build various Add Time menu items, and if showing the whole menu,
// build record-specific menu item
this.BuildMenuItemsForMenu(doc.SelectSingleNode("/ContextMenu/AddTimeMenu"), target, false);
if (fullMenu)
{
this.Items.Add(new ToolStripSeparator { Name = "separatorRecSpec" });
this.recordSpecificMenuItemControlNames.Add("separatorRecSpec");
this.BuildMenuItemsForMenu(doc.SelectSingleNode("/ContextMenu/RecordSpecificMenu"),
target, true);
}
}
The BuildMenuItemsForMenu
method does these same steps, and then invokes the AddClickEventHandler
method so the menu item will respond to a mouse click event. This is where some of the magic of this approach happens.
void BuildMenuItemsForMenu(XmlNode menuXmlNode, object target, bool isRecordSpecific)
{
foreach (XmlNode menuItemNode in menuXmlNode.ChildNodes)
{
// create toolstrip menu item, track record-specific menu items
// that need to be shown/hidden
// depending on where a user right clicks to call up the context menu
// so that clicks on a record get the whole menu, clicks not on a record do not get
// the record specific menu items
string controlName = menuItemNode.Attributes["ControlName"].Value;
if (isRecordSpecific)
if (!this.recordSpecificMenuItemControlNames.Contains(controlName))
this.recordSpecificMenuItemControlNames.Add(controlName);
string menuTitle = menuItemNode.Attributes["Title"].Value;
ToolStripMenuItem toolStripMenuItem = new ToolStripMenuItem(menuTitle)
{ Name = controlName };
this.Items.Add(toolStripMenuItem);
// add click event handler
if (target != null)
{
this.AddClickEventHandler(menuItemNode, toolStripMenuItem, target);
}
}
}
The basics of the AddClickEventHandler
method are to take the name of the method that should be invoked when a mouse click takes place, use reflection to look that method up on the target and obtain a MethodInfo
reference, wrap the MethodInfo
reference in a Delegate so it can be invoked and add the Delegate as the handler to the click event of the ToolStripMenuItem
.
void AddClickEventHandler(XmlNode menuItemNode,
ToolStripMenuItem toolStripMenuItem, object target)
{
string clickEventMethodName = menuItemNode.Attributes["EventHandlerName"].Value;
// tool strip click event
EventInfo toolStripClickEventInfo = toolStripMenuItem.GetType().GetEvent("Click");
// Target has a event called clickEventMethodName
Type type = target.GetType();
var methodInfo = type.GetMethod(clickEventMethodName,
BindingFlags.NonPublic | BindingFlags.Instance);
// make a delegate for the target method
Delegate handler = Delegate.CreateDelegate
(toolStripClickEventInfo.EventHandlerType, target, methodInfo);
// add to toolstrip click event
toolStripClickEventInfo.AddEventHandler(toolStripMenuItem, handler);
}
The sample application provides another example of using these classes to make a reusable context menu. It uses the same XML, but separates the target of the mouse clicks from the UI by placing the event handlers in a non-UI class called MyEventHandlers
. The sample shows reuse of the same context menu in five locations: the main form (cleverly named MainForm
) and in four other forms (cleverly named Form1
, Form2
, Form3
, and Form4
).