一个非常好的菜单克隆器
允许轻松克隆菜单项,并演示如何克隆组件的回调处理程序

引言
我有一个工具菜单和一个包含一些相同菜单项的上下文菜单。我想确保重复的项是同步的。如果将一个现有的菜单项添加到另一个菜单中,该项将被移动而不是复制。因此,我需要一种克隆菜单项的方法。
编写一个克隆方法应该很容易。许多文章都描述了如何使用反射来复制属性,网络上也提供了要复制的属性。但主要障碍是复制事件处理程序。为了添加或删除事件处理程序,需要知道实际的事件处理程序。.NET 的一个主要令人沮丧之处在于无法轻松访问组件的事件处理程序列表。
网上搜索到的信息很少,所以我自己写了一个。我称之为“相当不错的菜单克隆器”,因为虽然不完美,但限制很小,在实际应用中很少遇到。它还演示了如何使用反射来获取对象事件的列表。
背景
此代码通过使用反射读取 .NET 对象的私有
成员来工作。我假设读者理解这一点以及 LINQ。因此,我将不解释这些技术的细节。
使用示例程序
名为MenuItemCopyHandlersExample
的示例程序基本上是一个测试平台,展示了如何调用克隆方法来复制菜单项或创建上下文菜单。所有可重用代码都在EventHandlerSupport
项目中。
示例程序的文件菜单下有一个名为“做某事…”的菜单项。该项有三层深度,带有各种事件回调、图像和工具提示。“克隆到菜单”按钮会将此项克隆到“克隆菜单”项,“克隆到上下文菜单”按钮会将其克隆到上下文菜单。该程序演示了所有级别都已复制,并且所有回调都已注册。
Using the Code
EventHandlerSupport.dll包含了一组用于简化克隆的扩展。主要功能是ToolStipMenuItem
的Clone扩展。它返回一个克隆的菜单项,包括事件处理程序和子菜单。克隆的项没有所有者,名称与原始项略有不同,很可能是唯一的。
将克隆项命名为不同可能看起来奇怪,但这是两个因素的结果。首先,要克隆菜单项,必须复制任何子菜单。其次,虽然名称相同的不同对象可以插入到不同的组件中,但这并不是好的 Windows 窗体设计实践。
//
// Clone an item and adds it to another menu
//
ToolStripMenuItem menuItem = toolStripMenuItem.Clone();
menuItem.Text = “Cloned: “ + menuItem.Text;
anotherToolStripMenuItem.DropDownItems.Add(menuItem);
对组件的第二个扩展是AddHandlers
,它将一个组件的所有事件处理程序添加到另一个组件。它的用法(尽管不是实现)非常简单
//
// Add the handlers from the sourceToolStripMenuItem to menuItem
//
menuItem.AddHandlers(sourceToolStripMenuItem);
此例程适用于两个派生自IComponent
的类,而不仅仅是菜单项。
如何克隆菜单项
克隆菜单项有四个部分
- 创建新的菜单项
- 复制某些属性
- 克隆事件处理程序
- 复制
DropDownItems
复制属性
至于要复制哪些属性,我决定 Visual Studio 属性窗口中公开的读/写属性就足够了。这些是属性不是[Browsable(false)]
的属性。
唯一的问题是,微软决定添加一些属性以使ToolStripMenuITem
向后兼容旧的MenuItem
。其中唯一出现在属性窗口中的是DropDown
。
将所有这些要求结合起来,我得到了一个相当简洁(尽管晦涩)的 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;
需要注意的是,FirstOrDefault
对于布尔值返回false
,但缺少属性意味着将其显示在属性窗口中。所以如果 Browsable 属性是false
,我们想否定它使其变为true
。然后我们只选择所有false
、读/写并且名称不是特殊的DropDown
属性的属性。
接下来,我们进行常规的反射复制
// Copy over using reflections
foreach (var propertyInfo in propInfoList)
{
object propertyInfoValue = propertyInfo.GetValue(sourceToolStripMenuItem, null);
propertyInfo.SetValue(menuItem, propertyInfoValue, null);
}
属性克隆存在一个小问题。菜单项有一个通常设置为true
的Visible
属性;但是,当该项未显示时,此属性总是关闭的。因此,克隆无法知道原始值是什么。它必须重新设置为Visible
。但是,如果Visible
属性最初设置为false
,克隆会错误地将其打开。修复此问题需要未来的研究。
克隆事件处理程序
如果可以写menuItem.Click = sourceMenuItem.Click
,那么菜单克隆就会很容易,但事件不能出现在赋值的左侧。在 .NET 中,组件有一个存储在EventHandlerList
类中的可能事件列表。此外,此类有一个过程AddHandlers
,它将另一个EventHandlerList
中的所有事件添加进去。因此,要克隆,我们只需要编写
//
// Add one set of Event Handlers to another
//
EventHandlerList sourceEventHandlerList;
EventHandlerList destinationEventHandlerList
destinationEventHandlerList.AddHandlers(sourceEventHanderList)
唉,问题是事件列表存储在组件的一个名为Events
的私有
属性中。因此,我们必须使用反射来获取事件列表。这可以通过以下方式完成:
/// <summary>
/// Gets the event handler list from a component
/// </summary>
/// <param name="component">The source component.</param>
/// <returns>The EventHanderList or null if none</returns>
public static EventHandlerList GetEventHandlerList(this IComponent component)
{
var eventsInfo = component.GetType().GetProperty
("Events", BindingFlags.Instance | BindingFlags.NonPublic);
return (EventHandlerList)eventsInfo.GetValue(component, null);
}
这里找到了非静态的私有
属性“Events
”并返回了它的值。这有点危险,因为我们正在检查私有
成员并使用它的值。在另一个 .NET 版本中,这可能不起作用,因为不能保证私有
实现会保持不变。然而,在像 Windows Forms 这样的成熟框架中,不太可能发生这种变化。
复制下拉项
最后一件事情是克隆源的下拉项。我们不能仅仅复制它们,因为它们是菜单项,我们不能直接复制菜单项。此外,列表可以包含ToolStripMenuItems
和ToolStripSeparator
。通过递归可以轻松复制此列表。
//
// Recursively clone the drop down list
//
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);
}
现在我们可以组合这些部分来创建一个相当不错的克隆扩展。有关如何做到这一点,请参阅实际代码。
关注点
我一直想知道为什么微软没有克隆功能,答案很简单。一个人不想要一个真正的克隆,而且也不总是可能在没有其他知识的情况下深度复制。
在我的下一篇文章中,我将展示如何使用反射来遍历EventHandlerList
并将其以可用形式返回。然后可以使用它来删除事件或只复制特定的事件。
历史
- 首次发布,2009 年万圣节