前言
最近事情較多,終於有時間來寫完這篇。在上一篇的基礎上,本篇文章我們開始着手搭建一個簡單的基於插件架構的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