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


    前言

    最近事情较多,终于有时间来写完这篇。在上一篇的基础上,本篇文章我们开始着手搭建一个简单的基于插件架构的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

  • 相关阅读:
    阿里云服务器nginx配置https协议
    com.alibaba.fastjson.JSONObject cannot be cast to java.lang.Stringcom.alibaba.fastjson.JSONObject cannot be cast to java.lang.String
    Nginx日志报错open() "/opt/Nginx/nginx/nginx.pid" failed (2: No such file or directory)
    腾讯云COS使用前端js的api获取签名
    安装CURL 时报错GnuTLS: The TLS connection was nonproperly terminated. Unable to establish SSL connection.
    ABP项目构建
    is ok?
    JavaScript 字符串处理(截取字符串)的方法(slice()、substring()、substr() )
    医药消炎药蒲地蓝消炎片
    JavaScript 输出信息的方式
  • 原文地址:https://www.cnblogs.com/pszw/p/2513798.html
Copyright © 2020-2023  润新知