65.9K
CodeProject 正在变化。 阅读更多。
Home

通过反射工具动态构建应用程序功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (8投票s)

2016年8月2日

CPOL

7分钟阅读

viewsIcon

15971

downloadIcon

290

本文演示了如何在运行时加载和分析类库,如何选择用自定义属性标记的适当方法,并使用它们来扩展应用程序的功能。

引言

.NET 反射为开发人员提供了完整的工具,可以在运行时发现并进一步构建可执行代码。codeproject.com 上有许多专门介绍这些工具的文章。我们将回顾其中一些重要功能,并演示一个实际的使用示例。

我们将探索一些类库,并选择一组我们需要在单独线程中运行的专用方法。自定义属性将帮助我们识别适当的方法并扩展应用程序的 GUI。

背景

不久前,我在 codeproject.com 上发表了一篇文章 "通过自己的类封装 BackgroundWorker 以简化多线程应用程序的创建",其中介绍了在并行线程中执行不同排序算法。所描述的应用程序有一个缺陷:算法集是硬编码在程序中的,用户无法更改。这个缺陷一直困扰着我,因此我决定通过在运行时动态加载适当的方法来解决它。有趣的是,这种方法可以减少应用程序中的依赖关系并改进其架构。

反射工具

在运行时检查已编译代码的过程称为反射。在 .NET 世界中,我们可以通过编程方式选取和调查任何 exe 或 dll,获取有关其类的详细信息,动态实例化这些类并执行其代码等。System.Reflection 命名空间包含一组用于这些目的的类。我将回顾其中一些解决我的任务所需的类。

程序集加载

假设我们的应用程序需要 TheImportantClass,我们有几个包含由不同制造商生产的类实现的程序集,并且在设计时我们不知道我们将选择哪个程序集。我们可以在运行时选择程序集吗?是的,我们可以!可以在运行时确定程序集的名称并动态加载它。

请看下面的代码片段。

uses System.Reflection

public void LoadAssembly()
{
    // Get the assembly name in any way
    string assemblyName = myApplication.GetAssemblyName();
    // Load the assembly dynamically
    try
    {
        Assembly dynamicAssembly = Assembly.Load(assemblyName);
    }
    catch (System.IO.FileNotFoundException ex)
    {
        System.Windows.Forms.MessageBox.Show(ex.Message, "Assembly error",
           MessageBoxButtons.OK, MessageBoxIcon.Error);
        return;
    }
    // We suppose that the assembly name coincides with the name of its namespace
    string nameOfNamespace = assemblyName;
    // Use the assembly by GetTypes() and so on
    System.Type importantClassType = dynamicAssembly.GetType(nameOfNamespace + ".TheImportantClass");
    // Use the class with help of the instance of System.Type
    // ...

静态方法 Assembly.Load() 接受程序集名称(不带任何扩展名的文件名),并在应用程序本地文件夹中查找程序集,然后在全局程序集缓存中查找。

使用静态方法 Assembly.LoadFrom() 确定程序集的完整路径。

    string assemblyPath = myApplication.GetAssemblyPath();
    Assembly dynamicAssembly = Assebly.Load(assemblyName);
    string nameOfNamespace = System.IO.Path.GetFileNameWithoutExtension(assemblyPath);
    System.Type importantClassType = dynamicAssembly.GetType(nameOfNamespace + ".TheImportantClass");
    // ...

类型检查

加载程序集后,我们得到 Assembly 实例,它帮助我们发现程序集中的类型。如果我们知道它的名称(如下面的代码片段所示),我们可以获得某个类型。

// Get certain type
    Type aClass = dynamicAssembly.GetType(theQualifiedNameOfClass);
    // where string theQualifiedNameOfClass describes the class name preceded by the name of
    // its namespace like this: "CoolAssembly.TheImportantClass"

// Deal with aClass

或者可以获取程序集中定义的所有类型的数组。

// Get ALL types defined in the assembly
    Туре[] classes = dynamicAssembly.GetTypes();
    foreach (Type definedType in classes)
    {
        myApplication.DoSomethingWith(definedType);
    }

每个 System.Type 实例都是相应类反射的关键。它提供了一组属性来探索类的特性:IsAbstractIsEnumIsSealed 等。它还提供了单数和复数形式的方法集,例如 Assembly.GetType(typeName)Assembly.GetTypes(),以获取类的任何成员。例如,我们可以使用 Type.GetProperty(propName) 获取单个 PropertyInfo 对象,或者使用 Type.GetProperties() 检索 PropertyInfo[] 数组。因此,可以分析类的特定属性或所有属性。

PropertyInfo 对象对于获取(或设置)任何已发现类型实例的相应属性的值非常有用。只需调用方法 aPropertyInfo.GetValue(instanceOfDiscoveredType)

为了解决我的任务,我需要获取类中所有排序方法的信息。因此,我将使用 Type.GetMethods() 并分析接收到的 MethodInfo[] 数组。MethodInfo 实例为我们提供了有关方法的完整信息。我们可以使用各种属性(例如 IsAbstractIsConstructorIsPublicIsStaticNameReturnType 等)来检查它,并使用方法(例如 CreateDelegate()GetCustomAttributes()GetParameters()Invoke() 等)来操作它。

我如何将适当的排序方法与类中定义的所有方法分开?换句话说,我如何选择 MethodInfo[] 数组中相应的元素?能够通过后台线程(在我的应用程序中)对整数数组进行排序的方法接受三种特定类型的参数:int[]BackgroundWorkerDoWorkEventArgs。我们可以通过 GetParameters() 方法将方法的参数的详细描述作为 ParameterInfo[] 数组获取。然后我们可以检查参数的数量、它们的类型等等。

    List<MethodInfo> methods = new List<MethodInfo>();
    foreach (MethodInfo method in sortMethodProviderType.GetMethods())
    {
        parms = method.GetParameters();
        if (parms.Length == 3 && parms[0].ParameterType == typeof(int[]) &&
            parms[1].ParameterType == typeof(BackgroundWorker) &&
            parms[2].ParameterType == typeof(DoWorkEventArgs))
        {
            methods.Add(method);
        }
    }

MethodInfo.Name 属性可用于在 GUI 中显示所选方法。

 // methodComboBox is the GUI element for making selection of a sorting method
    foreach (MethodInfo method in methods)
    {
        methodComboBox.Items.Add(method.Name);
    }

为了启动任何方法,我们可以使用方法 MethodInfo.Invoke(theMethodOwner,Parameters)

// First argument must be null for the static method
   selectedMethod.Invoke(null, new object[] { arrayToSort, actualBackgroundWorker, eventArgs });
// where selectedMethod is the instance of MethodInfo

到目前为止,我描述了一种可能但不是最好的选择所需方法和构建 GUI 的方法。如果应用程序代码没有引用定义参数类型的程序集,那么通过类型比较进行方法识别将变得不可能。如果我们在应用程序组合框(或列表框)中使用方法的代码名称,那么 GUI 会变得不清晰。我们通过下面描述的代码属性来解决这些困难。

自定义属性

属性是允许我们用额外元数据装饰代码的特殊对象。在 .NET 命名空间中定义了许多属性:CLSCompliantAttributeObsoleteAttributeSerializableAttributeTestClassAttribute 等等。编译器、测试框架或其他程序读取属性以决定如何处理我们的代码。例如,当编译器在类定义之前读取 [CLSCompliant] 属性时,它会检查此类的每个公共成员是否与 CLS 兼容。BinaryFormatter.Serialize() 方法正在查找 [Serializable] 等等。如果一个类有一个属性,那么属性类型表明了该类的某个“属性”。因此,属性的类型是最重要的。此外,属性可以在其属性中提供任何额外信息。例如,[Obsolete("The Warning Message")] 属性强制编译器在 IDE 的错误列表中添加带有文本 "The Warning Message" 的警告。

为了我们的目的,我们可以使用适当的预定义属性或定义我们自己的属性。自定义属性类必须继承 System.Attribute 类,并且其名称必须以后缀“Attribute”结尾。属性在其属性中封装数据,该属性必须由构造函数设置。我们可以通过“属性的属性”[AttributeUsage] 定义属性的附加使用规则:我们可以指定属性的目标,限制其多次使用等等。

为了标记适当的排序方法,我使用了下面描述的 MethodNameAttribute

    // The special attribute marks sorting methods designed for binding at run time.
    // It holds the method name(s) for the user interface
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class MethodNameAttribute: System.Attribute
    {
        public string OuterName { get; set; }
        // the optional property holds ukrainian name of a method
        public string LocalName { get; set; }
        public MethodNameAttribute(string name)
        {
            OuterName = name;
        }
    }

属性的使用方式如下

    // Bubble sort algorithm (simple exchange method)
    [MethodName("Bubble sort", LocalName = "Метод бульбашки")]
    public static void BubbleSortInBackground(int[] arrayToSort, BackgroundWorker worker,
        DoWorkEventArgs e)
    {
        for ...

不需要写属性的全名——编译器会自动添加后缀“Attribute”。MethodNameAttribute 的构造函数接受一个字符串参数来初始化 OuterName 属性。如果我们要设置属性的可选属性,那么我们必须使用特殊的语法“property = value”,如上面的代码所示。

反射实战

让我回顾一下目标:扩展应用程序的功能,使其能够在运行时加载一组排序方法。我采取了几个步骤来实现这个目标。

第一。我不知道包含排序方法定义的程序集名称。我将通过执行 OpenFileDialog 获取它。但我确定正确的程序集包含类 SortMethodProviderMethodNameAttribute。我将检查这一点。

public bool LoadAssembly(string assemblyName)
{
    ...
    System.Type sortMethodProviderType = assembly.GetType(nameOfNamespace + ".SortMethodProvider");
    System.Type methodNameAttrType = assembly.GetType(nameOfNamespace + ".MethodNameAttribute");
    if (sortMethodProviderType == null || methodNameAttrType == null)
    {
        MessageBox.Show(
            "SortMethodProvider or MethodNameAttribute not found in " + assemblyName + " assembly",
            "Sorting assembly error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    } ...
}

第二。我将从所有 SortMethodProvider 的方法中选择适当的排序方法,这些方法是静态的,并用 MethodNameAttribute 标记。执行选择最简单的方法之一是使用 LINQ。

    MethodInfo[] methods = (from m in sortMethodProviderType.GetMethods()
                 where (m.IsStatic && m.IsDefined(methodNameAttrType, false))
                 select m).ToArray<MethodInfo>();

第三。我将从方法的属性中读取方法的 UI 名称。

    string[] methodNames = new string[methods.Length];
    PropertyInfo propInfo = methodNameAttrType.GetProperty("OuterName");
    for (int i = 0; i < methods.Length; ++i)
        methodNames[i] = propInfo.GetValue(methods[i].GetCustomAttribute(methodNameAttrType)).ToString();

第四。我将创建一个新的排序线程和一个新的可视组件,以便根据用户的请求动态反映排序过程。(不要忘记设置动态创建的可视组件的 Parent 属性!)

    // Dynamic creation of a new arrayView component
    private void btnAdd_Click(object sender, EventArgs e)
    {
        // an array to sort and view
        int[] array = model.GetArray();
        ArrayView a = new ArrayView(new Point(xLocation[Views.Count], yLocation), array);
        // set names to the arrayView's combobox
        a.AddRange(controller.MethodNames);
        // set event handlers
        a.ComboIndexChanged += arrayView_ComboIndexChanged;
        a.SortingComplete += DecreaseThreadsRunning;
        // set a new backGroundSorter
        a.SetSorter(controller.GetSorter(array));
        // visualize new component
        a.Parent = this;
        // store the component
        Views.Add(a);
    }

应用程序的早期版本是根据 MVC 模式构建的,但模型持有对控制器的引用以对数组进行一些更改。这不太好。在新版本中,只有视图分别与模型和控制器通信。

结论

.NET 反射工具很酷。它们运行良好,对于构建灵活的应用程序非常有用。

享受“线程排序”应用程序的新版本带来的乐趣吧!

© . All rights reserved.