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






4.45/5 (8投票s)
本文演示了如何在运行时加载和分析类库,如何选择用自定义属性标记的适当方法,并使用它们来扩展应用程序的功能。
引言
.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
实例都是相应类反射的关键。它提供了一组属性来探索类的特性:IsAbstract
、IsEnum
、IsSealed
等。它还提供了单数和复数形式的方法集,例如 Assembly.GetType(typeName)
和 Assembly.GetTypes()
,以获取类的任何成员。例如,我们可以使用 Type.GetProperty(propName)
获取单个 PropertyInfo
对象,或者使用 Type.GetProperties()
检索 PropertyInfo[]
数组。因此,可以分析类的特定属性或所有属性。
PropertyInfo
对象对于获取(或设置)任何已发现类型实例的相应属性的值非常有用。只需调用方法 aPropertyInfo.GetValue(instanceOfDiscoveredType)
。
为了解决我的任务,我需要获取类中所有排序方法的信息。因此,我将使用 Type.GetMethods()
并分析接收到的 MethodInfo[]
数组。MethodInfo
实例为我们提供了有关方法的完整信息。我们可以使用各种属性(例如 IsAbstract
、IsConstructor
、IsPublic
、IsStatic
、Name
、ReturnType
等)来检查它,并使用方法(例如 CreateDelegate()
、GetCustomAttributes()
、GetParameters()
、Invoke()
等)来操作它。
我如何将适当的排序方法与类中定义的所有方法分开?换句话说,我如何选择 MethodInfo[]
数组中相应的元素?能够通过后台线程(在我的应用程序中)对整数数组进行排序的方法接受三种特定类型的参数:int[]
、BackgroundWorker
和 DoWorkEventArgs
。我们可以通过 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 命名空间中定义了许多属性:CLSCompliantAttribute
、ObsoleteAttribute
、SerializableAttribute
、TestClassAttribute
等等。编译器、测试框架或其他程序读取属性以决定如何处理我们的代码。例如,当编译器在类定义之前读取 [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
获取它。但我确定正确的程序集包含类 SortMethodProvider
和 MethodNameAttribute
。我将检查这一点。
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 反射工具很酷。它们运行良好,对于构建灵活的应用程序非常有用。
享受“线程排序”应用程序的新版本带来的乐趣吧!