這次的內容主要介紹 C# 應用程式如何透過 dll 及反射 (Reflection) 來做 plugin,方法沒有很困難,不過一個 plugin 就是一個 dll 檔,也就等於是一個專案,所以在範例中會開蠻多專案的,手續會稍微繁複一點。現在就開始撰寫我們的範例程式吧!
建立 dll 插件
首先要開第一個 dll 專案做為 plugin 的介面 (interface)。雖然我覺得會想看這篇的人應該都已經知道怎麼開 dll 專案了,不過我還是簡單介紹一下,開 dll 專案沒有很困難,在新增專案的時候,專案類型選擇 ClassLibrary,編譯出來的程式就會是 dll 了。
我的專案名稱是取名為 IPlugin,然後這個介面很單純,程式碼如下
namespace IPlugin
{
public interface IPlugin
{
string Name { get; }
}
}
只有取得名稱的功能,夠簡單了吧!接下來開二個 dll 專案,也就是實際實作 IPlugin 介面的類別,我的專案名稱取名叫做 Foo。當然,因為要實作介面,所以要記得把 IPlugin.dll 加到專案的 Reference 裡面
Foo.cs 裡面的內容一樣超級簡單,只是實作 Name 的 Property 而已~
namespace Foo
{
public class Foo : IPlugin.IPlugin
{
public virtual string Name { get { return "Foo"; } }
}
}
為了讓我們的範例效果更明顯一點,我還創了第三個 dll 專案,專案名稱叫做 Bar ,至於程式內容和創建專案的方法我想各位也已經知道了,和 Foo 一樣,只是把 Foo 全部改成 Bar 而已,你也可以創幾個你自己喜歡的來測試。
建立主程式
把介面和實體的 Plugin 類別都準備好以後,就要來寫我們想要載入 plugin 的主程式啦。我這邊是開了一個 Console 專案作為範例,記得要 Reference IPlugin.dll ,不然會認不得 IPlugin 型別,而 Foo.dll 和 Bar.dll 就不必了,因為我們的插件是要在執行期動態載入的!
主程式的程式碼內容如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Reflection;
namespace Console
{
class Program
{
static void Main(string[] args)
{
// 1. 取得 dll 檔案名稱
string[] dllFileNames = null;
dllFileNames = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll");
// 2. 取得 Assembly
ICollection assemblies = new List(dllFileNames.Length);
foreach (string dllFile in dllFileNames)
{
AssemblyName an = AssemblyName.GetAssemblyName(dllFile);
Assembly assembly = Assembly.Load(an);
assemblies.Add(assembly);
}
// 3. 取得插件型別
Type pluginType = typeof(IPlugin.IPlugin);
ICollection pluginTypes = new List();
foreach (Assembly assembly in assemblies)
{
if (assembly == null)
continue;
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (type.IsInterface || type.IsAbstract)
continue;
else if (type.GetInterface(pluginType.FullName) != null)
pluginTypes.Add(type);
}
}
// 4. 產生物件並實際操作物件
foreach (Type type in pluginTypes)
{
IPlugin.IPlugin plugin = (IPlugin.IPlugin)Activator.CreateInstance(type);
System.Console.WriteLine(plugin.Name);
}
}
}
}
稍微長一點了,我做個重點說明,每一個段落分別對應到程式碼的註解數字。
-
搜尋插件:這裡呼叫了 GetFiles 來取得指定路徑下所有附檔名為 dll 的檔案名稱,而我的路徑指定為 AppDomain.CurrentDomain.BaseDirectory,也就是執行檔的所在目錄,所以之後 dll 插件記得要放在案執行檔相同目錄下才有效。
-
取得 Assembly ,你可以把它想像成"開檔":流程很簡單,我們已經知道所有 dll 的檔案位置了,只要透過 Assembly.GetAssemblyName 及 Assembly.Load 就可以把 dll 的內容給讀取進來,並把它並放在 List 裡面。
-
我們已經快要接近目標了,第三步就是去搜尋所有 Assembly 內的類別中是實作 IPlugin 介面的。這裡的小技巧是使用 GetInterface 這一個函數來判斷我們搜尋到的類別是不是實作 IPlugin ,是個話他會 return 一個 Type,不是的話他會 return null,函數說明可以參考 MSDN。
-
到這裡我們已經取得所有插件的類別啦,最後一步是透過 Activator.CreateInstance 這個函數來實際產生物件,有了物件我們就可以實際執行插件的內容囉!這個範例就是把 plugin 裡面的 Name 給印出來 WriteLine(plugin.Name),實際輸出大概會長這樣
如果你的程式沒有印出Foo, Bar 或者是你插件定義的內容,確認一下是否已經把插件的 dll 放到程式的執行目錄下,也可以試著把插件的 dll 移出執行目錄比較結果,不必重新編譯主程式就會動態載入囉!