Introduction
In this article, I'll be showing how you can generate a dynamic MenuStrip
(.NET 2.0's replacement of the MainMenu
control) via an XML Configuration file, using recursion in C#. The MenuStrip
will be based on XML data which contains all the details about the generated menu. Things like: Caption
, Name
, Text
, and the corresponding Event
it has to trigger when a user clicks a MenuItem
.
This is a generalized re-usable control written in such a way that it can be easily plugged in to any application based on the application's requirements.
Why an XML Driven MenuStrip?
Honestly, I've battled with the concept of an XML driven menu, and while I'm still not fully convinced that it would be 100% useful in all situations … I can see how it would be useful in some applications.
The only thing I can come up with to really answer this is … "why not?" I mean, .NET 3.0 will pretty much make this control out-dated with WPF and XAML, but in the meantime, why shouldn't I be able to supply a path to an XML configuration file to setup my MenuStrip
control and have it generated at run time as opposed to hard coding all the ToolStripMenuItem
values at design time.
The company that I work for has multiple clients that run our application. Each customer puts more focus on different areas of the application than the other customers, which is perfectly understandable. As many developers know, customers tend to want different things out of the application. This typically controls how dynamic an application should be as well as how customizable an application should be. Having to make a separate build for each of the customers just because we want to rename one of their menu items was not a feasible solution. This is where I originally got the idea for the XML-driven Menu Strip.
Pros and Cons
Pros of an XML-Driven Menu
- Adding and removing
ToolStripMenuItem
does not require an application rebuild. - Customers, end-users, developers, etc. can rename
MenuItem
in the XML configuration file to better suit their needs or to give a MenuItem
more "meaning" in their environment. - A decent way to get new developers started on the learning curve for the application.
Cons of an XML-Driven Menu
- Customers, End Users, Developers, etc., have the ability to rename their
MenuItem
elements at any time. This could become confusing if someone renames the "File > Exit" MenuItem
to "File > Open." - Supporting the application becomes a bit more difficult as Support no longer has the ability to say for certain where to go in the menu structure as their "File" could be easily renamed to "Option1" (just an example).
Of course, I'm sure there are many more pros and cons that you can come up with, but these were just a few that I came up with off the top of my head.
Points of Interest
One main thing here is that this control uses the new, relatively speaking, .NET 2.0 MenuStrip
control and not the .NET 1.1 MainMenu
control. There are some noticeable differences in the two. For a more detailed explanation, check out Jim Hollenhorst's CodeProject article: "Upgrading from MainMenu and ToolBar to MenuStrip and ToolStrip."
Things to Note
Aside from showing how to enhance the MenuStrip
control to use an XML file as its data source, this control also shows some pretty basic techniques for using Reflection, loading files from embedded resources, parsing an XML file, and organizing an XML file. I'm sure there are other interesting things to note as well.
Using the Code
The main purpose of this control is to generate an applications menu via a MenuStrip
control, at runtime, based on values provided in an XML configuration file that can be located in a file system folder or embedded as a resource directly in to your application.
The XML Structure
<root>
<TopLevelMenu ID="&File">
<MenuItem ID="New" OnClick="_New" />
<MenuItem ID="Open" OnClick="_Open" />
<MenuItem ID="-" />
<MenuItem ID="Close" OnClick="_Close" />
<MenuItem ID="-" />
<MenuItem ID="E&xit" OnClick="_Exit" />
</TopLevelMenu>
<TopLevelMenu ID="&Edit">
<MenuItem ID="Undo" OnClick="_Undo" />
<MenuItem ID="-" />
<MenuItem ID="Cut" OnClick="_Cut" />
<MenuItem ID="Copy" OnClick="_Copy" />
<MenuItem ID="Paste" OnClick="_Paste" />
<MenuItem ID="-" />
<MenuItem ID="Options">
<MenuItem ID="Sub Menu Item">
<MenuItem ID="Sub Sub Menu Item" />
</MenuItem>
</MenuItem>
</TopLevelMenu>
</root>
There's really nothing too extreme about the XML structure. It uses a root
element to encompass the TopLevelMenu
elements. The application can support N number of TopLevelMenu
elements. Under each of the TopLevelMenu
elements are MenuItem
elements which also have N-level support.
The XML list above will generate a simple menu that appears as:
Currently, the control doesn't have an XSD file, but perhaps in future revisions, I'll get around to adding one for added functionality. The control only has a limited number of attributes at the moment, but in the near future, it will attempt to provide many hooks in to all the ToolStripMenuItem
events and properties.
Valid XML Attributes
Name | Required | This is the .Name value that gets assigned to the ToolStripMenuItem . |
Text | Required | This is the .Text value that gets assigned to the ToolStripMenuItem . |
OnClick | Optional | This is the OnClick Event Handler that will be called when the ToolStripMenuItem is clicked. |
The OnClick
attribute is the most notable attribute as it tells the application which event should be fired when a user clicks that specific MenuItem
element. Let's take a look at the "New" MenuItem
in the above XML.
<MenuItem ID="New" OnClick="_New" />
This means that in the Form
that this MenuStrip
is being displayed, it should have the following code to handle the Event:
private void MenuItemOnClick_New( object sender, EventArgs e )
{
MessageBox.Show( "New Clicked" );
}
This event handler is based on the following format:
MenuItemOnClick | This is a hard coded value in the control. It doesn't come from your XML configuration file. |
_New | This is defined in the OnClick attribute of your element. If you change the _New to _NewItem , then your Form event should also be changed as well. |
The Dynamic Menu control exposes Event Handlers for each and every MenuItem
element you have created with an attribute value of "OnClick
." If you don't want to create an Event Handler for a MenuItem
, then you don't need to specify the OnClick
attribute for it in the XML file.
Now, let's get into the nitty-gritty about the control…
MenuStripEnhanced Control
The MenuStripEnhanced
control inherits from System.Windows.Forms.MenuStrip
. So it provides all the original functionality of the standard MenuStrip
as well as the added functionality for loading its data via an XML configuration file.
* Revision 1: Edited to reflect Matt's suggestions about the OnPaint()
event.
The public
method LoadDynamicMenu()
is where all the magic happens. The method begins by doing some checks to make sure that the XML configuration file exists and is in the correct directory. After I check to make sure each of the conditions have been met, the control will then begin parsing the XML configuration file.
* Revision 2: Edited to reflect the overloaded LoadDynamicMenu()
. Thanks, Matt.
LoadDynamicMenu()
has three overloads to support different types of loading. The three overloads are:
public void LoadDynamicMenu();
public void LoadDynamicMenu( string xmlPath );
public void LoadDynamicMenu( XmlTextReader xmlReader );
public void LoadDynamicMenu()
{
if ( XmlPath != string.Empty )
LoadDynamicMenu( XmlPath );
}
This overload just allows you to set the .XmlPath
property and load the XML configuration file directly from a physical XML file on the hard drive:
public void LoadDynamicMenu( string xmlPath )
{
if ( System.IO.Path.IsPathRooted( XmlPath ) )
xmlPath = XmlPath;
else
xmlPath = System.IO.Path.Combine( Application.StartupPath, XmlPath );
if ( System.IO.File.Exists( XmlPath ) )
{
XmlTextReader reader = new XmlTextReader( xmlPath );
LoadDynamicMenu( reader );
}
}
This overload allows you to pass in a specific XML file so that you don't need to set the property and then call the method:
public void LoadDynamicMenu( XmlTextReader xmlReader )
{
this.Items.Clear();
XmlDocument document = new XmlDocument();
document.Load( xmlReader );
XmlElement element = document.DocumentElement;
foreach ( XmlNode node in document.FirstChild.ChildNodes )
{
ToolStripMenuItem menuItem = new ToolStripMenuItem();
menuItem.Name = node.Attributes["Name"].Value;
menuItem.Text = node.Attributes["Text"].Value;
this.Items.Add( menuItem );
GenerateMenusFromXML( node, (ToolStripMenuItem)this.Items[this.Items.Count - 1] );
}
}
This code allows you to pass in an XmlTextReader
object. This is useful if you want to store your XML files as resources inside your application.
The downloadable sample application shows details on how you can use each of the overloads to load the menu configuration file.
All these overloads are fairly straightforward. They each flow to one main method that loads up the XML configuration file in to an XmlDocument()
object and then walks through all the TopLevelMenu
elements.
It will then pass in the current XML node as well as the ToolStripMenuItem
in to the GenerateMenusFromXML( XmlNode rootNode, ToolStripMenuItem menuItem )
function. The GenerateMenusFromXML()
is a recursive function that will parse N-level MenuItem
s.
private void GenerateMenusFromXML( XmlNode rootNode, ToolStripMenuItem menuItem )
{
ToolStripItem item = null;
ToolStripSeparator separator = null;
foreach ( XmlNode node in rootNode.ChildNodes )
{
if ( node.Attributes["Text"].Value == "-" )
{
separator = new ToolStripSeparator();
menuItem.DropDownItems.Add( separator );
}
else
{
item = new ToolStripMenuItem();
item.Name = node.Attributes["Name"].Value;
item.Text = node.Attributes["Text"].Value;
menuItem.DropDownItems.Add( item );
if ( node.Attributes["FormLocation"] != null )
item.Tag = node.Attributes["FormLocation"].Value;
if ( node.Attributes["OnClick"] != null )
{
FindEventsByName( item, this.Form, true, "MenuItemOn",
node.Attributes["OnClick"].Value );
}
GenerateMenusFromXML( node,
(ToolStripMenuItem)menuItem.DropDownItems[menuItem.DropDownItems.Count - 1] );
}
}
}
So far, the code has been fairly straightforward, but things get a bit more interesting when the GenerateMenusFromXML()
finds an OnClick
attribute on a MenuItem
element.
if ( node.Attributes["OnClick"] != null )
{
FindEventsByName( item, this.Form, true, "MenuItemOn",
node.Attributes["OnClick"].Value );
}
FindEventsByName()
uses reflection to search the passed in receiver object, which is actually the Form
that the MenuStripEnhanced
control is attached to, to find the Event Handler that was provided in the value of the OnClick
attribute.
Possible Future Enhancements
- Add
Icon
attribute to support MenuItem
element. - Add more
Event
attributes to the XML configuration. - Add support for encrypting the XML file so that it cannot be changed by just anyone. [This was sort of done by allowing the XML files to be loaded as Embedded Resources.]
- Add XSD support for the XML configuration file.
Revision History
Wednesday, April 18, 2007
Version 1.0.2
- Added two overloads for
LoadDynamicMenu()
- Can now load the menu configuration file from an Embedded Resource
Tuesday, April 17, 2007
Version 1.0.1
- Removed the
OnPaint()
override and updated the article to reflect the changes [*Based on Matt's comments below]
Monday, April 16, 2007
Version 1.0
Conclusion
The control itself could still use some work, but for the most part, it's a decent start to getting a fully customized MenuStrip
control and I intend on making upgrades to it as I have the time.