前言
前一篇我們說到了如何利用應用程序域的相關技術實現熱升級的目的。下面我來介紹另一種場景,如下圖所示:
主程序僅提供作為MdiContainer的窗體框架,所有的功能都以單獨的子窗體形式加載。每個子窗體對應的是一個單獨的功能模塊(dll文件)。
比如管理公司結構的時候,員工管理模塊和部門管理模塊就分別以單獨的dll文件的形式加載到主窗體中,我們今天要做的就是對這樣一個單獨的子窗體功能模塊進行升級。
評估方式一
我們先評估一下利用上一篇所說創建新應用程序域的方式能否實現。
首先,主程序Mdi窗體在默認應用程序域中啟動,然后要顯示某個功能界面的時候,通過創建應用程序域加載對應的功能模塊。OK,這一步能夠實現,如下效果:
不過,還沒有達到我們想要的效果,我們想實現的是功能模塊1這個窗體是作為應用程序框架這個窗體的子窗體顯示的。還要繼續做。
要達到上述效果,只要設置功能模塊1的MdiParent屬性為主程序窗體就行了。
不過問題就在這了,主窗體和子窗體分別屬於不同的應用程序域!上一篇我們介紹過,不同應用程序域中的成員是密封的,任何程序域中都不能直接訪問其他程序域中的成員!只能使用按引用封送或按值封送的技術,才能間接地訪問。顯然窗體對象不是值類型,如果要按引用封送的話,不管是把主窗體封送進子窗體所在程序域,還是把子窗體封送進入主窗體所在程序域,都必須保證一點,窗體類型必須繼承自MarshalByRefObject,查看Form類的繼承關系:
public class Form : ContainerControl
public class ContainerControl : ScrollableControl, IContainerControl
public class ScrollableControl : Control, IComponent, IDisposable
public class Control : Component, IDropTarget, ISynchronizeInvoke, IWin32Window, IBindableComponent, IComponent, IDisposable
public class Component : MarshalByRefObject, IComponent, IDisposable
最終“驚喜地”發現,Form是繼承自MarshalByRefObject,那么就可以使用按引用封送的技術了?
可是,當我使用MemberwiseClone方法將主窗體對象封成MarshalByRefObject發送到子窗體所在程序域,並設置子窗體的MdiParent屬性時,
運行卻產生如下異常:
看來即使封送Form類,也只能傳送諸如Name, Text之類的簡單屬性,像ControlCollection這樣的並沒有做可序列化實現。
另辟蹊徑
既然使用新應用程序域無法實現,那就只能看在同一個應用程序域中能不能有辦法實現。
在上一篇中也說過,無法在程序運行期間更新文件的根本原因,就是運行中的程序“霸占”着文件的句柄,直到卸載程序域或者退出進程的時候才被釋放。那么只要在加載完模塊后,同時使用某種方式釋放句柄應該就可以了!
如果不需要使用AppDomain,那么一般我們加載程序集使用Assembly的相關靜態方法,調用Assembly方法生成的對象就處在調用所在的域中,這樣子窗體和主窗體對象處於同一個程序域中,也就很方便地設置子窗體的MdiParent屬性了。
關於加載程序集的相關主要方法(略去重載方法)如下:
public static Assembly Load(AssemblyName assemblyRef); public static Assembly Load(byte[] rawAssembly); public static Assembly Load(string assemblyString); public static Assembly LoadFile(string path); public static Assembly LoadFrom(string assemblyFile);
觀察這些方法發現,不管是assemblyRef,還是assemblyString, path, assemblyFile這些都跟文件名有關,通過調用測試發現,這些方法加載完文件后並沒有釋放文件句柄。
唯獨方法
public static Assembly Load(byte[] rawAssembly);
加載的是一組字節流!調用這個方法,需要先把文件讀入內存字節流,然后再從這個字節流加載,已經跟硬盤上的文件沒有關系了,也就是說當文件被讀入內存字節流中后,句柄會被釋放,這個不就是我們希望的么!
具體實現
OK,既然找到了這樣一個方法,那么我們建一個解決方案來驗證一下。
如下所示:
其中,Modules.xml作為配置文件,用來描述主程序需要加載的功能模塊信息
<?xml version="1.0" encoding="utf-8" ?> <Modules> <Module name="Module1" file="Modules\\Module1.dll" interface_name="Module1.Loader" /> </Modules>
功能模塊中有個簡單的窗體
點擊顯示后,會彈出該模塊的版本號,后面我們以這個消息判斷是否升級成功。
主程序啟動后,首先讀取Modules.xml文件,在工具欄中生成對應的按鈕,表示已發現對應功能模塊
當點擊按鈕時加載並顯示子窗體:
void ctl_Click(object sender, EventArgs e) { string name = ((ToolStripButton)sender).Text; Form frm = ModuleManager.LoadModule(name); frm.MdiParent = this; frm.Show(); }
public static Form LoadModule(string moduleName) { if (!_modules.ContainsKey(moduleName)) return null; ModuleInfo mInfo = _modules[moduleName]; if (mInfo.Frm_Module != null && !mInfo.Frm_Module.IsDisposed) return mInfo.Frm_Module;
byte[] bFile = File.ReadAllBytes(mInfo.File); Assembly amy = Assembly.Load(bFile); ILoader loader = (ILoader)amy.CreateInstance(mInfo.Interface_Name); Form frm = loader.LoadModule(); mInfo.Frm_Module = frm; return frm; }
通過File.ReadAllBytes方法將dll文件讀入字節流,這時候文件句柄就已經被釋放了,也就可以在運行中進行升級操作。
其中,ModuleInfo保存Modules.xml中讀取的模塊信息。
class ModuleInfo { string name; /// <summary> /// 模塊唯一標識,也作為應用程序域的名稱 /// </summary> public string Name { get { return name; } } string file; /// <summary> /// 模塊對應的文件名 /// </summary> public string File { get { return file; } set { file = value; } } string interface_name; /// <summary> /// 用於程序框架加載模塊的入口 /// </summary> public string Interface_Name { get { return interface_name; } set { interface_name = value; } } /// <summary> /// 功能模塊窗體 /// </summary> public Form Frm_Module; public ModuleInfo(string name, string file, string interface_name) { this.name = name; this.file = file; this.interface_name = interface_name; } }
邏輯很簡單,我們直接運行看一下
如何判斷這時候磁盤上的文件和正在運行的子窗體沒有關系呢,為了便於演示,我在功能模塊1的窗體上再加一個按鈕用於刪除相應的dll文件。
再次運行,點擊刪除
刪除成功了!說明沒有問題,句柄已經被釋放,這時候我們重新生成一下Module1,把版本修改為1.0.0.1
[assembly: AssemblyVersion("1.0.0.1")] [assembly: AssemblyFileVersion("1.0.0.1")]
關閉子窗體,(只是關閉子窗體,並不關閉主窗體),再次點擊Module1按鈕
加載成功,並且版本已經更改為最新的1.0.0.1!
至此,整個程序升級的工作完成。
完整的解決方案 點擊下載
注:我在blog中使用的代碼都是為了演示使用的精簡的代碼,並不適合拿來直接使用,只是希望大家能理解解決的方法,再以自己理解的方式實現。