Real-world Reflection in .NET

he .NET platform supports a number of seemingly esoteric programming techniques, such as reflection, dynamic loading, late binding, and custom attributes. At first glance these topics may appear to be of little more than academic interest, having little use in the “real world.” However, this is most certainly not the case, as these very technologies allow you to build extremely flexible software. Before diving into a full-blown example of these topics at work, here’s how I’m using the terminology:

  • Reflection: The ability to discover the composition of a type (e.g., class, interface, structure, enumeration, or delegate) at runtime.
  • Dynamic Loading: The act of loading an assembly into memory programmatically at runtime.
  • Late Binding: The ability to create types and invoke their members at runtime without prior compile time knowledge. In contrast, early binding demands that the code base has compile time knowledge of the type you wish to create.
  • Attributes: Attributes provide the ability for developers to annotate their code base with bits of custom metadata. Attributes by themselves are completely useless until another piece of software discovers its presence via reflection.

Of the preceding four terms, reflection is listed first because it’s the foundation for dynamic loading, late binding, and the usefulness of attributes (in addition to numerous other .NET technologies). Again, despite what you might be thinking, these topics are not limited to the construction of object browsers, compilers, or code generation utilities.

Defining Extensible Applications
Assume that you are on a programming team that is building an application with the following requirement:

  • The product must be extensible through the use of third-party tools.

So, what exactly does extensible mean? Consider Visual Studio 2005. When Microsoft developed it, the company inserted various “hooks” to allow other software vendors to snap custom modules into the IDE. Obviously, the Visual Studio 2005 team had no way to set references to external .NET assemblies for which no code was yet available (thus, no early binding), so how exactly did they provide the required hooks? Here is one possible approach.

  • First, an extensible application must provide some input vehicle (such as a dialog box or command-line argument) to allow users to specify the module they want to plug in. This feature requires dynamic loading.
  • Second, an extensible application must be able to determine if the module supports the correct functionality (usually a set of predefined interfaces) to plug into its environment. This feature requires reflection.
  • Finally, an extensible application must be able to obtain access to the required infrastructure (e.g., the interface types) and invoke their members to trigger the underlying functionality. This feature requires late binding.

Simply put, if an extensible application has been preprogrammed to query for specific interfaces, it will then be able to determine at runtime if it can use the types in an external assembly (such types typically go by the term ‘snap-ins’). This is the exact approach taken by the Visual Studio 2005 team, and despite what you may be thinking, is not at all difficult.

Building an Extensible Application
In the sections that follow, you’ll see a complete example that illustrates the process of building an extensible Windows Forms application that you can augment via external assemblies. To serve as a road map, the extensible sample application includes the following assemblies:

  • CommonSnappableTypes.dll?This assembly contains type definitions that will be implemented by each snap-in as well as referenced by the extensible Windows Forms application.
  • CSharpSnapIn.dll?A snap-in written in C# that leverages the types of CommonSnappableTypes.dll.
  • Vb2005SnapIn.dll?A snap-in written in Visual Basic 2005, which leverages the types of CommonSnappableTypes.dll.
  • MyPluggableApp.exe?This Windows Forms application (which also leverages CommonSnappableTypes.dll) will be the entity that may be extended by the functionality of each snap-in. This application will make use of dynamic loading, reflection, and late binding to dynamically discover the functionality of assemblies of which it has no prior knowledge.

Building CommonSnappableTypes.dll
First you’ll need to create an assembly that contains the types a given snap-in must leverage to plug into your extensible Windows Forms application. The CommonSnappableTypes class library project defines two such types:

   using System;      namespace CommonSnappableTypes   {     // All snap-ins must implement this interface.     public interface IAppFunctionality     { void DoIt(); }        // Optionally, snap-in designers may supply      // company information.      [AttributeUsage(AttributeTargets.Class)]     public sealed class CompanyInfoAttribute :         System.Attribute     {       private string companyName;       private string companyUrl;       public CompanyInfoAttribute(){}          public string Name       {         get { return companyName; }         set { companyName = value; }       }          public string Url       {         get { return companyUrl; }         set { companyUrl = value; }       }     }   }

The IAppFunctionality type provides a polymorphic interface for all snap-ins that the extensible Windows Forms application can consume. Because this example is purely illustrative, it exposes a single method named DoIt(). In a more realistic example, imagine an interface (or a set of interfaces) that allows the snap-in to generate scripting code, render an image onto the application’s toolbox, or integrate into the main menu of the hosting application.

The CompanyInfoAttribute type is a custom attribute that snap-in creators can optionally apply to their snap-in. As you can tell by the name of this class, [CompanyInfo] allows the snap-in developers to provide some basic details about the component’s point of origin. Notice that you can create custom attributes by extending the System.Attribute base class, and you can annotate them with the [AttributeUsage] attribute to define valid targets where developers can apply your attribute (limited to class types in the preceding code example).

Building the C# Snap-In
Next, you need to create a type that supports the IAppFunctionality interface. Again, to focus on the overall design of an extensible application, a trivial implementation is in order. Create a new C# code library named CSharpSnapIn that defines a class type named “TheCSharpModule.” Given that this class must make use of the types defined in CommonSnappableTypes, be sure to set a reference to that assembly (as well as System.Windows.Forms.dll so you can display a pertinent message for the example).

   using System;   using CommonSnappableTypes;   using System.Windows.Forms;      namespace CSharpSnapIn   {     [CompanyInfo(Name = "Intertech Training",       Url = "www.intertechtraining.com")]     public class TheCSharpModule : IAppFunctionality      {       // Using explicit interface implementation,        // as only the extensible app       // will need to obtain IAppFunctionality.        void IAppFunctionality.DoIt()       {          MessageBox.Show(             "You have just used the C# snap in!");       }     }   }

Notice that I chose to make use of explicit interface implementation when supporting the IAppFunctionality interface. This is not required; however, the idea is that the only part of the system that needs to directly interact with this interface type is the hosting Windows Forms application. Also note that the code uses named property syntax to specify the values used to set the Name and Url properties of the [CompanyInfo] attribute.

Building a Visual Basic 2005 Snap-In
Now, to simulate the role of a third-party vendor who prefers Visual Basic 2005 over C#, create a new Visual Basic 2005 code library (Vb2005SnapIn) that references the same assemblies as the previous CSharpSnapIn project. The code behind this class type is again intentionally simple:

   Imports System.Windows.Forms   Imports CommonSnappableTypes       _   Public Class TheVb2005Module     Implements IAppFunctionality        Public Sub DoIt() Implements         CommonSnappableTypes.IAppFunctionality.DoIt        MessageBox.Show(           "You have just used the VB 2005 snap in!")     End Sub   End Class

Notice that applying attributes in Visual Basic 2005 requires angle bracket syntax (< >) rather than the C#-centric square brackets ([ ]). Additionally, note that the code uses the Implements keyword at both the class and method level to add support for interface types.

Building the Extensible Windows Forms Application
The final step is to create a new Windows Forms application (MyExtensibleApp) that allows users to select a snap-in using a standard Windows Open dialog box. Set a reference to the CommonSnappableTypes.dll assembly, but not the CSharpSnapIn.dll or Vb2005SnapIn.dll code libraries. Remember that the whole goal of this application is to make use of dynamic loading, late binding, and reflection to determine the “snappability” of external binaries created by third-party vendors.

I won’t bother to discuss all the details of building the Windows Form discussed in this article. Basically though, place a MenuStrip component onto the Forms designer, and define a topmost menu item named Tools that provides a single submenu named Snap In Module. The Windows Form should also contain a descriptive Label for a ListBox (named lstLoadedSnapIns) that the form uses to display the names of each snap-in loaded by the user. Figure 1 shows the final GUI design.

?
Figure 1. Sample Windows Form: The figure shows the final design of the sample application used to select and list loaded snap-ins.

Next, be sure to update your initial C# file with using directives for the System.Reflection and CommonSnappableTypes namespaces:

   using System.Reflection;   using CommonSnappableTypes;

The code that handles the Click event of the “Tools?> Snap In Module” menu item (which you can create by double-clicking the menu item from the menu editor) displays a File Open dialog box and extracts the path to the selected file.

   private void snapInModuleToolStripMenuItem_Click(      object sender, EventArgs e)   {     // Allow user to select an assembly to load.     OpenFileDialog dlg = new OpenFileDialog();     if (dlg.ShowDialog() == DialogResult.OK)     {       if (LoadExternalModule(dlg.FileName) == false)         MessageBox.Show(           "Nothing implements IAppFunctionality!");     }   }

The preceding code passes the user-selected assembly path to a helper function named LoadExternalModule() for processing. LoadExternalModule() inspects the selected assembly and returns false when it is unable to find a type implementing IAppFunctionality; therefore, the Click method handles that possibility by displaying a message to the end user.

The LoadExternalModule() method performs the following core tasks:

  • Dynamically loads the assembly into memory via the Assembly.LoadFrom() method.
  • Determines if the assembly contains a type implementing IAppFunctionality by analyzing the type information for each type in the assembly.

When LoadExternalModule finds a type that implements IAppFunctionality, it calls the DoIt() method and adds the fully qualified name of the type to the ListBox.

   private bool LoadExternalModule(string path)   {     bool foundSnapIn = false;     IAppFunctionality itfAppFx = null;     Assembly theSnapInAsm = null;        try     {       // Dynamically load the selected assembly.       theSnapInAsm = Assembly.LoadFrom(path);     }     catch     {       // If any error at all takes place, just        // return false.       return false;     }        // Get all types in assembly.     Type[] theTypes = theSnapInAsm.GetTypes();        // See if the type implements IAppFunctionality.     for (int i = 0; i < theTypes.Length; i++)     {       Type t = theTypes[i].GetInterface(          "IAppFunctionality");       if (t != null)       {         foundSnapIn = true;                        // Use late binding to create the type.         object o = theSnapInAsm.CreateInstance(            theTypes[i].FullName);            // Call DoIt() off the extracted interface.         itfAppFx = o as IAppFunctionality;         itfAppFx.DoIt();         lstLoadedSnapIns.Items.Add(           theTypes[i].FullName);       }     }     return foundSnapIn;   } 

Note that the for loop shown above iterates over all types in the assembly to handle the possibility that a single assembly has multiple snap-ins.

Testing the Application
At this point you can run your application. When you select the CSharpSnapIn.dll or Vb2005SnapIn.dll assemblies, you should see the messages displayed via the calls to MessageBox.Show(), and the ListBox will update with the names of the loaded external modules. Figure 2 shows one possible run.

?
Figure 2. Loaded Snap-ins: After selecting the CSharpSnapIn and Vb2005SnapIn assemblies, you'll see these items in the "Loaded Snap-ins" ListBox.
?
Figure 3. Invalid File Message: This is the message you'll see if you try to load an external file that doesn't contain a type that implements IAppFunctionality.

If you attempt to load any external file that does not contain a type implementing IAppFunctionality, you'll get the message shown in Figure 3.

Reflecting over Custom Attributes
The final task is to display the metadata provided by the custom [CompanyInfo] attribute. To do so, update the current implementation of LoadExternalModule() to call a new helper function named DisplayCompanyData() before exiting the scope of the conditional block that begins with if (t != null). The method takes a single System.Type parameter.

   private bool LoadExternalModule(string path)   {   ...     if (t != null)     {   ...       // Show company info.       DisplayCompanyData(theTypes[i]);     }   ...     return foundSnapIn;   }

Using the incoming type, simply reflect over the [CompanyInfo] attribute using the Type.GetCustomAttributes() method (the Boolean parameter of this method controls whether to also search parent classes for the attribute set):

   private void DisplayCompanyData(Type t)   {     // Get all attributes on type.     object[] customAtts = t.GetCustomAttributes(false);
?
Figure 4. Company Info: The DisplayCompanyData() method extracts the metadata from the [CompanyInfo] attribute and displays it in a MessageBox.
// Show data for any [CompanyInfo] attributes. foreach (CompanyInfoAttribute c in customAtts) { MessageBox.Show(c.Url, string.Format( "More info about {0} can be found at", c.Name)); } }

Now, when you select an assembly containing a type implementing IAppFunctionality, you'll find the metadata applied by the type's developers to the [CompanyInfo] attribute displayed as yet another informational message (see Figure 4).

That wraps up the example application. I hope at this point you can see that reflection services and custom attributes can be quite helpful in your real-world applications, and are not limited to tool builders.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Related Posts