Inheritable Menus in C#
08 May 2009I've been coding a program at work that is supposed to interface with and control a wireless sensor network. It's a heterogeneous network where each node may be one of several different types with different capabilities, represented internally by child classes of an abstract "WirelessSensorNode" parent class. To keep track of things, I display all the nodes and their children (things, including other nodes that connect through them) in a TreeView.
The problem I ran into was trying to create a context menu that would be displayed when I right-clicked on a TreeNode in the tree. Because each node class was different, I needed to display slightly different menus for each. I started the naive way, trying to create a new ContextMenuStrip item for each different class, and I ended up with a huge messy piece of logic like this:
object o = myTreeView.SelectedNode.Tag;
if(o is NodeType1) menu1.Show();
else if(o is NodeType2) menu2.Show();
else if(o is NodeType3) menu3.Show();
...
And each menu, because there were some options common to all nodes, needed to contain some duplicate logic. This was, in short, a mess and extending this system to add new node types (and therefore new menus) was a real pain. What I really wanted to do was something like this, and leave the logic of menu building to the classes themselves:
WirelessSensorNode n = (WirelessSensorNode)myTreeView.SelectedNode.Tag;
ContextMenuStrip menu = n.GetContextMenu();
if(menu != null) menu.Show();
So I decided instead to fix this system up yesterday to do the right thing in a better and more encapsulated way. The node classes themselves know what menu items they are capable of showing, my GUI should not be in charge of figuring that information out. So, I added this code to create a new attribute type, ContextMenuHandler for methods in a class, and a routine to read all methods with this attribute and add them to a menu:
[AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
public class ContextMenuHandler : Attribute
{
public string name;
public ContextMenuHandler(string name)
{
this.name = name;
}
}
public static void AddContextMenuHandlers(System.Type c, object parent, ContextMenuStrip menu)
{
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.DeclaredOnly |
BindingFlags.Instance;
foreach (MethodInfo method in c.GetMethods(flags)) {
foreach(object attribute in method.GetCustomAttributes(
typeof(ContextMenuHandler), false)) {
string name = ((ContextMenuHandler)attribute).name;
menu.Items.Add(new ToolStripMenuItem(name, null, new EventHandler(
delegate(object o, EventArgs e) {
method.Invoke(parent, null);
}
)));
}
}
}
And I added this code into the abstract parent class:
public virtual ContextMenuStrip GetContextMenu()
{
ContextMenuStrip menu = new ContextMenuStrip();
Helpers.AddContextMenuHandlers(typeof(WirelessSensorNode), this, menu);
Helpers.AddContextMenuHandlers(this.GetType(), this, menu);
return menu;
}
Not a whole lot of code to write for the flexibility that the system gives me. For people who aren't familiar with attributes, they are metadata items that can be added to methods (or other things) and can be examined at runtime using reflection. With this system, to add a new item to the menu of a particular class, I only need to create a new private method in that class with the ContextMenuHandler attribute:
[ContextMenuHandler("Menu Item 1")]
private void Menu_Item_1_ContextMenuHandler()
{
MessageBox.Show("You clicked 'Menu Item 1'");
}
No need to "install" the new menu item anywhere, the simple existance of this method in the class will enable the item to be shown in that menu. This is great for encapsulation because I don't want to be having to modify all sorts of existing code, especially not unrelated existing code, every time I want to add an option to my right-click menu. Plus, it gives me the ability to inherit menu items from the parent class into the menus of the child types, without having to write code in each child class to duplicate them!
There are some limitations to this method, obviously my class hierarchy is only 2-levels deep so this method as-is doesn't extend to arbitrarily deep class hierarchies. I'm sure it could be extended to do that with a little bit more System.Reflection magic, but I don't need to do it so I'm not going to spend the time. Despite the limitations my goals were met: context menus are inherited between classes, and I can edit/expand menus without having to modify all sorts of existing, working code.
This entry was originally posted on Blogger and was automatically converted. There may be some broken links and other errors due to the conversion. Please let me know about any serious problems.