應用程式體系結構
在我專攻代碼之前,我想談談我嘗試做的事。您可能記得,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(
string[] filenames = (string[]) e.Data.GetData(DataFormats.FileDrop);
graph.CopyFiles(filenames);
}