JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

Chapter 13 "Dynamic Loading with Reflection"

In Chapter two I alluded to the power of abstract classes (and interfaces). In this chapter I am going to demonstrate how to implement a plug in architecture using an interface and the power of dynamic reflection. In chapter two I mentioned four uses of abstract classes:

 
bulletDefer the Final Implementation to Another More Knowledgeable Class
bulletFormally Hide or Encapsulate the Implementation for Flexibility 
bulletAllow Runtime Variation in Implementation (Polymorphism)
bulletMark Thyself

In this chapter I am going to demonstrate the use of encapsulation and polymorphism to allow the dynamic loading of a "plug in" class at runtime. The loading of a third  party plug in class at runtime is useful when you want to add new functionality to an existing program. Since the application does not know the name of the plug in class at compile time, the application must "discover" the new class at runtime.  Runtime discovery is done using reflection. The framework supports discovery with classes in the System.Reflection.Assembly and System.Type namespaces. In this tutorial you will use the method Assembly.Load to dynamically load a class at runtime.

The application must also create an instance of the new class at runtime using System.Activator. In this tutorial you will use the method Activator.CreateInstance to create an instance of the dynamically loaded class. As long as the plug in class implements the expected abstract method or interface, say IDrawable.DrawYourself(), then the new functionality can be invoked at runtime through the magic of polymorphism! Unfortunately, implementing this solution is not simple, so I will be your guide.

Define the Interface

Here is a slightly altered version of the familiar IDrawable interface that the plug in must implement. 

public interface IDrawable
{
	void DrawYourself(Graphics g, Point p, int size);
}

You program to the public or outside view as defined in IDrawable. The actual implementation details are hidden or encapsulated by the concrete classes in the plug in. The concrete classes must implement the IDrawable interface. In a sense, the IDrawable interface is a contract between the calling application and the classes in the plug in.

Creating the Projects

In this hands on tutorial, you will create three projects: DrawDemo, MyInterface and DrawPlugIn. Separating the IDrawable interface into a separate project, MyInterface, simplifies deployment and versioning. More importantly, if you define the interface IDrawable in the main project DrawDemo, Visual Studio will not let you add a reference to the DrawDemo.exe in the plug in project DrawPlugIn. So, you should define the IDrawable interface in a separate project so that it only exist in "a single assembly in memory." In other words, if two assemblies contain the same class with the same fully qualified name, the runtime still sees the two classes as different types. In a sense, I conceptualize this as having the runtime "prepend" the assembly "name" to the fully qualified class name. Got that? Check here for more information about InvalidCastExceptions and plug ins, compliments of Jon Skeet: http://www.yoda.arachsys.com/csharp/plugin.html

Go ahead and create three projects. The main project DrawDemo is a Windows Form Application. The plug in and utility projects, DrawPlugIn and MyInterface should be created as class libraries. The DrawDemo project will generate an .exe file. The plug in and utility interface projects will generate .dll files. 

MyInterface.IDrawable

This is simply a utility project that defines the IDrawable interface. You should create this project as a class library project that will compile to MyInterface.dll. Since the IDrawable interface takes a parameter of the System.Drawing.Graphics type, you must add a reference to the System.Drawing.dll to compile the project. You can do this from Project--> Add reference... --> .NET. Here is the source code for the utility project:

using System;
using System.Drawing;
// compile as a class library
// creates a dll as output
// this project references the System.Drawing.dll
namespace MyInterface
{
	public interface IDrawable
	{
		void DrawYourself(Graphics g, Point p, int size);
	}
}

Select a release build and compile the project. You set a release build from Build --> Configuration Manager --> Release. Now copy the MyInterface.dll from the bin/Release folder for later use.

DrawPlugIn.Triangle

This project creates a plug in that implements the IDrawable interface by defining a new Triangle class. This new concrete class adds new functionality to the drawing program. Again, create this project as a class library that will compile to DrawPlugIn.dll. You must add a reference to the System.Drawing.dll to access the Graphics class. Now the confusing part is that you must also add a reference to the MyInterface.dll that you created in the first project. Here is the source code for DrawPlugIn:

using System;
using System.Drawing; // project references System.Drawing.dll
using MyInterface; // project references MyInterface.dll
namespace DrawPlugIn
{
	public class Triangle : IDrawable 
	{
		public void DrawYourself(Graphics g, Point p, int size){
			Console.WriteLine("Triangle");
		}
	}
}

Select a release build and compile the plug in to DrawPlugIn.dll. Copy the DrawPlugIn.dll from the bin/Release folder for later use.

DrawDemo.Form1

OK. Now for the fun part. Create a third project called DrawDemo. This should be a Windows Form application that will compile to DrawDemo.exe. Add a reference to MyInterface.dll and System.Drawing.dll. This allows you to call methods in the namespace MyInterface and System.Drawing. You must now copy the DrawPlugin.dll file to a "plugins" file in the AppDomain BaseDirectory folder. You can determine the BaseDirectory using code like this:

public static void Main() 
{
	AppDomain currentDomain = AppDomain.CurrentDomain;
	Console.WriteLine(currentDomain.BaseDirectory);
	Application.Run(new Form1());
}

In debug mode you should add the DrawPlugIn.dll to "bin/Debug/plugins".

Just for fun, you can now add two standard classes Circle and Square to the DrawDemo project. Here again is the now familiar IDrawable code modified for graphics output:

	public class Circle : MyInterface.IDrawable 
	{
		public void DrawYourself(Graphics g, Point p, int size){
			Console.WriteLine("Circle");
		}
	}
	public class Square : MyInterface.IDrawable 
	{
		public void DrawYourself(Graphics g, Point p, int size) {
			Console.WriteLine("Square");
		}
	}

These classes are loaded statically at compile time. Here is the complete code that creates an array of IDrawable objects at runtime.

/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
private ArrayList drawableList= new ArrayList();
public Form1()
{
	//
	// Required for Windows Form Designer support
	//
	InitializeComponent();
	//
	// TODO: Add any constructor code after InitializeComponent call
	//
	// thanks to Jon Skeet for his suggestions
	// add statically compiled objects
	drawableList.Add(new Circle());
	drawableList.Add(new Square());
	// now dynamically load class DrawPlugIn.Triangle which
	// implements interface MyInterface.IDrawable
	Assembly assembly= null;
	try 
	{        
		// tell app where to look for plugins
		AppDomain.CurrentDomain.AppendPrivatePath("plugins");
		// get absolute path to our private assemblies
		string path= AppDomain.CurrentDomain.BaseDirectory+"plugins";
				
		// create plugins folder if one does not exist
		DirectoryInfo info= new DirectoryInfo(path);
		if (!info.Exists){ info.Create();}
		// discover all dlls in plugins folder
		string[] dir= Directory.GetFiles(path,"*.dll");
		// iterate over files in folder plugins
		foreach (string s in dir)
		{
			string dll= Path.GetFileNameWithoutExtension(s);
			assembly= Assembly.Load(dll);  // in folder plugins
			//Type[] types= assembly.GetTypes();
			Type[] types= assembly.GetExportedTypes(); //safer
			foreach(Type t in types) 
			{
				Console.WriteLine("Type: {0}",t);
				try 			
				{
					// only load if implements IDrawable
					// use fully qualified name!
					//if (t.GetInterface("MyInterface.IDrawable")!= null) 
					if (typeof(IDrawable).IsAssignableFrom(t) // safer call
							&& !t.IsAbstract) // even safer call ) 
					{
						// dynamically load this class
						object obj= Activator.CreateInstance(t);
						drawableList.Add(obj); // no need to cast
					}
				}
				catch(Exception e) 
				{
					Console.WriteLine(e);
				}
			}
				}
	}
	catch(Exception e) 
	{
		Console.WriteLine(e);
	}
			
	// exercse our arraylist
	foreach (IDrawable d in drawableList)
	{
		d.DrawYourself(null,new Point(0,0),0);  // throws exception if not IDrawable
	}
}

Here are the using statements:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Reflection;
using System.IO; // FileNotFoundException
using MyInterface; // this project references MyInterface.dll

Let's look at this code in detail. You first create an ArrayList to hold the IDrawable objects. The statically created objects are instantiated with a call to new and added to the arraylist:

	// create an arraylist
	private ArrayList drawableList= new ArrayList();

	// add statically compiled objects in the Form1 constructor
	drawableList.Add(new Circle());
	drawableList.Add(new Square));

Before you can load the plug in class Triangle, you must dynamically load the DrawPlugIn assembly. You do this with a call to Assembly.Load.

Note: This code uses dynamic discovery of the plug ins in a "plugins" folder. The AppendPrivatePath method has been deprecated in the .NET 2.0 API.

To support dynamic discovery of the dlls in the "plugins" folder you add the "plugins" folder to the domain path like this:

	// tell app where to look for plugins
	AppDomain.CurrentDomain.AppendPrivatePath("plugins");

The "plugins" folder normally is at the same level as the DrawDemo.exe file. The next step is to create a a path to the "plugins" folder. As an added touch, the code will then create an empty "plugins" folder if one does not already exist!

	// get absolute path to our private assemblies
	string path= AppDomain.CurrentDomain.BaseDirectory+"plugins";

	// create plugins folder if one does not exist
	DirectoryInfo info= new DirectoryInfo(path);
	if (!info.Exists){info.Create();}

 You can now dynamically discover all of the existing dlls in the "plugins" folder with a call to GetFiles :

	// discover all dlls in plugins folder
	string[] dir= Directory.GetFiles(path,"*.dll");

Using this array of files, you iterate over the array and load any dlls into memory:

	// iterate over files in folder plugins
	foreach (string s in dir)
	{
		string dll= Path.GetFileNameWithoutExtension(s);
		assembly= Assembly.Load(dll);  // in folder plugins
		...
	}

You dynamically create the IDrawable classes in each plug in assembly using Activator.CreateInstance. The objects are then added to the arraylist thusly:

	assembly= Assembly.Load(dll);  // in folder plugins
	//Type[] types= assembly.GetTypes();
	Type[] types= assembly.GetExportedTypes(); //safer
	foreach(Type t in types) 
	{
		Console.WriteLine("Type: {0}",t);
		try 			
		{
			// only load if implements MyInterface.IDrawable
			// use fully qualified name!			
			//if (t.GetInterface("MyInterface.IDrawable")!= null)// or use
			if (typeof(IDrawable).IsAssignableFrom(t) // safer call
				&& !t.IsAbstract) // even safer call 
			{
				// dynamically load this class
				object obj= Activator.CreateInstance(t);
				drawableList.Add(obj); // no need to cast
			}
		}
		catch(Exception e) 
		{
			Console.WriteLine(e);
		}
	}

Finally, you can iterate over the array list calling the DrawYourself method:

	foreach (IDrawable id in drawableList)
	{
		id.DrawYourself(null,new Point(0,0),0);
	}

Through the magic of polymorphism the proper implementation is called at runtime. Here is the output in the Debug console after the application closes:

Type: DrawPlugIn.Triangle
Circle
Square
Triangle
The program '[3888] DrawDemo.exe' has exited with code 0 (0x0).

Security

I should mention that loading a third party plug in at runtime exposes your application to security risks. The subject of security for "untrusted" plug ins is beyond the scope of this tutorial.

Note: The .NET Framework v2.0 supports a security sandbox that will let you load plug-ins into a low trust app domain!

Congratulations. If you were able to compile and execute this application, you have demonstrated the power of using reflection, encapsulation and polymorphism to dynamically load and call a concrete class method at runtime.


App.Config

Accoring to Benny R. the following code in app.config provides the equivalent functionality to AppendPrivatePath in .Net 2.0.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <runtime>
     <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
       <probing privatePath="Plugins"/>
     </assemblyBinding>
   </runtime>
</configuration>

A Programming Challenge

A real world drawing program should update the menu bar to reflect the dynamically loaded types and draw the selected items on mouse up. I have written the IBasicShape interface with an eye to graphic output and tested the interface in the OnPaint method:

		protected override void OnPaint(PaintEventArgs e) 
		{
			Graphics g= e.Graphics;
			int x=0;
			int y=0;
			int position= 20;
			foreach (IBasicShape ibs in basicShapeList) 
			{
				ibs.Size= 20;
				ibs.Position= new Point(x,y);
				ibs.DrawYourself(g);
				y += position;
			}
		}

Your code would need to pass the x and y coordinates of the cursor on mouse up to the concrete basic shape before calling DrawYourself.

Learn More

MSDN: Efficient Reflection

Have fun,
Jeff

Send mail to [email protected] with questions or comments about this web site. Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 © 
Last modified: 08/04/09
Hosted by www.Geocities.ws

1