一个 XML 驱动的菜单条
在本文中,我将展示如何使用 C# 中的递归来生成动态 MenuStrip(.NET 2.0 替换了 MainMenu 控件)。MenuStrip 将基于 XML 数据,该数据包含有关生成菜单的所有详细信息。
引言
在本文中,我将展示如何通过 XML 配置文件,使用 C# 中的递归来生成动态 MenuStrip
(.NET 2.0 替换了 MainMenu
控件)。MenuStrip
将基于 XML 数据,该数据包含有关生成菜单的所有详细信息。例如:Caption
、Name
、Text
以及用户单击 MenuItem
时要触发的相应 Event
。
这是一个通用的可重用控件,其编写方式可以根据应用程序的要求轻松地将其集成到任何应用程序中。
为什么使用 XML 驱动的 MenuStrip?
老实说,我一直在纠结 XML 驱动菜单的概念,虽然我仍然不完全相信它在所有情况下都能 100% 有用……但我可以看到它在某些应用程序中的用处。
我唯一能想到的真正回答是……“为什么不呢?” 我的意思是,.NET 3.0 的 WPF 和 XAML 将使这个控件过时,但在此期间,为什么我不能提供一个 XML 配置文件路径来设置我的 MenuStrip
控件,并在运行时生成它,而不是在设计时硬编码所有 ToolStripMenuItem
的值?
我工作的公司有多个客户运行我们的应用程序。每个客户比其他客户更侧重于应用程序的不同区域,这是完全可以理解的。正如许多开发人员所知,客户倾向于从应用程序中获得不同的东西。这通常决定了应用程序应该多么动态以及应用程序应该多么可定制。仅仅因为我们想重命名其中一个客户的菜单项就必须为每个客户进行单独的构建,这并不可行。这就是我最初获得 XML 驱动 Menu Strip 想法的来源。
优点和缺点
XML 驱动菜单的优点
- 添加和删除
ToolStripMenuItem
不需要重新编译应用程序。 - 客户、最终用户、开发人员等可以重命名 XML 配置文件中的
MenuItem
,以更好地满足他们的需求或为MenuItem
在其环境中赋予更多“含义”。 - 让新开发人员快速入门应用程序学习曲线的好方法。
XML 驱动菜单的缺点
- 客户、最终用户、开发人员等可以随时重命名他们的
MenuItem
元素。如果有人将“文件 > 退出”MenuItem
重命名为“文件 > 打开”,这可能会令人困惑。 - 支持应用程序变得更加困难,因为支持部门无法确定在菜单结构中去哪里,因为他们的“文件”可能会被轻松重命名为“Option1”(这只是一个例子)。
当然,我相信您还能想到更多的优缺点,但这些只是我脱口而出的几个。
关注点
这里的一个主要问题是,此控件使用了相对较新的 .NET 2.0 MenuStrip
控件,而不是 .NET 1.1 的 MainMenu
控件。两者之间存在一些明显的差异。有关更详细的解释,请查看 Jim Hollenhorst 的 CodeProject 文章:“从 MainMenu 和 ToolBar 升级到 MenuStrip 和 ToolStrip。”
注意事项
除了展示如何增强 MenuStrip
控件以使用 XML 文件作为其数据源之外,此控件还展示了一些相当基本的技术,例如使用反射、从嵌入式资源加载文件、解析 XML 文件以及组织 XML 文件。我相信还有其他有趣的事情值得注意。
Using the Code
此控件的主要目的是通过 MenuStrip
控件在运行时生成应用程序的菜单,该菜单基于 XML 配置文件中的值,该文件可以位于文件系统文件夹中,也可以直接嵌入到您的应用程序作为资源。
XML 结构
<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>
XML 结构并没有什么特别之处。它使用一个 root
元素来包含 TopLevelMenu
元素。应用程序可以支持 N 个 TopLevelMenu
元素。在每个 TopLevelMenu
元素下是 MenuItem
元素,它们也支持 N 级。
上面的 XML 列表将生成一个简单的菜单,如下所示:
目前,该控件还没有 XSD 文件,但也许在未来的版本中,我会设法添加一个以增加功能。该控件目前只有有限数量的属性,但在不久的将来,它将尝试为所有 ToolStripMenuItem
事件和属性提供许多挂钩。
有效的 XML 属性
名称 | 必需 | 这是分配给 ToolStripMenuItem 的 .Name 值。 |
文本 | 必需 | 这是分配给 ToolStripMenuItem 的 .Text 值。 |
OnClick | 可选 | 这是当 ToolStripMenuItem 被单击时将被调用的 OnClick 事件处理程序。 |
OnClick
属性是最值得注意的属性,因为它告诉应用程序当用户单击特定 MenuItem
元素时应该触发哪个事件。让我们来看一下上面 XML 中的“新建”MenuItem
。
<MenuItem ID="New" OnClick="_New" />
这意味着在显示此 MenuStrip
的 Form
中,应该有以下代码来处理该事件:
private void MenuItemOnClick_New( object sender, EventArgs e )
{
MessageBox.Show( "New Clicked" );
}
此事件处理程序基于以下格式:
MenuItemOnClick | 这是控件中的一个硬编码值。它不是来自您的 XML 配置文件。 |
_New | 这在您元素的 OnClick 属性中定义。如果您将 _New 更改为 _NewItem ,那么您的 Form 事件也应该相应更改。 |
Dynamic Menu 控件为您创建的每个 MenuItem
元素公开了事件处理程序,只要该元素具有“OnClick
”属性值。如果您不想为 MenuItem
创建事件处理程序,那么您不需要在 XML 文件中为此指定 OnClick
属性。
现在,让我们深入了解该控件的细节……
MenuStripEnhanced 控件
MenuStripEnhanced
控件继承自 System.Windows.Forms.MenuStrip
。因此,它提供了标准 MenuStrip
的所有原始功能,以及通过 XML 配置文件加载其数据的附加功能。
* 版本 1:编辑以反映 Matt 关于 OnPaint()
事件的建议。
public
方法 LoadDynamicMenu()
是所有魔法发生的地方。该方法首先进行一些检查,以确保 XML 配置文件存在并且位于正确的目录中。在我检查确保所有条件都已满足后,控件将开始解析 XML 配置文件。
* 版本 2:编辑以反映重载的 LoadDynamicMenu()
。谢谢,Matt。
LoadDynamicMenu()
有三个重载来支持不同类型的加载。这三个重载是:
public void LoadDynamicMenu();
public void LoadDynamicMenu( string xmlPath );
public void LoadDynamicMenu( XmlTextReader xmlReader );
public void LoadDynamicMenu()
{
if ( XmlPath != string.Empty )
LoadDynamicMenu( XmlPath );
}
此重载允许您设置 .XmlPath
属性,并直接从硬盘上的物理 XML 文件加载 XML 配置文件。
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 );
}
}
此重载允许您传递一个特定的 XML 文件,这样您就不需要设置属性然后再调用方法。
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] );
}
}
此代码允许您传递一个 XmlTextReader
对象。如果您想将 XML 文件存储为应用程序内的资源,这将非常有用。
可下载的示例应用程序详细介绍了如何使用每个重载来加载菜单配置文件。
所有这些重载都相当简单。它们都流向一个主方法,该方法将 XML 配置文件加载到 XmlDocument()
对象中,然后遍历所有 TopLevelMenu
元素。
然后,它会将当前的 XML 节点以及 ToolStripMenuItem
传递到 GenerateMenusFromXML( XmlNode rootNode, ToolStripMenuItem menuItem )
函数中。GenerateMenusFromXML()
是一个递归函数,它将解析 N 级 MenuItem
。
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;
// add an event handler to the menu item added
if ( node.Attributes["OnClick"] != null )
{
FindEventsByName( item, this.Form, true, "MenuItemOn",
node.Attributes["OnClick"].Value );
}
GenerateMenusFromXML( node,
(ToolStripMenuItem)menuItem.DropDownItems[menuItem.DropDownItems.Count - 1] );
}
}
}
到目前为止,代码相当简单,但当 GenerateMenusFromXML()
在 MenuItem
元素上找到 OnClick
属性时,事情会变得更有趣。
// add an event handler to the menu item added
if ( node.Attributes["OnClick"] != null )
{
FindEventsByName( item, this.Form, true, "MenuItemOn",
node.Attributes["OnClick"].Value );
}
FindEventsByName()
使用反射来搜索传入的接收器对象(实际上是附加到 MenuStripEnhanced
控件的 Form
),以查找在 OnClick
属性值中提供的事件处理程序。
未来的可能增强功能
- 添加
Icon
属性以支持MenuItem
元素。 - 将更多
Event
属性添加到 XML 配置中。 - 添加对 XML 文件加密的支持,使其不易被任何人更改。[这可以通过允许 XML 文件作为嵌入式资源加载来部分实现。]
- 为 XML 配置文件添加 XSD 支持。
修订历史
2007 年 4 月 18 日,星期三
版本 1.0.2
- 为
LoadDynamicMenu()
添加了两个重载。 - 现在可以从嵌入式资源加载菜单配置文件。
2007 年 4 月 17 日,星期二
版本 1.0.1
- 删除了
OnPaint()
重写,并更新了文章以反映更改[*基于 Matt 在下面的评论]
2007 年 4 月 16 日,星期一
版本 1.0
- 首次发布
结论
控件本身仍有改进空间,但总的来说,它是实现完全自定义 MenuStrip
控件的一个不错的开端,我打算在有时间的时候对其进行升级。