-
面臨的問題
在開發插件系統中,我們通常會面臨這樣的問題:
- 一些功能並不是在開啟時就要被使用的,例如VS中的大量功能對一個大部分程序員來說用不着,但框架本身卻應該向用戶提供該插件的相應信息?
- 在可視化的插件功能列表中,我們不僅希望提供簡單的插件名稱信息,更希望能以圖片,或動畫等形式展示其功能特性,便於用戶選擇。
- 插入輔助類來解決上一個問題? 想法雖好,但破壞了“插件”的精髓,它應該是獨立可插拔的,如果存在其之外的輔助類,那真是得不償失。
據我所知,.NET的MEF插件系統提供了完整的插件系統框架,但可定制化程度不高。 一些插件功能是不需要每次都調用的,如果實例化所有的插件會帶來很大的資源開銷,而且不方便管理。因此本文將通過一些技巧,實現本文標題的目標:不實例化獲取插件信息和可視化方法。我的項目本身是基於WPF的,但基本不影響整個文章的通用性。
如果想了解更多關於作者對本問題早期的思考,請看 我上一篇關於.NET插件系統的文章。
2. 改造attribute,提供類信息的方法
attribute 即可以應用於編程元素(如類型、字段、方法和屬性 (Property))的描述性聲明。屬性與 .NET Framework 文件的元數據一起保存,並且可用於向公共語言運行時描述代碼或影響應用程序的運行時行為。
通過繼承atrribute,擴展我們想要獲得的使用方法,我們可以通過自定義兩類attribute:
一類是針對插件接口名稱的定義,用於定義插件接口的名稱,搜索方法等
1 /// <summary> 2 /// 執行搜索策略 3 /// </summary> 4 public enum SearchStrategy 5 { 6 7 /// <summary> 8 /// 目錄內搜索 9 /// </summary> 10 FolderSearch, 11 /// <summary> 12 /// 目錄內遞歸搜索 13 /// </summary> 14 RecursiveFolderSearch, 15 } 16 /// <summary> 17 /// 該類定義了插件系統的接口契約記錄 18 /// </summary> 19 20 public class InterfaceAttribute : Attribute 21 { 22 /// <summary> 23 /// 該插件接口的名稱 24 /// </summary> 25 public string myName { get; set; } 26 27 /// <summary> 28 /// 搜索策略 29 /// </summary> 30 public SearchStrategy mySearchStrategy { get; set; } 31 32 33 /// <summary> 34 /// 相關信息 35 /// </summary> 36 public string DetailInfo { get; set; } 37 38 39 public InterfaceAttribute(string thisName, string thisDetailInfo, SearchStrategy thisSearchStrategy) 40 // 定位參數 41 { 42 43 this.myName = thisName; 44 this.DetailInfo = thisDetailInfo; 45 this.mySearchStrategy = thisSearchStrategy; 46 47 48 } 49 }
另一類是定義實現接口的插件的規約,定義插件的名稱,信息和接口約束
/// <summary>
/// 自定義的Attribute,可在框架中提供程序集名稱等信息
/// </summary>
public class XFrmWorkAttribute : Attribute // 必需以System.Attribute類為基類
{
public string DetailInfo //提供該類的詳細文字性說明
{
get;
set;
}
private string mainKind;
public string MainKind //該類類別
{
get { return mainKind; }
set { mainKind = value; }
}
private string name;
public string Name //提供類名稱
{
get { return name; }
set { name = value; }
}
private string myresource;
public string myResource //提供資源名稱
{
get { return myresource; }
set { myresource = value; }
}
// 值為null的string是危險的,所以必需在構造函數中賦值
public XFrmWorkAttribute(string thisName, string thisKind, string thisDetailType)
// 定位參數
{
this.MainKind = thisKind;
this.Name = thisName;
this.DetailInfo = thisDetailType;
}
// 值為null的string是危險的,所以必需在構造函數中賦值
public XFrmWorkAttribute(string thisName, string thisKind, string thisDetailType, string thisResource)
// 定位參數
{
this.MainKind = thisKind;
this.Name = thisName;
this.DetailInfo = thisDetailType;
this.myresource = thisResource;
}
}
自定義可由項目需求進行,具體請參考相關文檔,此處並不打算具體說明。本定義中,MainKind字段用於存儲該插件類型,這在一些搜索方法中是必要的。 而myResource字段可保存當前類的資源URI。這在WPF程序中尤為有效,在程序集中可嵌入圖形,音樂甚至視頻資源,可提供更好的用戶體驗。 而DetailInfo字段可保存對該插件的一些文字性描述。
下面展示該attribute的使用方法

1 [XFrmWorkAttribute("Unity3D控制器", "IProgramWPF", "針對Unity3D的游戲編程接口", "/XFrmWork.XMove.Program;component/Images/Unity3D控制器.jpg")]
2 public partial class Unity3DController : UserControl,IProgramWPF,IProgramUI
3 {
4 //類方法代碼
5 }
其四個構造函數的參數分別是:名稱,類型(一般是實現的接口),類說明,資源名稱。
3. 主程序框架動態查找可用插件的方法
主程序框架如何獲知當前插件的定制信息,並在需要的時候實例化呢? 我們將保存插件實現的atrribute標識,需要特別注意的是Type: 我們在此處存儲了該類的Type,使得能在需要的時候實例化之。
那么,如何執行插件搜索呢? 網上已經有大量的說明和介紹。 不外乎是搜索某一程序集,獲取所有類型Type,並查詢其是否實現了某類接口。 但當工程中有不止一種插件類型時,我們就該考慮實現代碼復用:
可以定義一個特殊的類SingletonProvider,考慮到該類功能較為固定,采用單例模式實現。 在內部,給出了一個靜態字典和兩個靜態方法:
- static Dictionary<string, ObservableCollection<ObjectBasicInfo>> myObjectBasicInfoDictionary: 插件字典:使用可通知集合ObservableCollection作為字典值,字典Key是接口名稱(再次聲明,此接口非一般意義上的接口,而是某種對插件分類的某種定義或約束,當然,可以使用C#的接口實現) 使用ObservableCollection僅僅是因為它在集合項目更改時可提供通知,方便WPF的數據綁定,如果不需要此項,你完全可以修改成你想要的集合類型如List
- public static ObservableCollection<ObjectBasicInfo> GetInstance(string interfaceName) : 查詢被某種接口約束的所有插件方法:
具體信息可以查看具體代碼, 當字典中已經存在該接口插件集合,則直接返回結果,否則執行查詢。需要注意:Assembly assembly = Assembly.GetCallingAssembly(); 可獲得當前被調用的程序集,這就保證了查找不同接口時是在保存當前插件程序集的dll上動態執行的,這點非常重要,就可不用提供程序集名稱。 另外,當找到某一滿足需求的Type時,就可以查找當前type所有的attribute,並獲取信息所有保存至字典中。 在下次調用同樣接口的插件列表時,就可以不用再次執行查找。
- public static Object GetObjectInstance(string interfaceName, int index) 封裝了的反射實例化方法。 類型參數是接口名稱和在插件列表中的位置,為了簡化, 類的構造函數必須是沒有形參的。
下面的代碼做了詳細的說明:

1 /// <summary>
2 /// 該類定義了插件系統通過attribute記錄的類基本信息
3 /// </summary>
4 public class ObjectBasicInfo
5 {
6 public string myLogoURL { get; set; }
7 public string myName { get; set; }
8 public Type myType { get; set; }
9 public string myDetail { get; set; }
10 }
11 /// <summary>
12 /// 單例模式提供的插件搜索器
13 /// </summary>
14 public class SingletonProvider
15 {
16 SingletonProvider()
17 {
18 }
19 static Dictionary<string, ObservableCollection<ObjectBasicInfo>> myObjectBasicInfoDictionary = new Dictionary<string, ObservableCollection<ObjectBasicInfo>>();
20
21 public static Object GetObjectInstance(string interfaceName, int index)
22 {
23 return Activator.CreateInstance(GetInstance(interfaceName)[index].myType);
24 }
25 public static ObservableCollection<ObjectBasicInfo> GetInstance(string interfaceName)
26 {
27 if (myObjectBasicInfoDictionary.ContainsKey(interfaceName)) //如果字典中存在該方法,則直接返回結果
28 return myObjectBasicInfoDictionary[interfaceName];
29 else
30 {
31
32 ObservableCollection<ObjectBasicInfo> tc = new ObservableCollection<ObjectBasicInfo>(); //執行查找方法
33
34 Assembly assembly = Assembly.GetCallingAssembly();
35 Type[] types = assembly.GetTypes();
36 foreach (Type type in types)
37 {
38 if (type.GetInterface(interfaceName) != null && !type.IsAbstract)
39 {
40
41
42 // Iterate through all the Attributes for each method.
43 foreach (Attribute attr in
44 type.GetCustomAttributes(typeof(XFrmWorkAttribute), false))
45 {
46 XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute;
47 tc.Add(new ObjectBasicInfo() { myLogoURL = attr2.myResource, myType = type, myDetail = attr2.DetailInfo, myName = attr2.Name });
48 }
49 }
50 }
51 myObjectBasicInfoDictionary.Add(interfaceName, tc);
52 return tc;
53 }
54 }
55
56
57 }
使用時非常方便,下面我們會介紹如何使用他。
4. 使用方法
下面我們以一個具體場景介紹這一方法的使用: 假設有一個通信系統,要求動態增減通信功能,如USB,串口,WIFI,藍牙等。 我們將其公共方法為接口ICommMethod ,所有通信方法都必須實現這一接口,並增加自定義的XFrmworkAttribute.
ICommMethod的接口規范標記如下:
/// <summary> /// 通信方法的基類接口 /// <remarks>繼承於INotifyPropertyChanged接口實現通知外部世界的能力</remarks> /// </summary> [InterfaceAttribute("ICommMethod", "", SearchStrategy.FolderSearch)] public interface ICommMethod : IProcess,INotifyPropertyChanged { /// <summary> /// 資源名稱 /// </summary> string ResourceName { get; } /// <summary> /// 當前端口序號 /// </summary> int CurrentPortIndex { get; set; } //其他成員變量。。。。
1 [XFrmWorkAttribute("標准藍牙通信", "ICommMethod", "提供支持絕大多數藍牙設備的socket藍牙通信","/XFrmWork.XMove.Comm;component/Images/標准藍牙.jpg")]
2 public class BluetoothAdvanced : AbstartComm
3 {
4 public BluetoothAdvanced():base
5 ()
6 {
7
8 }
9 }
其他方法不一一列舉。
我們在一個插件管理類中直接這樣調用:
ComMethodList.DataContext = SingletonProvider.GetInstance("ICommMethod");
ComMethodList是我的系統中一個可用通信方法列表的WPF的Listbox控件。 直接將 ObservableCollection<ObjectBasicInfo>> 集合綁定到該控件的數據上下文中,即可顯示當前所有的通信列表。 Listbox中的對應index即插件集合的index,通過上面描述的實例化方法,就能反射實例化之。 討論WPF的數據綁定超過了本文的范疇,但可查詢相關資料。讀者可以自行設計和使用該集合提供的數據。顯示效果如下圖:
5. 在已實例化的對象中獲取該類的attribute數據
還有一個遺留問題,即在實例化的對象中,我們依舊要獲得類的名稱,描述和其他相關信息,如何做呢? 我們定義如下的attribute方法實現之

1 public class AttributeHelper
2 {
3 public static XFrmWorkAttribute GetCustomAttribute(Type source)
4 {
5
6 object[] attributes = source.GetCustomAttributes(typeof(XFrmWorkAttribute), false);
7 foreach (object attribute in attributes)
8 {
9 if (attribute is XFrmWorkAttribute)
10 return (XFrmWorkAttribute)attribute;
11 }
12
13 return new XFrmWorkAttribute("不存在定義","NULL","NULL","無定義資源");
14 }
15 }
若該類未實現自定義的attribute,返回一個“空”值,提醒設計者或開發人員。
使用起來也很簡單,例如我們想獲得該對象的“名稱”:
public string PublicName
{
get { return AttributeHelper.GetCustomAttribute(this.GetType()).Name; }
}
注意,屬性訪問器中,顯然只應該實現get方法,它是運行時的不可修改對象。
6. 其他
本文的來源是作者項目中的實際需要,不實例化類而獲得類的相關信息是一種很普遍的需求,attribute是一種做法,也可以通過xml實現,很多插件系統就是這么做的,但對於輕量級的系統來說,attribute可能更合適,而單例模式的引用給該方法帶來了很大的方便。 有任何問題歡迎隨時留言交流。