基於插件架構的簡單的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM