天天看點

C#.Net 如何動态加載與解除安裝程式集(.dll或者.exe)3---- 動态加載Assembly應用程式

下載下傳 ​​supergraphfiles.exe​​ 示例檔案。 

應用程式體系結構 

在我專攻代碼之前,我想談談我嘗試做的事。您可能記得,SuperGraph 讓您從函數清單中進行選擇。我希望能夠在具體的目錄中放置外接程式程式集,讓 SuperGraph 檢測它們,加載它們,并找到它們中包含的所有函數。 

如果 SuperGraph 自己能完成此操作則不需要單獨的 AppDomain。Assembly.Load() 通常運作良好,但程式集無法獨立解除安裝(隻有 AppDomain 可以解除安裝)。這意味着如果您正在編寫伺服器,而且您希望使用者無需啟動和停止伺服器即能更新他們的外接程式,那麼您将無法使用預設的 AppDomain 實作此任務。 

要實作此功能,我們将在一個獨立的 AppDomain 中加載所有外接程式程式集。當添加或修改檔案時,我們将解除安裝 AppDomain,建立新的 AppDomain,然後将目前檔案加載到其中。這樣,一切就都完美無缺了。 

建立 AppDomain 

第一項任務是建立 AppDomain。要以正确的方式建立 AppDomain,我們需要向 AppDomain 傳遞一個 AppDomainSetup 對象。一旦您了解了這一切的工作原理,關于這些的文檔就足夠使用了,但是如果您正在試圖了解其工作原理,那麼這些文檔的幫助并不大。當關于該主題的 Google 搜尋将上個月的專欄作為較高的比對之一傳回時,我懷疑我可能有點麻煩了。 

必須處理的基本問題是如何在運作時加載程式集。預設情況下,運作時将檢視全局程式集緩存或目前應用程式目錄樹。而我們希望從完全不同的目錄中加載我們的外接程式。 

當您檢視 AppDomainSetup 的文檔時,您将發現可以把 ApplicationBase 屬性設定為要搜尋程式集的目錄。然而,我們也需要參考原始的程式目錄,因為那是 RemoteLoader 類存在的地方。 

AppDomain 的創作者們了解這一點,是以他們已經提供了額外的位置,用于從中搜尋程式集。我們将使用 ApplicationBase 引用外接程式目錄,然後将 PrivateBinPath 設定為指向主應用程式目錄。 

下面是來自 Loader 類的代碼,可實作此功能: 

AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = functionDirectory;
setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory;
setup.ApplicationName = "Graph";
appDomain = AppDomain.CreateDomain("Functions", null, setup);
 
remoteLoader = (RemoteLoader) 
    appDomain.CreateInstanceFromAndUnwrap("SuperGraph.exe",
        "SuperGraphInterface.RemoteLoader");      

建立 AppDomain 之後,使用 CreateInstanceFromAndUnwrap() 函數在新的應用程式域中建立 RemoteLoader 類的執行個體。請注意,需要使用類所在的程式集的檔案名以及類的全名。 

當執行此調用時,我們傳回如同 RemoteLoader 一樣的執行個體。實際上,它是一個小型代理類,将所有調用轉發到其他 AppDomain 中的 RemoteLoader 執行個體中。這和 .NET Remoting 使用的是同一種結構。 

程式集綁定日志檢視器 

當您編寫代碼實作此功能時,您會産生錯誤。本文檔對如何調試應用程式并未提供什麼建議,但是如果您知道該向誰詢問,他們将告訴您有關程式集綁定日志檢視器(名為“fuslogvw.exe”,因為加載子系統稱為“fusion”)的資訊。運作檢視器時,您可以要求它記錄故障,然後當您運作的應用程式出現加載程式集的問題時,您可以重新整理檢視器,獲得目前情況的詳細資訊。 

例如,您會發現 Assembly.Load() 的檔案名末尾不需要 .dll,這一點非常有用。您可以從日志中獲知這一點,因為它将告訴您它曾試圖加載 f.dll.dll。 

動态加載程式集 

是以,既然我們已經建立了應用程式域,下一步應該搞清楚如何加載元件并從中提取函數。這需要兩段互相獨立的代碼。第一段代碼在目錄中查找檔案,然後加載找到的每個檔案: 

void LoadUserAssemblies()
{
    availableFunctions = new FunctionList();
    LoadBuiltInFunctions();
 
    DirectoryInfo d = new DirectoryInfo(functionAssemblyDirectory);
    foreach (FileInfo file in d.GetFiles("*.dll"))
    {  
        string filename = file.Name.Replace(file.Extension, "");
        FunctionList functionList = loader.LoadAssembly(filename);
 
        availableFunctions.Merge(functionList);
    }
}      

Graph 類中的函數在外接程式目錄中查找所有的 dll 檔案,删除它們的擴充名,然後告訴加載程式加載它們。傳回的函數清單将并入目前的函數清單。 

第二段代碼在 RemoteLoader 類中,它實際加載程式集并查找函數: 

public FunctionList LoadAssembly(string filename)
{
    FunctionList functionList = new FunctionList();
    Assembly assembly = AppDomain.CurrentDomain.Load(filename);
 
    foreach (Type t in assembly.GetTypes())
    {
        functionList.AddAllFromType(t);
    }   
    return functionList;
}      

這段代碼隻是對傳入的檔案名(實際是程式集名稱)調用 Assembly.Load(),然後将所有有用的函數加載到 FunctionList 執行個體中傳回給調用程式。 

此時,應用程式可以啟動,加載外接程式程式集,然後使用者就可以引用它們。 

重新加載程式集 

下一項任務是能夠按照需要重新加載這些程式集。最終,我們希望能夠自動實作該任務,但是出于測試目的,我将 Reload 按鈕添加到窗體中,以使程式集能夠重新加載。該按鈕的處理程式僅調用 Graph.Reload(),它需要執行以下操作: 

1.       解除安裝 AppDomain。

2.       建立新的 AppDomain。

3.       在新的 AppDomain 中重新加載程式集。

4.       将圖形線條挂鈎到新建立的 AppDomain。

步驟 4 是必需的,因為 GraphLine 對象包含來自原 AppDomain 的 Function 對象。解除安裝 AppDomain 後,函數對象無法再被使用。 

為解決此問題,HookupFunctions() 修改了 GraphLine 對象,使它們從目前應用程式域指向正确的函數。 

代碼如下: 

loader.Unload();
loader = new Loader(functionAssemblyDirectory);
LoadUserAssemblies();
HookupFunctions();
reloadCount++;
 
if (this.ReloadCountChanged != null)
    ReloadCountChanged(this, new ReloadEventArgs(reloadCount));      

隻要執行重新加載操作,最後兩行将引發一個事件。其作用是更新窗體上的重新加載計數器。 

檢測新的程式集 

下一步是能夠檢測在外接程式目錄中顯示的新的或修改過的程式集。該架構提供 FileSystemWatcher 類來實作此功能。下面是我添加到 Graph 類構造函數中的代碼:

watcher = new FileSystemWatcher(functionAssemblyDirectory, "*.dll");
watcher.EnableRaisingEvents = true;
watcher.Changed += new FileSystemEventHandler(FunctionFileChanged);
watcher.Created += new FileSystemEventHandler(FunctionFileChanged);
watcher.Deleted += new FileSystemEventHandler(FunctionFileChanged);      

當建立 FileSystemWatcher 類時,我們告訴它要在什麼目錄中查找,要跟蹤哪些檔案。EnableRaisingEvents 屬性表示當它檢測到更改時,我們是否需要它發送事件。最後 3 行将事件挂鈎到類中的某個函數。該函數僅僅調用 Reload() 以重新加載程式集。 

這種方法有一些累贅的地方。在更新程式集時,我們必須解除安裝程式集才能夠加載新的版本,但是添加或删除檔案時不需要解除安裝程式集。在這種情況下,對所有更改執行此操作的成本并不是很高,而且它使代碼更簡單。 

在構造此代碼之後,我們運作該應用程式,然後嘗試把新的程式集複制到外接程式目錄中。正如我們所希望的那樣,我們獲得了檔案更改事件,當重新加載完畢時,新的函數就可供使用。 

然而,當我們試圖更新現有的程式集時,我們遇到了一個問題。運作時已經鎖定該檔案,這意味着我們無法将新的程式集複制到外接程式目錄中,我們收到一個錯誤。 

AppDomain 類的設計人員意識到這是一個問題,是以他們提供一種不錯的解決方法。當 ShadowCopyFiles 屬性設定為 true(字元串 true,不是布爾常數 true。不要問我為什麼……)時,運作時将把程式集複制到緩存目錄中,然後打開該程式集。這樣,原檔案就不會被鎖定,我們也就能更新正在使用的程式集。ASP.NET 使用了這種機制。 

為了啟用此功能,我在 Loader 類的構造函數中添加了以下行: 

setup.ShadowCopyFiles = "true"; 

然後我重新生成了該應用程式,并得到相同的錯誤。我檢視了 ShadowCopyDirectories 屬性的文檔,該文檔明确指出 PrivateBinPath 指定的所有目錄(包括 ApplicationBase 指定的目錄)是陰影複制的(如果未設定此屬性)。記得我是如何說該文檔在這個方面不是很好的嗎? 

有關此屬性的文檔肯定是錯了。我沒有驗證确切的表現方式,但是我可以告訴您 ApplicationBase 目錄中的檔案在預設情況下并不是陰影複制的。明确指定目錄可以解決此問題: 

setup.ShadowCopyDirectories = functionDirectory; 

搞明白這一點至少花了我半個小時。 

現在我們可以更新現有檔案并将其正确地加載進去。可我剛把這個理順,又遇到了另外一個小的問題。當我們從窗體的按鈕上運作重新加載函數時,重新加載總是和繪制發生在同一個線程中,這意味着在重新加載過程中我們不可能嘗試繪制直線。 

既然我們已經切換到檔案更改事件,那麼在解除安裝 AppDomain 之後和加載新的 AppDomain 之前,有可能會進行繪制。如果發生這種情況,我們會得到一個異常。 

這是傳統的多線程程式設計問題,使用 C# lock 語句很容易處理。我在繪圖函數和重新加載函數中添加了 lock 語句,這就確定了它們不會同時發生。這就解決了該問題,添加程式集的更新版本将使程式自動切換到函數的新版本。這相當不錯。 

還有一個奇怪的現象。原來用于檢測檔案更改的 Win32® 函數發送的更改數量很大,是以對檔案做一次更新将發送五個更改事件,程式集也将被重新加載五次。解決方法是編寫更智能的、可以将這些操作組合在一起的 FileSystemWatcher,但是此版本中沒有提供這種解決方法。 

拖放 

将檔案複制到目錄中不是很友善,是以我決定在該應用程式中添加拖放功能。實作該任務的第一步是把窗體的 AllowDrop 屬性設定為 true,這将打開拖放功能。下一步,我将一個例程挂鈎到 DragEnter 事件。當光标在對象上移動進行拖放操作以确定目前對象是否接受拖放時,将調用該事件。 

private void Form1_DragEnter(
    object sender, System.Windows.Forms.DragEventArgs e)
{
    object o = e.Data.GetData(DataFormats.FileDrop);
    if (o != null)
    {
        e.Effect = DragDropEffects.Copy;
    }
    string[] formats = e.Data.GetFormats();
}      

在此處理程式中,我檢視是否有可用的 FileDrop 資料(也就是說,檔案被拖放到視窗中)。如果有,我把效果設定為“複制”,這将相應地設定光标,并且如果使用者釋放滑鼠按鈕,将發送 DragDrop 事件。該函數中的最後一行完全是出于調試目的,用于檢視操作中有哪些可用資訊。 

下一項任務是為 DragDrop 事件編寫處理程式:

private void Form1_DragDrop(
    object sender, System.Windows.Forms.DragEventArgs e)
{
    string[] filenames = (string[]) e.Data.GetData(DataFormats.FileDrop);
    graph.CopyFiles(filenames);
}      

此例程獲得與此操作關聯的資料(檔案名數組),将其傳遞到圖形函數,然後圖形函數把檔案複制到外接程式目錄中,觸發檔案更改事件以便重新加載它們。 

狀态

此時,您可以運作該應用程式,把新的程式集拖到程式上,程式将很快加載它們并保持運作。這相當不錯。