Introduction
I had a Tool Menu and a context menu with some of the same menu items. I wanted to make sure the duplicate items were in sync. If one takes an existing menu item and adds it to another menu, the item is moved not duplicated. Thus I needed a way of cloning menu items.
Writing a clone method should have been easy. Many articles describe how to use Reflection to copy properties and the web gives what properties to copy. But the major stumbling block was copying the handlers. In order to add or delete a handler, one needs to know the actual handler. One of .NET’s major frustrations is the inability to easily get at the list of Event Handlers for a component.
A web search showed little so I wrote my own. I call it a Pretty Good Menu Cloner because, although not perfect, the restrictions are modest and rarely come up in actual practice. It also demonstrates how to use Reflection to obtain the list of an objects' events.
Background
This code works by using Reflection to read the private
members of a .NET object. I am assuming the reader understands this as well as LINQ. Thus, I will not be explaining the details of these technologies.
Using the Sample Program
The sample program, called MenuItemCopyHandlersExample
, is basically a test bed that shows how to call the clone method to duplicate a menu item or create a context menu. All the reusable code is in the EventHandlerSupport
project.
The sample program has a menu item called ‘Do Something…’ under the file menu. This item is three levels deep with assorted event callbacks, images and tool tips. The ‘Clone to Menu’ button will clone this item to the Cloned Menu item and the ‘Clone to Context Menu’ will clone it to a context menu. The program demonstrates that all the levels are copied and all the callbacks registered.
Using the Code
The EventHandlerSupport.dll contains a set of extensions to make the cloning easier. The major function is the Clone extension to ToolStipMenuItem
. It returns a cloned menu item, including handlers and submenus. The cloned item has no owner and a slightly different, most likely unique, name from the original.
The decision to make a clone with a different name may seem strange but it is a result of two things. First to clone a menu item, one must duplicate any submenus. Second, although different objects with the same name can be inserted into different components, it is not good Windows Form design practice.
ToolStripMenuItem menuItem = toolStripMenuItem.Clone();
menuItem.Text = "Cloned: " + menuItem.Text;
anotherToolStripMenuItem.DropDownItems.Add(menuItem);
A second extension to a component is AddHandlers
, this adds all the handlers from one component to another. Its usage, albeit not its implementation, is quite simple:
menuItem.AddHandlers(sourceToolStripMenuItem);
This routine will work with two classes derived from IComponent
, not just a menu item.
How to Clone a Menu Item
There are four parts to cloning a menu item:
- Create a new menu item
- Copy over certain properties
- Clone the Event Handlers
- Duplicate the
DropDownItems
Copying Properties
As to what properties to copy, I decided the Read/Write properties that are exposed in the Visual Studio Properties Window would be sufficient. These are the properties whose attribute is not [Browsable(false)]
.
The one problem is that Microsoft decided to add some properties to make ToolStripMenuITem
backward compatible with the older MenuItem
. The only one of these that appears in the property window is DropDown
.
Combining all these requirements, I came up with a pretty concise (albeit opaque) bit of LINQ.
var propInfoList = from p in typeof(ToolStripMenuItem).GetProperties()
let attributes = p.GetCustomAttributes(true)
let notBrowseable = (from a in attributes
where a.GetType() == typeof(BrowsableAttribute)
select !(a as BrowsableAttribute).Browsable
).FirstOrDefault()
where !notBrowseable && p.CanRead && p.CanWrite && p.Name != "DropDown"
select p;
The thing to notice is that FirstOrDefault
returns false
for a Boolean but a lack of attribute means to display it in the properties windows. So if the Browsable attribute is false
, we want to negate it to make it true
. Then we just choose all the ones that are false
, read/writable and name is not the special DropDown
property.
Next, we do the usual Reflection copy:
foreach (var propertyInfo in propInfoList)
{
object propertyInfoValue = propertyInfo.GetValue(sourceToolStripMenuItem, null);
propertyInfo.SetValue(menuItem, propertyInfoValue, null);
}
The property clone has a slight problem. A menu item has a Visible
property that is usually set to true
; however, this property is always turned off when the item is not shown. Thus the clone cannot know what the original value was. It must set Visible
back on. But if the Visible
property was set to false
originally, the clone improperly turns it on. Fixing this will require future investigation.
Cloning the Event Handlers
The menu clone would be easy if one could write menuItem.Click = sourceMenuItem.Click
but events cannot occur on the left side of an assignment. In .NET, a component has a list of possible events stored in an EventHandlerList
class. Furthermore, this class has a procedure AddHandlers
that will add all the events in another EventHandlerList
. Thus to clone, we need only write:
EventHandlerList sourceEventHandlerList;
EventHandlerList destinationEventHandlerList
destinationEventHandlerList.AddHandlers(sourceEventHanderList)
Alas, the problem is that the event list is stored in a private
property of the component called Events
. Thus we must use Reflection to get the event list. This can be done by:
public static EventHandlerList GetEventHandlerList(this IComponent component)
{
var eventsInfo = component.GetType().GetProperty
("Events", BindingFlags.Instance | BindingFlags.NonPublic);
return (EventHandlerList)eventsInfo.GetValue(component, null);
}
Here the non-static private
property “Events
” is found and its value returned. This is somewhat dangerous in the sense that we am inspecting private
members and using its value. In another .NET release, this may not work because there is no guarantee that the private
implementation will remain the same. However, in a mature framework as Windows Forms, such a change is unlikely.
Duplicating the DropDown Items
The very last thing is to clone the sources drop down items. We cannot just copy them because they are menu items and we cannot copy menu items directly. Moreover, the list can contain both ToolStripMenuItems
and ToolStripSeparator
. Duplicating this list is easily done via recursion.
foreach (var item in sourceToolStripMenuItem.DropDownItems)
{
ToolStripItem newItem;
if (item is ToolStripMenuItem)
{
newItem = ((ToolStripMenuItem)item).Clone();
}
else if (item is ToolStripSeparator)
{
newItem = new ToolStripSeparator();
}
else
{
throw new NotImplementedException
("Menu item is not a ToolStripMenuItem or a ToolStripSeparatorr");
}
menuItem.DropDownItems.Add(newItem);
}
Now we can assemble these parts to create a pretty good clone extension. See the actual code for how this is done.
Points of Interest
I've wondered why Microsoft does not have a clone function and the answer is pretty simple. One does not want an actual clone, nor is it possible to always deeply copy without other knowledge.
In my next article, I will show how to use Reflection to walk the EventHandlerList
and return it in a useable form. This can then be used to delete the events or only copy specific ones.
History
- First release, Halloween 2009