前言
最近事情较多,终于有时间来写完这篇。在上一篇的基础上,本篇文章我们开始着手搭建一个简单的基于插件架构的Winform框架。(其实也就是一个小例子,也是对之前写过的代码的总结)
设计思路
写这个Winform小例子的想法来源主要是:
1.希望Winform程序能够根据配置动态生成菜单;
2.运行时按需加载指定模块,必要时还能在运行时更新指定模块;
3.新的功能模块能够即插即用,可扩展;
4.宿主、插件间的基础信息可共享,可交互。
实现
如上文所述,我们想要使用插件结构,那么解决方案的项目结构可如下图所示:
解决方案分为三个project:
wZhang.Host.csproj——宿主。window应用程序,主窗体为MainForm.cs,且为MDI窗体;输出路径:/publish。
wZhang.PlugInCommon.csproj——公共接口。类库;输出路径:/publish。
wZhang.MyPlugIn.csproj——具体插件。类库;输出路径:/publish/MyPlugIn。
说明:我这里为了方便起见,将所有的dll都输出的指定目录publish下了。
具体步骤如下:
步骤一:从配置文件中读取菜单项
首先,定义菜单结构如下:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <Main>
3: <MenuItem>
4: <ID>010000</ID>
5: <MenuName><![CDATA[文件(&F)]]></MenuName>
6: <MenuItem>
7: <ID>010100</ID>
8: <MenuName><![CDATA[新建(&N)]]></MenuName>
9: <PlugIn></PlugIn>
10: <MenuItem>
11: <ID>010101</ID>
12: <MenuName><![CDATA[项目(&P)]]></MenuName>
13: <PlugIn></PlugIn>
14: </MenuItem>
15: <MenuItem>
16: <ID>010102</ID>
17: <MenuName><![CDATA[网站(&W)]]></MenuName>
18: <PlugIn></PlugIn>
19: </MenuItem>
20: </MenuItem>
21: <MenuItem>
22: <ID>010200</ID>
23: <MenuName><![CDATA[打开(&O)]]></MenuName>
24: <PlugInPath>./MyPlugIn/wZhang.MyPlugIn.dll</PlugInPath>
25: <PlugIn>wZhang.MyPlugIn.MyPlugIn,wZhang.MyPlugIn</PlugIn>
26: </MenuItem>
27: </MenuItem>
28: <MenuItem>
29: <ID>020000</ID>
30: <MenuName><![CDATA[编辑(&E)]]></MenuName>
31: <MenuItem>
32: <ID>020100</ID>
33: <MenuName><![CDATA[复制(&C)]]></MenuName>
34: <PlugIn></PlugIn>
35: </MenuItem>
36: </MenuItem>
37: </Main>
说明:为了能够正确的反射出具体的插件,
(1)这里使用PlugIn标签配置插件的FullName和AssemblyName;
(2)使用PlugInPath配置插件路径,大部分情况下dll都是按模块存放的;
(3)Winform快捷键使用“&E”结构实现,所以文档中出现了如“<![CDATA[编辑(&E)]]>”节点
(4)这里稍微约定了一下菜单ID的含义:010000,020000,…,0N0000:标识一级菜单,0N0100,0N0200,…0N0N00:标识二级菜单,0N0N01,0N0N02,…,0N0N0N:标识三级菜单。(如果还有更多级菜单可以继续扩展)。
其次,在PlugInCommon项目中添加类MenuItem和AppMenu两个类,用于反序列化菜单项:
1: /// <summary>
2: /// 主菜单
3: /// </summary>
4: [XmlRoot("Main", IsNullable = false)]
5: public class AppMenu
6: {
7: [XmlElement("MenuItem",IsNullable=false)]
8: public List<MenuItem> MenuItems { get; set; }
9: }
10:
11: /// <summary>
12: /// 菜单项
13: /// </summary>
14: public class MenuItem
15: {
16: /// <summary>
17: /// 菜单名称
18: /// </summary>
19: [XmlElement("MenuName", IsNullable = false)]
20: public string MenuName { get; set; }
21:
22: /// <summary>
23: /// 菜单ID
24: /// </summary>
25: [XmlElement("ID",IsNullable = false)]
26: public string MenuId { get; set; }
27:
28: /// <summary>
29: /// 插件
30: /// </summary>
31: [XmlElement("PlugIn")]
32: public string PlugIn { get; set; }
33:
34: /// <summary>
35: /// 插件所在路径
36: /// </summary>
37: [XmlElement("PlugInPath")]
38: public string PlugInPath { get; set; }
39:
40: /// <summary>
41: /// 子菜单
42: /// </summary>
43: [XmlElement("MenuItem")]
44: public List<MenuItem> SubMenus { get; set; }
45: }
最后,反序列化菜单项。读出配置文件资源后,使用XmlSerializer类反序列化菜单项:
1: /// <summary>
2: /// 获取菜单对象
3: /// </summary>
4: /// <returns></returns>
5: private AppMenu GetAppMenu()
6: {
7: var currentAssembly = Assembly.GetExecutingAssembly();
8: string menuPath = currentAssembly.GetName().Name + ".Menu.xml";
9: using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(menuPath))
10: {
11: XmlSerializer serializer = new XmlSerializer(typeof(AppMenu));
12: AppMenu appMenu = serializer.Deserialize(stream) as AppMenu;
13: return appMenu;
14: }
15: }
代码说明:获取菜单时候由于上面我们将菜单文件的属性设置成了嵌入的资源(这样输出目录中看不到具体的菜单配置信息),所以菜单路径(menuPath)读取方式跟普通文件的读取有些许区别。
这样我们就得到了一个菜单对象。等需要的时候我们就可以按需添加到页面上.
步骤二 : 定义接口
首先,我们创建一个窗体基类:BaseForm.cs,基类的好处,不多说了。
其次,上篇文章我们已经知道如何加载插件了,为了实现插件能够获取宿主信息,这里再增加一个上下文的接口IAppContext,用于插件获取宿主信息:
1: /// <summary>
2: /// 上下文
3: /// </summary>
4: public interface IAppContext
5: {
6: UserInfo User { get; set; }
7:
8: //还可以定义很多需要的属性
9: }
上篇文章中的插件接口稍微修改一下:
1: /// <summary>
2: /// 插件接口
3: /// </summary>
4: public interface IPlugIn
5: {
6: /// <summary>
7: /// 应用程序上下文
8: /// </summary>
9: IAppContext AppContext
10: {
11: get;
12: set;
13: }
14:
15: /// <summary>
16: /// 创建子窗体
17: /// </summary>
18: /// <returns></returns>
19: BaseForm CreatePlugInForm();
20: }
通过属性AppContext插件可以获取到宿主相关信息;另外,由于我们这里做的是一个winform程序,所以提供CreatePlugInForm方法来创建具体的窗体。
需要指出的是,这里并没有直接将窗体作为插件,因为:
(1)窗体对象实在太大;
(2)限定为窗体后程序不方便移植到别的地方,如:WPF上。
定义好接口后,需要将宿主实现IAppContext,宿主想要给插件哪些信息,只需要给IAppContext中定义的属性赋值即可。
步骤三:创建菜单控件
创建菜单控件之前,先介绍本示例是如何实现插件的动态加载的:
我们将从配置文件中读取出的每一个菜单(即:每一个MenuItem对象),都绑定到每一个ToolStripMenuItem对象的Tag属性上。这样当我们点击某一个菜单时,便可去除Tag属性中的MenuItem对象,从而加载MenuItem中指定的插件。
到这里为止,我们知道宿主程序是一个Windows应用程序,有一个主窗体Mainform,实现了IAppContext接口,且已经从菜单项配置文件中获得了指定的菜单对象,那么这时我们应该创建一个菜单控件了,代码如下:
1: /// <summary>
2: /// 创建菜单控件
3: /// </summary>
4: /// <param name="appMenu">配置文件中读取的菜单对象</param>
5: private void CreateMenuStrip(AppMenu appMenu)
6: {
7: foreach (var item in appMenu.MenuItems)
8: {
9: ToolStripMenuItem rootMenu = new ToolStripMenuItem();
10: rootMenu.Name = item.MenuId;
11: rootMenu.Text = item.MenuName;
12: rootMenu.Tag = item;
13: if (item.SubMenus != null && item.SubMenus.Count > 0)
14: {
15: var subItem = CreateDropDownMenu(rootMenu, item.SubMenus);
16: }
17: MainMenu.Items.Add(rootMenu);
18: }
19: MainMenu.Refresh();
20: }
21:
22: /// <summary>
23: /// 创建下拉菜单
24: /// </summary>
25: /// <param name="rootMenu">根菜单</param>
26: /// <param name="menuItems">需要加载的菜单项(配置文件中的)</param>
27: /// <returns>一组完整的菜单</returns>
28: private ToolStripMenuItem CreateDropDownMenu(ToolStripMenuItem rootMenu, List<MenuItem> menuItems)
29: {
30: foreach (var item in menuItems)
31: {
32: ToolStripMenuItem subItem = new ToolStripMenuItem();
33: subItem.Name = item.MenuId;
34: subItem.Text = item.MenuName;
35: if (item.SubMenus != null && item.SubMenus.Count > 0)
36: {
37: var dropdowmItem = CreateDropDownMenu(subItem, item.SubMenus);
38: rootMenu.DropDownItems.Add(subItem);
39: }
40: else
41: {
42: subItem.Tag = item;
43: subItem.Click += subItem_Click;
44: rootMenu.DropDownItems.Add(subItem);
45: }
46: }
47: return rootMenu;
48: }
可能注意到有这样一句代码:subItem.Click += subItem_Click,这里既是为每一个叶子菜单绑定一个菜单点击事件,该事件里做的事情就是加载指定的插件,也就是步骤四将要描述的。
步骤四:动态加载插件
我们这里加载插件的方式和上篇文章中介绍的有点区别:这里是一次只加载一个插件。代码稍微修改后如下:
1: /// <summary>
2: /// 加载插件
3: /// </summary>
4: /// <param name="dllPath">插件所在路径</param>
5: /// <param name="fullName">插件全名:namespace+className</param>
6: /// <returns>具体插件</returns>
7: public IPlugIn LoadPlugIn(string dllPath,string fullName)
8: {
9: Assembly pluginAssembly = null;
10: string path = System.IO.Directory.GetCurrentDirectory() + dllPath;
11: try
12: {
13: //加载程序集
14: pluginAssembly = Assembly.LoadFile(path);
15: }
16: catch (Exception ex)
17: {
18: return null;
19: }
20: Type[] types = pluginAssembly.GetTypes();
21: foreach (Type type in types)
22: {
23: if (type.FullName == fullName && type.GetInterface("IPlugIn") != null)
24: {//仅是需要加载的对象才创建插件的实例
25: IPlugIn plugIn = (IPlugIn)Activator.CreateInstance(type);
26: plugIn.AppContext = this;
27: return plugIn;
28: }
29: }
30: return null;
31: }
代码说明:代码plugIn.AppContext = this;便是将宿主对象共享给插件使用。
点击菜单触发加载插件的点击事件实现如下:
1: /// <summary>
2: /// 点击菜单触发事件
3: /// </summary>
4: /// <param name="sender"></param>
5: /// <param name="e"></param>
6: void subItem_Click(object sender, EventArgs e)
7: {
8: ToolStripMenuItem tooStripMenu = sender as ToolStripMenuItem;
9: if (tooStripMenu == null)
10: {
11: return;
12: }
13: MenuItem menuItem = tooStripMenu.Tag as MenuItem;
14: if (menuItem == null)
15: return;
16: //获取插件对象
17: IPlugIn plugIn = LoadPlugIn(menuItem.PlugInPath, menuItem.PlugIn.Split(',')[0]);
18:
19: //创建子窗体并显示
20: BaseForm plugInForm = plugIn.CreatePlugInForm();
21: plugInForm.MdiParent = this;
22: plugInForm.Show();
23: }
上述几个步骤完成后,宿主程序就基本完成了,现在还需要一个插件。
步骤五:实现一个插件
插件项目wZhang.MyPlugIn我们已经建立,这是我们在项目中分别添加类MyPlugIn.cs和窗体MyPlugInForm.cs,其中MyPlugIn实现接口IPlugIn,窗口继承自BaseForm.我们在MyPlugIn的CreatePlugInForm方法中创建一个MyPlugInForm的实例,便完成了一个最基本的插件(当然实际业务中插件窗体中还有很多事情要处理)。代码如下(示例代码中奖应用程序上下文通过构造函数传给了插件窗体):
1: /// <summary>
2: /// 插件
3: /// </summary>
4: public class MyPlugIn:IPlugIn
5: {
6: private IAppContext _app;
7:
8: public IAppContext AppContext
9: {
10: get { return _app; }
11: set { _app = value; }
12: }
13:
14: public BaseForm CreatePlugInForm()
15: {
16: return new MyPlugInForm(AppContext);
17: }
18: }
经过以上五个步骤,我们便完成了一个利用插件方式实现的Winform动态加载模块的小示例。最终的代码结构如下:
运行示例程序,点击某项菜单后的显示效果为:
示例代码:PlguIn.7z