|
YOUR FEEDBACK
|
TOP MICROSOFT .NET LINKS INETA's Choice Building Dynamic Systems Using Attributes and Reflection
Building Dynamic Systems Using Attributes and Reflection
By: Bill Wagner
Aug. 10, 2004 12:00 AM
Building binary components sometimes means utilizing late binding and reflection to find the code with the particular functionality you need. Reflection is a powerful tool, and it enables you to write software that is much more dynamic. Using reflection, an application can be upgraded with new capabilities by adding new components that were not available when the application was deployed. That's the upside. With this flexibility comes increased complexity, and with increased complexity comes increased chance for many problems. When you use reflection, you circumvent C#'s type safety. Instead the invoke members use parameters and return values typed as object. You must make sure the proper types are used at runtime. In short, using reflection makes it much easier to build dynamic programs, but it is also much easier to build broken programs. The .NET language compilers use reflection to compile your code: reflection tells the compiler the names of types, and the signature of each member of a type. The compilers use this information to verify the types and signatures of all your method calls. When you use reflection, you work around the compiler to find types and members on your own. You must perform all the validation on those types; you must handle errors when you get it wrong. Let's begin with creating instances of a given type. You can often accomplish the same result using a class factory. Consider this code fragment that loads and assembly and creates an instance of the first public type, using the default constructor:
// Creation with reflection:
Type t = typeof( MyType );
MyType obj = NewInstance( t ) as
MyType;
// Factory function,
// based on Reflection:
object NewInstance( Type t )
{
ConstructorInfo ci =
t.GetConstructor(
new Type[ 0 ] );
if ( ci != null )
return ci.Invoke( null );
return null;
}
The above code examines the type using reflection and invokes the default constructor to create the object. This is brittle code that relies on the presence of a default constructor. It still compiles if you remove the default constructor from MyType. Another use of reflection is to access members of a type. You can use the member name and the Type to call a particular function at runtime:
// Example usage:
Dispatcher.InvokeMethod(
AnObject, "MyHelperFunc" );
// Dispatcher Invoke Method:
public void InvokeMethod(
object o, string name )
{
// Find the member functions
// with that name.
MemberInfo[] myMembers =
o.GetType( ).GetMember(
name );
foreach( MethodInfo m in
myMembers )
{
// Make sure the parameter
// list matches:
if ( m.GetParameters( ).Length
== 0 )
// Invoke:
m.Invoke( o, null );
}
}
There are several possible runtime errors lurking in the code above. If the name is typed wrong, the method won't be found. No method will be called. It's also a very simple example. Creating a more robust version of InvokeMethod would need to check the types of all proposed parameters against the list of all parameters returned by the GetParameters() method. That code is lengthy enough and ugly enough that I did not even want to waste the space to show it to you. To improve the situation and find the proper alternatives, examine the code above again. There are three common practices used in the reflection-based idioms that cause the problems. First, the human readable names of types or members are used to find them. This is a cause for human errors: if you mistype the name of a method, property, or type, you have a runtime error. Second, all the types, parameters, and return values are used through the base class: System.Object. You've sacrificed a strongly typed environment. You must use reflection to find any implemented interfaces. The chance for human error is high. It's also harder to maintain: any changes to working code that makes use of these techniques results in a runtime error, not a compilation error. Code that makes use of reflection must be the target of ongoing regression testing. I've spent the last page trying to scare you away from overusing reflection as a programming tool. I do not mean to scare you away from using reflection. Rather, I want you to use reflection to solve the right problems, with the proper safeguards in place. By defining custom attributes for the types, methods and properties you intend to use with reflection you make them easier to access. The custom attribute indicates how you intended the method to be used at runtime. Attributes can verify some properties of the target. As an example, let's build a mechanism to add menu items and command handlers to a running software system. The requirements are simple: drop an assembly into a directory, and the program will find out about it and add new menu items for the new command. This is one of those jobs that is best handled with reflection: your main program needs to interact with assemblies that have not yet been written. There are several tasks necessary to add a command to your menus dynamically. You need to load an assembly using the Assembly.LoadFrom( ) function. You need to find the types that do provide menu handlers. You need to create an object of the proper type. Type.GetConstructor( ) and ConstructorInfo.Invoke( ) are the tools for that. You need to find a method matches the menu command event handler signature. After all those tasks, you need to figure out where on the menu to add the new text, and what the text should be. Attributes make many of these tasks easier. By tagging different classes and event handlers with custom attributes, you greatly simplify your task of finding and installing those potential command handlers. You use attributes in conjunction with reflection is to minimize the risks described in the previous item. The first task is to write the code that finds and loads the add-in assemblies. Assume that the add-ins are in a subdirectory under the main executable directory. The code to find and load the assemblies is very simple:
// Find all the assemblies in
// the Add-ins directory:
string AddInsDir =
string.Format( "{0}/Addins",
Application.StartupPath );
string[] assemblies =
Directory.GetFiles( AddInsDir,
"*.dll" );
foreach ( string assemblyFile in
assemblies )
{
Assembly asm =
Assembly.LoadFrom(
assemblyFile );
// Find and install command
// handlers from the assembly.
}
Next, we need to replace those comments with the code that finds the classes that implement command handlers and installing the handlers. After you load an assembly, you can use reflection to find all the exported types in an assembly. Use attributes to figure out which exported types contain command handlers, and which methods are the command handlers. An attribute class marks the types that have command handlers:
// Define the Command Handler
// Custom Attribute:
[AttributeUsage(
AttributeTargets.Class )]
public class
CommandHandlerAttribute :
Attribute
{
public
CommandHandlerAttribute( )
{
}
}
This attribute is all the code you need to write in order to mark each command. There are two conventions to remember when you create attribute classes. First, always mark an attribute class with the AttributeUsage attribute. It tells other programmers, and the compiler, where your attribute can be used. The example above states that the CommandHandlerAttribute can be applied only to classes; it cannot be applied on any other language element. Secondly, as you see below, it is customary to omit the "Attribute" from the name when attaching an attribute to an item:
[ CommandHandler ]
public class CmdHandler
{
// Implementation coming soon.
}
By including the Command HandlerAttribute attribute on every type that implements a command handler, you simplify your task of finding the command handler types using reflection. You call GetCustomAttributes to determine if a type has the CommandHandlerAttribute. The attributes matching the name are returned in an array. If there are no attributes that match, it returns an empty array:
// Find all the assemblies in
// the Add-ins directory:
string AddInsDir =
string.Format(
"{0}/Addins",
Application.StartupPath);
string[] assemblies =
Directory.GetFiles( AddInsDir,
"*.dll" );
foreach ( string assemblyFile in
assemblies )
{
Assembly asm =
Assembly.LoadFrom(
assemblyFile );
// Find and install command
// handlers from the assembly.
foreach( System.Type t in
asm.GetExportedTypes( ))
{
if (t.GetCustomAttributes(
typeof(
CommandHandlerAttribute ),
false).Length > 0 )
{
// Found the command
// handler attribute
// on this type.
// This type implements a
// command handler.
// configure and add it.
}
// Else, not a command
// handler. Skip it.
}
}
Tagging a type with an attribute is the simplest and clearest method of finding that item later using reflection. You follow the same strategy to find the command event handlers inside the types that contain command handlers. A type might easily implement several command handlers, so you define a new attribute to attach to each command handler. This attribute will include parameters the define where in the hierarchy to place menu items. This new attribute will help you add the text to the proper location in the menu and locate the method that handles the command. Each event handler handles one specific command. That command is located in a specific spot on the menu. To tag a command handler, you define an attribute that marks a property as a command handler, and declares the text for the menu item, and the text for the parent menu item:
[DynamicCommand( "Test Command",
"Parent Menu" )]
public EventHandler CmdFunc
{
get
{
if ( theCmdHandler == null )
theCmdHandler = new
System.EventHandler
( DynamicCommandHandler );
return theCmdHandler;
}
}
private void
DynamicCommandHandler(
object sender,
EventArgs args )
{
// Implementation of the
// command goes here.
}
The DynamicCommand attribute is constructed with two parameters: the command text, and the text of the parent menu. The attribute class contains a constructor that initializes the two strings for the menu item. Those strings are available as properties:
[AttributeUsage(
AttributeTargets.Property ) ]
public class DynamicMenuAttribute
: System.Attribute
{
private readonly string
_menuText;
private readonly string
_parentText;
public DynamicMenuAttribute(
string CommandText,
string ParentText )
{
_menuText = CommandText;
_parentText = ParentText;
}
public string MenuText
{
get { return _menuText; }
set { _menuText = value; }
}
public string ParentText
{
get { return _parentText; }
set { _parentText = value; }
}
}
This attribute class is tagged so that it can only be applied to properties. The command handler must be exposed as a property in the class that provides access to the command handler. Using this technique simplifies finding the command handler code and attaching it to the program at startup. Now you create an object of that type, find the command handlers and attach them to new menu items. You guessed it, you use a combination of attributes and reflection to find and use the command handler properties.
// Expanded from the first
// code sample:
// Find the types in the assembly
foreach( Type t in
asm.GetExportedTypes( ) )
{
if (t.GetCustomAttributes(
typeof(
CommandHandlerAttribute
), false).Length > 0 )
{
// Found a command handler
// type:
ConstructorInfo ci =
t.GetConstructor(
new Type[0] );
// No Default ctor:
if ( ci == null )
continue;
object obj = ci.Invoke(
null );
PropertyInfo [] pi =
t.GetProperties( );
// Find the properties that
// are command handlers
foreach( PropertyInfo p in
pi )
{
string menuTxt = "";
string parentTxt = "";
object [] attrs =
p.GetCustomAttributes(
typeof (
DynamicMenuAttribute ),
false );
foreach ( Attribute attr
in attrs )
{
DynamicMenuAttribute dym
= attr as
DynamicMenuAttribute;
if ( dym != null )
{
// This is a command
// handler.
menuTxt = dym.MenuText;
parentTxt =
dym.ParentText;
MethodInfo mi =
p.GetGetMethod();
EventHandler h =
mi.Invoke( obj, null )
as EventHandler;
UpdateMenu( parentTxt,
menuTxt, h );
}
}
}
}
}
private void UpdateMenu( string
parentTxt, string txt,
EventHandler cmdHandler )
{
MenuItem menuItemDynamic =
new MenuItem();
menuItemDynamic.Index = 0;
menuItemDynamic.Text = txt;
menuItemDynamic.Click +=
cmdHandler;
//Find the parent menu item.
foreach ( MenuItem parent in
mainMenu.MenuItems )
{
if ( parent.Text ==
parentTxt )
{
parent.MenuItems.Add(
menuItemDynamic );
return;
}
}
// Existing parent not found:
MenuItem newDropDown = new
MenuItem();
newDropDown.Text = parentTxt;
mainMenu.MenuItems.Add(
newDropDown );
newDropDown.MenuItems.Add(
menuItemDynamic );
}
This example shows you how you can utilize attributes to simplify programming idioms that use reflection. You tagged each type that provided a dynamic command handler with an attribute. That made it easier to find the command handlers when you dynamically loaded the assembly. By applying Attribute Targets (another Attribute) you limit where the dynamic command attribute can be applied. This simplifies the difficult task of finding the sought types in a dynamically loaded assembly: you greatly decrease the chance of using the wrong types. It's still not simple code, but it is a little more palatable than without attributes. The attribute on the command handler performs the same task: it simplifies the task of finding the event handlers for the dynamic commands. You declare that a property returns a command handler. Furthermore, the attribute declares the text for the command and its location on the main menu. Attributes declare your runtime intent. Tagging an element with an attribute indicates its use, and simplifies the task of finding that element at runtime. Without attributes you use some ad-hoc naming convention to find the types and the elements that will be used at runtime. Any naming convention is a source of human error. Tagging your intent with Attributes shifts more responsibilities from the developer to the compiler. The attributes can only be placed on a certain kind of language element. The attributes carry syntactic and semantic information. You will use reflection to create dynamic code that can be reconfigured in the field. Designing and implementing attribute classes to force developers to declare the types, methods, and properties that can be used dynamically will decrease the potential for runtime errors. That will increase your chances are creating applications that will satisfy your users. This simple sample should have shown you the idiom you need to use this same technique yourself. Excerpted and adapted from the book Effective C#, forthcoming from Addison-Wesley. Copyright 2004 by Pearson Education, Inc. All rights reserved. MICROSOFT .NET LATEST STORIES
SUBSCRIBE TO THE WORLD'S MOST POWERFUL NEWSLETTERS SUBSCRIBE TO OUR RSS FEEDS & GET YOUR SYS-CON NEWS LIVE!
|
SYS-CON FEATURED WHITEPAPERS MOST READ THIS WEEK BREAKING NEWS FROM THE WIRES
|
|||||||||||||||||||||||||||||||||||