面临的问题
在开发插件系统中,我们通常会面临这样的问题:
- 一些功能并不是在开启时就要被使用的,例如VS中的大量功能对一个大部分程序员来说用不着,但框架本身却应该向用户提供该插件的相应信息?
- 在可视化的插件功能列表中,我们不仅希望提供简单的插件名称信息,更希望能以图片,或动画等形式展示其功能特性,便于用户选择。
- 插入辅助类来解决上一个问题? 想法虽好,但破坏了“插件”的精髓,它应该是独立可插拔的,如果存在其之外的辅助类,那真是得不偿失。
据我所知,.NET的MEF插件系统提供了完整的插件系统框架,但可定制化程度不高。 一些插件功能是不需要每次都调用的,如果实例化所有的插件会带来很大的资源开销,而且不方便管理。因此本文将通过一些技巧,实现本文标题的目标:不实例化获取插件信息和可视化方法。我的项目本身是基于WPF的,但基本不影响整个文章的通用性。
2. 改造attribute,提供类信息的方法
attribute 即可以应用于编程元素(如类型、字段、方法和属性 (Property))的描述性声明。属性与 .NET Framework 文件的元数据一起保存,并且可用于向公共语言运行时描述代码或影响应用程序的运行时行为。
通过继承atrribute,扩展我们想要获得的使用方法,我们可以通过自定义两类attribute:
一类是针对插件接口名称的定义,用于定义插件接口的名称,搜索方法等
/// <summary>
/// 执行搜索策略
/// </summary>
public enum SearchStrategy
{
/// <summary>
/// 目录内搜索
/// </summary>
FolderSearch,
/// <summary>
/// 目录内递归搜索
/// </summary>
RecursiveFolderSearch,
}
/// <summary>
/// 该类定义了插件系统的接口契约记录
/// </summary>
public class InterfaceAttribute : Attribute
{
/// <summary>
/// 该插件接口的名称
/// </summary>
public string myName { get; set; }
/// <summary>
/// 搜索策略
/// </summary>
public SearchStrategy mySearchStrategy { get; set; }
/// <summary>
/// 相关信息
/// </summary>
public string DetailInfo { get; set; }
public InterfaceAttribute(string thisName, string thisDetailInfo, SearchStrategy thisSearchStrategy)
// 定位参数
{
this.myName = thisName;
this.DetailInfo = thisDetailInfo;
this.mySearchStrategy = thisSearchStrategy;
}
}
另一类是定义实现接口的插件的规约,定义插件的名称,信息和接口约束
/// <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的使用方法
类attribute使用实例
[XFrmWorkAttribute("Unity3D控制器", "IProgramWPF", "针对Unity3D的游戏编程接口", "/XFrmWork.XMove.Program;component/Images/Unity3D控制器.jpg")]
public partial class Unity3DController : UserControl,IProgramWPF,IProgramUI
{
//类方法代码
}
其四个构造函数的参数分别是:名称,类型(一般是实现的接口),类说明,资源名称。
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) 封装了的反射实例化方法。 类型参数是接口名称和在插件列表中的位置,为了简化, 类的构造函数必须是没有形参的。
下面的代码做了详细的说明:
插件搜索器
/// <summary>
/// 该类定义了插件系统通过attribute记录的类基本信息
/// </summary>
public class ObjectBasicInfo
{
public string myLogoURL { get; set; }
public string myName { get; set; }
public Type myType { get; set; }
public string myDetail { get; set; }
}
/// <summary>
/// 单例模式提供的插件搜索器
/// </summary>
public class SingletonProvider
{
SingletonProvider()
{
}
static Dictionary<string, ObservableCollection<ObjectBasicInfo>> myObjectBasicInfoDictionary = new Dictionary<string, ObservableCollection<ObjectBasicInfo>>();
public static Object GetObjectInstance(string interfaceName, int index)
{
return Activator.CreateInstance(GetInstance(interfaceName)[index].myType);
}
public static ObservableCollection<ObjectBasicInfo> GetInstance(string interfaceName)
{
if (myObjectBasicInfoDictionary.ContainsKey(interfaceName)) //如果字典中存在该方法,则直接返回结果
return myObjectBasicInfoDictionary[interfaceName];
else
{
ObservableCollection<ObjectBasicInfo> tc = new ObservableCollection<ObjectBasicInfo>(); //执行查找方法
Assembly assembly = Assembly.GetCallingAssembly();
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
if (type.GetInterface(interfaceName) != null && !type.IsAbstract)
{
// Iterate through all the Attributes for each method.
foreach (Attribute attr in
type.GetCustomAttributes(typeof(XFrmWorkAttribute), false))
{
XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute;
tc.Add(new ObjectBasicInfo() { myLogoURL = attr2.myResource, myType = type, myDetail = attr2.DetailInfo, myName = attr2.Name });
}
}
}
myObjectBasicInfoDictionary.Add(interfaceName, tc);
return tc;
}
}
}
使用时非常方便,下面我们会介绍如何使用他。
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;
}
//其他成员变量。。。。
[XFrmWorkAttribute("标准蓝牙通信", "ICommMethod", "提供支持绝大多数蓝牙设备的socket蓝牙通信","/XFrmWork.XMove.Comm;component/Images/标准蓝牙.jpg")]
public class BluetoothAdvanced : AbstartComm
{
public BluetoothAdvanced():base
()
{
}
}
其他方法不一一列举。
我们在一个插件管理类中直接这样调用:
ComMethodList.DataContext = SingletonProvider.GetInstance("ICommMethod");
ComMethodList是我的系统中一个可用通信方法列表的WPF的Listbox控件。 直接将 ObservableCollection<ObjectBasicInfo>> 集合绑定到该控件的数据上下文中,即可显示当前所有的通信列表。 Listbox中的对应index即插件集合的index,通过上面描述的实例化方法,就能反射实例化之。 讨论WPF的数据绑定超过了本文的范畴,但可查询相关资料。读者可以自行设计和使用该集合提供的数据。显示效果如下图:
5. 在已实例化的对象中获取该类的attribute数据
还有一个遗留问题,即在实例化的对象中,我们依旧要获得类的名称,描述和其他相关信息,如何做呢? 我们定义如下的attribute方法实现之
AttributeHelper
public class AttributeHelper
{
public static XFrmWorkAttribute GetCustomAttribute(Type source)
{
object[] attributes = source.GetCustomAttributes(typeof(XFrmWorkAttribute), false);
foreach (object attribute in attributes)
{
if (attribute is XFrmWorkAttribute)
return (XFrmWorkAttribute)attribute;
}
return new XFrmWorkAttribute("不存在定义","NULL","NULL","无定义资源");
}
}
若该类未实现自定义的attribute,返回一个“空”值,提醒设计者或开发人员。
使用起来也很简单,例如我们想获得该对象的“名称”:
public string PublicName
{
get { return AttributeHelper.GetCustomAttribute(this.GetType()).Name; }
}
注意,属性访问器中,显然只应该实现get方法,它是运行时的不可修改对象。
6. 其他
本文的来源是作者项目中的实际需要,不实例化类而获得类的相关信息是一种很普遍的需求,attribute是一种做法,也可以通过xml实现,很多插件系统就是这么做的,但对于轻量级的系统来说,attribute可能更合适,而单例模式的引用给该方法带来了很大的方便。 有任何问题欢迎随时留言交流。