.NET插件系统之二——不实例化获取插件信息和可视化方法

简介:
  1.  面临的问题

      在开发插件系统中,我们通常会面临这样的问题:

  •        一些功能并不是在开启时就要被使用的,例如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可能更合适,而单例模式的引用给该方法带来了很大的方便。 有任何问题欢迎随时留言交流。   


相关文章
|
7月前
|
容器
.NET Core - 选项框架:服务组件集成配置的最佳实践
.NET Core - 选项框架:服务组件集成配置的最佳实践
|
API
【.NET 6】使用.NET 6开发minimal api以及依赖注入的实现、VS2022热重载和自动反编译功能的演示
.net 6 LTS版本发布已经有若干天了。此处做一个关于使用.net 6 开发精简版webapi(minimal api)的入门教程,以及VS2022 上面的两个强大的新技能(热重载、代码自动反编译)的顺带演示。
284 0
【.NET 6】使用.NET 6开发minimal api以及依赖注入的实现、VS2022热重载和自动反编译功能的演示
|
XML API 数据格式
十九、.net core使用SoapCore开发webservice接口,以及使用HttpClientFactory动态访问webservice接口
使用SoapCore实现在.net core平台下开发webservice;以及使用HttpClientFactory动态访问webservice。首先,需要在包项目下面引用SoapCore:
815 0
十九、.net core使用SoapCore开发webservice接口,以及使用HttpClientFactory动态访问webservice接口
|
.NET C# 开发框架
【技术贴】c# asp.net 无法直接启动带有“类库输出类型”的项目 若要调试此项目,请在此解决方
c# asp.net 无法直接启动带有“类库输出类型”的项目 若要调试此项目,请在此解决方案中添加一个引用库项目的可执行项目。将这个可执行项目设置为启动项     解决办法: 右键解决方案->属性->通用属性->启动项目->单启动项目->选择Web项目
1419 0
|
XML 存储 Go
一起谈.NET技术,通过自定义配置实现插件式设计
  软件设计有一句话叫做约定优于配置,很多人将其作为拒绝配置的理由。但是,约定和配置的使用,都有个度的问题。我不赞为了所谓的扩展性,为你的应用设计一套只有你自己才能看懂的配置体系。但是,在很多场景中,配置是提供应用灵活度的首要甚至是唯一途径。
623 0
|
XML C# 数据格式
一起谈.NET技术,从数据到代码—基于T4的代码生成方式
  在之前写一篇文章《从数据到代码》(上篇、下篇)中,我通过基于CodeDOM+Custom Tool的代码生成方式实现了将一个XML表示的消息列表转换成了相应的C#代码,从而达到了强类型编程的目的。实际上,我们最常用的代码生成当时不是CodeDOM,而是T4,这是一个更为强大,并且适用范围更广的代码生成技术。
988 0
|
存储 .NET Windows
一起谈.NET技术,如何实现对上下文(Context)数据的统一管理 [提供源代码下载]
在应用开发中,我们经常需要设置一些上下文(Context)信息,这些上下文信息一般基于当前的会话(Session),比如当前登录用户的个人信息;或者基于当前方法调用栈,比如在同一个调用中涉及的多个层次之间数据。
977 0
|
SQL 存储 C#
一起谈.NET技术,创建代码生成器可以很简单:如何通过T4模板生成代码?[下篇]
  在《上篇》中我们通过T4模板为我们指定的数据表成功生成了我们需要的用于添加、修改和删除操作的存储过程。但是这是一种基于单个文件的解决方案,即我们必须为每一个生成的存储过程建立一个模板。如果我们提供一种基于多文件的代码生成方式,将会为编程人员带来极大的便利。
1832 0
|
XML 自然语言处理 测试技术
一起谈.NET技术,VS2010测试功能之旅:编码的UI测试(3)-操作对象的识别原理
  回顾    在之前的两章分别介绍了一个简单的示例, 操作动作的录制原理,通过修改UIMap.UItest文件控制操作动作代码的生成。想必大家对编码的UI测试操作动作的录制应该有一定了解了,在UI操作中,操作动作总是离不开具体的操作对象,例如点击,拖动,填写值等操作,总是要对文本框,或者按钮,窗体这些对象进行操作,不是凭空生成的。
894 0
|
C# Android开发 iOS开发
MobileForm控件的使用方式--用.NET(C#)开发APP的学习日志
今天继续Smobiler开发APP的学习日志,这次是做一个title、toolbar、侧边栏三种效果   样式一 一、          Toolbar 1.       目标样式 我们要实现上图中的效果,需要如下的操作: 2.