Introduction
Lot of WPF UserControls are nowadays available through third parties, either for free or based on a fee. This article explains ways to integrate those third party user control plug-ins into the current user application.
Background
Before going to the code details, let's see the exact requirements of the problem we are going to solve. We have two plug-ins: UI.dll, ImageViewer.exe, which are stored in the plug-in store called "ExtPlugins".
- UI.dll - shows some first name and last name strings
- ImageViewer.exe - shows an image, and the path is the same as the ExtPlugins folder
The current application has to do two things:
- Read the User Control from the external module
- Load the appropriate data used by the User Control
Let's explore these two steps in more detail:
- Read the User Control from the external module: For this, we need to know the exact name of the User Control that resides in the external plug-in. Because, there may be many User Controls that exist in the module. We need to know which one we are interested in.
- Load the appropriate data used by the User Control.
The second requirement is where things get much more interesting. The first question is how the user control expects the data, i.e., in which format. Of course, we know that the external User Control has some kind of data -binding mechanism already defined.
Let's take one of the external plug-ins we have here: "ImageViewer". The documentation of the module says that the current image path should be set through the property. There is a property "CurrentImagePath
" available in the UserControl, and when we set this property, the UI syncs and displays the appropriate image in the control.
Now, take another external plug-in we have here: "UI". This module does not have any setter property like for the first one. The User Control does not have properties like "FirstName", "LastName" defined within the external module. Rather it is data bound to an object with two properties defined for "FirstName", "LastName". How do we solve this problem?
Approach (1): Using our knowledge of the external plug-in. In the user application, define a custom class with two properties. This needs apriori knowledge about the plug-in during coding time. The advantage of this approach is an easy to define class and structure during the coding stage of the user application. The disadvantage is the user needs to change the C# code when the plug-in requirements change; e.g., if the external plug-in adds another property, then we need to change the C# code.
Approach (2): Using the C# Reflection emit feature. Here, we read the external plug-in requirements (the best way is to use XML syntax to define the requirements), and then create a typebuilder on the fly and add properties to it with the property builder. The advantage is easy adaptability with the new requirements change in the external plug-in. We are not changing the C# code for the new properties added or deleted. And it is extensible to load different types of plug-ins. The only thing is you need a mechanism to describe the plug-in requirements. Disadvantage: a little extra code for using Reflection techniques.
Now, let's dive into the C# code.
Using the Code
This the code-behind file for the main window:
public Window1()
{
InitializeComponent();
mainframe = new Mainframe();
mainframe.LoadPlugins();
AddPluginNames();
}
During main-window startup, we load the plug-ins and read the required User Controls and their corresponding data:
private void btnShow_Click(object sender, RoutedEventArgs e)
{
this.stkCanvas.Children.Add(PluginControl);
this.DataContext = PluginData;
}
In the main window, we handle the button click event and add the User Ccontrol dynamically:
UserControl LoadUserControlPlugin(string plguinRelativePath)
{
UserControl myControl = null;
try
{
Assembly asm = Assembly.LoadFile(plguinRelativePath);
Type[] tlist = asm.GetTypes();
foreach (Type t in tlist)
{
if(t.BaseType == typeof(UserControl))
{
myControl = Activator.CreateInstance(t) as UserControl;
break; }
}
}
catch (Exception ex) { myControl = null; }
return myControl;
}
This is a very straightforward approach in C# to load a library, read the types, and create an instance of a type. Here, we are interested in the User Control type. Reading the User Control is kind of a crude technique, but I have only shown this for example purposes. If the user has the exact name of the User Control he is interested in, then he can use that name to directly read a particular User Control.
namespace Main
{
class MyModel
{
public object InitializePluginData()
{
ModuleBuilder modBuilder = CreateModule("PDAssembly", "PD");
TypeBuilder tb = modBuilder.DefineType("PDT", TypeAttributes.Public);
CreatePluginDataObject(tb);
Type t = tb.CreateType();
object obj = Activator.CreateInstance(t);
t.GetProperty("FirstName").SetValue(obj, "Adam", new object[0]);
t.GetProperty("LastName").SetValue(obj, "Smith", new object[0]);
return obj;
}
public void CreatePluginDataObject(TypeBuilder tb)
{
CreateProperty<string>(tb, "FirstName");
CreateProperty<string>(tb, "LastName");
}
public ModuleBuilder CreateModule(string nameAssembly, string nameModule)
{
AppDomain assdomain = AppDomain.CurrentDomain;
AssemblyName assName = new AssemblyName(nameAssembly);
AssemblyBuilder assBuilder =
assdomain.DefineDynamicAssembly(assName,
AssemblyBuilderAccess.RunAndSave);
string nameDll = nameModule + ".dll";
ModuleBuilder modBuilder =
assBuilder.DefineDynamicModule(nameModule, nameDll);
return modBuilder;
}
public void CreateProperty <proptype>(TypeBuilder tb,
string propName, Type propType)
{
string filedName = "_" + propName;
FieldBuilder fdBuilder = tb.DefineField(filedName,
typeof(propType), FieldAttributes.Private);
PropertyBuilder prBuilder = tb.DefineProperty(propName,
PropertyAttributes.None, typeof(propType), new Type[0]);
MethodBuilder getter = tb.DefineMethod(
"get", MethodAttributes.Public |
MethodAttributes.SpecialName,
typeof(propType), new Type[0]);
ILGenerator getGen = getter.GetILGenerator();
getGen.Emit(OpCodes.Ldarg_0);
getGen.Emit(OpCodes.Ldfld, fdBuilder);
getGen.Emit(OpCodes.Ret);
MethodBuilder setter = tb.DefineMethod(
"set",
MethodAttributes.Assembly |
MethodAttributes.SpecialName,
null, new Type[] { typeof(propType) });
ILGenerator setGen = setter.GetILGenerator();
setGen.Emit(OpCodes.Ldarg_0);
setGen.Emit(OpCodes.Ldarg_1);
setGen.Emit(OpCodes.Stfld, fdBuilder);
setGen.Emit(OpCodes.Ret);
prBuilder.SetGetMethod(getter);
prBuilder.SetSetMethod(setter);
}
}
}
Here, I used the Reflection emit technique to create the appropriate plug-in data object dynamically. I used the TypeBuilder, PropertyBuilder to assign the required properties to a class. Another point here is CreateProperty()
is a generic method and can create different types of properties.
public void LoadPluginA(string name)
{
object myData = null;
UserControl myControl;
MyModel mymodel = new MyModel();
myData = mymodel.InitializePluginData();
_dictPluginData.Add(name, myData);
string fullpath = Path.GetFullPath(@"../../../ExtPlugin/UI.dll");
myControl = LoadUserControlPlugin(fullpath);
_dictUserControl.Add(name, myControl);
}
public void LoadPluginB(string name)
{
UserControl myControl;
string fullpath = Path.GetFullPath(
@"../../../ExtPlugin/ImageViewer.exe");
myControl = LoadUserControlPlugin(fullpath);
_dictUserControl.Add(name, myControl);
string pathImage = Path.GetFullPath(@"../../../ExtPlugin/" +
@"funny-pictures-cat-plans-your-doom.jpg");
if (myControl != null)
{
Type t2 = myControl.GetType();
t2.GetProperty("CurrentImagePath").SetValue(
myControl, pathImage, null);
}
_dictPluginData.Add(name, null);
}
Here, the basic approach is similar. First, get the User Control type and then set the data it is expecting. In LoadPluginB
, we use the GetProperty
, SerValue
functions to set the appropriate data to the property in the external plug-in object. In LoadPluginA
, we use Reflection.Emit
to create a custom object to pass to the User Control. The interesting point is the User Control datacontext is expecting the object type to be set.
this.DataContext = PluginData
The WPF framework provides us more flexibility to solve interesting problems.
Points of Interest
Setting data to an external User Control is an interesting topic, and there are many ways of solving this problem. Here, we looked at two approaches:
- Using the knowledge about the external plug-in. In the user main application, define a custom class.
- Using C# Reflection, we create a generic class to create classes depending on the plug-in requirements.
In my opinion the second approach is more elagant interms of maintainabilty of plugins in large projects. And also it is more scalable with requirmets of plugins.
References
The ImageViewer plug-in was downloaded from this website, and the plug-in module belongs to the author: http://mel-green.com/2009/09/wpf-simple-vista-image-viewer/.
History
- 02/17/2010 - Initial version.