Introduction
Nowadays, the idea of pluggable controls does not seem new, but back in 2002, it did. At that time, one of my students (Denis Soroka), after learning some basics of .NET Framework (God save Anders Hejlsberg and his amazing skill of creating wonders), quickly realized the advantages of using plug-ins. By the way, I am not going nuts with hypocycloids (some may think I am). This is a different article (pure coincidence).
Background
Imagine a truck delivering a bunch of pluggable DLLs and unloading them into a folder specified by your application. Let the folder name be Plugins. Then, imagine a FileSystemWatcher
object monitoring the activity inside this folder and notifying your app about all the changes. Lastly, imagine an algorithm that distinguishes proper (or desired) DLLs from others (foreign ones) and turning in the desired functionality, i.e., creating unknown objects with predefined behaviour and launching its methods and property-form at run time.
This scenario was implemented by Denis Soroka in no time (based on my task of creating several controls using the Graph
class developed earlier). Here, I am going to discuss the application (now used in one of the C# courses) and I will try to explain the technique of launching unknown objects and making them work (do the job you want them to do) at run time.
Using the Code
Launch the program. Try to click the lines in a list on the right. Then, without stopping the program, move all (or any) of the DLLs (you will find in a folder Plugins) to some other place in your file system. Watch the behaviour of the program. Bring the DLLs back to the Plugins folder. Now, you know what I want to discuss.
Solution Structure
Let the predefined (stipulated) behaviour of the would be plugged-in modules be described in an IPlottable
interface. It serves to define the rules of the game, that is: showing different plots using the Graph
class. It was written in 1987 as an exercise in C++. Now, it is revived as a simple C# class.
public interface IPlottable
{
PointF[] GetPoints();
void ShowProperty();
void DestroyProperty();
string Name { get; }
string Caption { get; }
string Unit { get; }
event Action ChangeProperty;
}
These requirements seem enough to render a one-curve chart. This code should be placed in a separate project of type Class Library so that any other project in our solution might get the reference to the IPlottable
interface and thus obey the rules of the game.
public class Aperiodic : IPlottable
{
}
The logic of observing the Plugins folder and interacting with the newcomers should be implemented in a main project. Let it reside in the following class:
public partial class FormMain : Form
{
string pluginPath, pluginName;
FileSystemWatcher watcher;
Dictionary<string, Type> plots;
IPlottable plot;
public FormMain()
{
pluginPath = FindFolder("Plugins");
plots = new Dictionary<string, Type>();
InitializeComponent();
}
}
Let's see the code of a method that discerns the new DLLs (plug-ins) in a folder being watched by FileSystemWatcher
. Here, plot
is currently the active plug-in. listDll
is a ListBox
to show the names of all plug-ins found in a folder, and plots
is a collection to remember the would be plugged objects. The latter stores the pairs of type <string, Type>
.
void FillDllList()
{
if (plot != null)
plot.DestroyProperty();
pluginName = null;
listDll.BeginUpdate();
listDll.Items.Clear();
plots.Clear();
foreach (string file in Directory.GetFiles(pluginPath))
{
if (Path.GetExtension(file) != ".dll")
continue;
Assembly a = Assembly.LoadFrom(file);
foreach (Type type in a.GetTypes())
{
if (type.GetInterface("IPlottable") != null)
{
plots.Add(type.Name, type);
listDll.Items.Add(type.Name);
}
}
}
ResetPlot();
listDll.EndUpdate();
}
The things to pay attention to are:
Assembly.LoadFrom(file)
a.GetTypes()
type.GetInterface("IPlottable")
They are examples of how to use Reflection (the famous .NET Framework instrument to investigate the unknown reality).
Note that you may use another (or rather additional) indicator to distinguish a proper DLL from others. Besides testing for the presence of the IPlottable
interface, you may test one (or many) Custom Attributes that a DLL provider may have set to its plug-in.
It may be done in (with the code of) the file AssemblyInfo.cs. You have such a file in any new .NET project but probably never use it. I offer my apology to those who knew this feature and frequently used it. I often delete the file AssemblyInfo.cs as it is never used. Here is an example of testing for Custom Attributes. This fragment might be inserted into the FillDllList
method.
object[] attr = a.GetCustomAttributes(true);
if (attr.Length < 13)
continue;
string descr = null;
foreach (object o in attr)
{
AssemblyDescriptionAttribute da = o as AssemblyDescriptionAttribute;
if (da != null)
{
descr = da.Description;
break;
}
}
if (descr == null || descr != "PlotPlugin") continue;
Well, speaking of the FileSystemWatcher
object, there is not much to say. The logic is straightforward.
void SetWatcher()
{
watcher = new FileSystemWatcher
{
Path = pluginPath,
EnableRaisingEvents = true,
SynchronizingObject = this
};
watcher.Changed += OnWatcherChanged;
watcher.Created += OnWatcherChanged;
watcher.Deleted += OnWatcherChanged;
watcher.Renamed += OnWatcherChanged;
}
We shortcut all the events generated by FileSystemWatcher
to the only method OnWatcherChanged
. The important setting here is: watcher.SynchronizingObject = this
. It will help you to debug your project so that you don't have to use the Invoke/BeginInvoke scenario.
I believe you will find and read more about COM-objects related whims and troubles (implicitly invoked threads etc.) here in the huge CodeProject library.
Points of interest
I want you to pay attention to the way you start the unknown object. This is accomplished with the following code fragment:
Type type = plots[pluginName];
if (plot != null)
plot.DestroyProperty();
plot = type.GetConstructor(Type.EmptyTypes).Invoke(null) as IPlottable;
plotControl.SetGraph(plot);
plot.ShowProperty();
plot.ChangeProperty += delegate() { plotControl.SetGraph(plot); };
Again, the trick is in the code that uses Reflection:
plot = type.GetConstructor(Type.EmptyTypes).Invoke(null) as IPlottable;
I wish you good luck and happy coding!