Reflection is generally all about getting information about code. However, using the reflection APIs, you can dynamically execute code you load from an arbitrary assembly, giving you an easy way to implement a plugin architecture in your app.
Solution: Reflection is most commonly used for discovery, and nearly everything in .NET is discoverable.
This snippet of code populates a System.Windows.Forms.TreeView control with classes, methods, properties, fields, and events from an assembly:
See the EnumerateAssemblyTypes sample project for the full source code.
Solution: Define an attribute class. Attributes are used extensively in .NET. You’ve seen in Chapter 11, “Files and Serialization,” that all you need to do to get basic serialization working on a class is to add the [Serializable] attribute above the class definition. In Chapter 12, “Networking and the Web,” you saw that attributes on methods define service interfaces in WCF.
There is nothing magical about Attributes, however. All they do is attach metadata to another piece of code. It’s up to your code to extract that metadata and do something with it.
The following example shows a simple attribute (which we used in Chapter 6, “Enumerations”) that allows a developer to attach a culture string to any other program construct:
The AttributeTargets enumeration allows you to decide what target are valid for this attribute. The valid values are Assembly, Module, Class, Struct, Enum, Constructor, Method, Property, Field, Event, Interface, Parameter, Delegate, ReturnValue, GenericParameter, and All. If you want to combine values you can use the |
operator, like this: AttributeTargets.Field | AttributeTargets.Property.
You can also specify with AllowMultiple whether multiple instances of this attribute are valid on a single element.
To apply this attribute, you use the square brackets:
Since every attribute ends in “Attribute,” the compiler will allow you to just specify the first part of the class name when applying them.
In order to make use of the attribute, you must write code that is aware of the CultureAttribute
class and look for it.
This example produces the output:
Cultures of Program: en-US, en-GB
Solution: Using reflection, it’s possible to instantiate code from assemblies that are not referenced at build time. Suppose you have a class defined in DynamicInstantiateLib.dll:
In a separate assembly that does not have a reference to DynamicInstantiateLib.dll, you can use this code to nevertheless create an instance of TestClass
:
Because you don’t have a reference, you can’t use the actual type to refer to it. This would seem to make it hard to use, but there are a few ways around this, as seen in the next section.
Solution: Assuming the following code, you have a few options:
Method 1:
Note that we pass the obj
because Add
is an instance method, and obj
is that instance.
Method 2:
InvokeMember
does not work for generic methods, so here’s another way (also valid for Add
):
Method 3:
New in C# 4.0, you can use dynamic types to have the method call resolved at runtime, as essentially a shortcut syntax for the previous methods:
You won’t get IntelliSense with dynamic types, but this definitely looks cleaner than using MethodInfo
objects and Invoke
calls.
Beware: Putting off type checking until runtime has its costs, mostly in unforeseen errors. Take advantage of compiler-enforced static typing whenever possible and only use dynamic typing when absolutely necessary.
Solution: By using all the principles of this chapter, plus a few more tidbits, it’s extremely easy in .NET to create your own plugin system for your application (see Figure 24.1).
First, you need an assembly that both the plugins and the app can reference. This assembly defines the interfaces that plugins must implement and any common code you want to give them access to. For our simple example, there’s just a single interface:
Create a new assembly and add a reference to the previous assembly with the IImagePlugin interface. Then define this code:
The final step is discovery, which is just loading each DLL and checking to see if there are types that implement our interface. If so, create an instance of it and add it to the menu.
The following code is an excerpt from the PluginDemo sample project:
Making a plugin work is really about defining a good set of interfaces. For example, you could allow communication the other way by defining an interface that your application implements and passing that to the plugin, allowing it to communicate and modify the application itself.