代码改变世界

基于插件架构的简单的Winform框架(下)

2012-07-11 22:49  ps_zw  阅读(3420)  评论(2编辑  收藏  举报

前言

最近事情较多,终于有时间来写完这篇。在上一篇的基础上,本篇文章我们开始着手搭建一个简单的基于插件架构的Winform框架。(其实也就是一个小例子,也是对之前写过的代码的总结)

 

设计思路

写这个Winform小例子的想法来源主要是:

1.希望Winform程序能够根据配置动态生成菜单;

2.运行时按需加载指定模块,必要时还能在运行时更新指定模块;

3.新的功能模块能够即插即用,可扩展;

4.宿主、插件间的基础信息可共享,可交互。

实现

如上文所述,我们想要使用插件结构,那么解决方案的项目结构可如下图所示:

image

解决方案分为三个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动态加载模块的小示例。最终的代码结构如下:

image

运行示例程序,点击某项菜单后的显示效果为:

image

示例代码:PlguIn.7z