Building an application based on discrete pieces (plug-ins) isn’t a new thing. It has been around for ages. Loads of applications support a plug-in model. So, obviously you can do the same using Silverlight. There are even a lot of different ways of doing it. They all have benefits and cool tricks up their sleeve, but they also come with bad things as well. I have decided to take a look at three of the more common ways of handling this whole thing. The first attempt I’m going to show is using the good old “I’ll do it myself” approach. I want to start out in this end, and then compare two other common approaches to the custom built one. This way I feel that you can thorough comparison.
The other two approaches I will show are Managed Extensibility Framework, commonly known as MEF, and Composite Application Guidance, also known as CAG or Prism. And to be honest, I don’t know if the third option should be CAG/Prism or CAL (Composite Application Library), but who cares. You know what I mean…
The applications will all work the same way.They will have a shell (a “base” user control) and then download two other controls from the server and add them to the UI. They will all get their list of plug-ins to load from a webservice. This should offer a good comparison between the three. The plug-ins will vary slightly in their implementation due to the way that the different technologies work, but all in all they will work the same.
So the approach I am going to show today is the “I’ll do it myself” (IDIM) one. But let’s start by looking at the plug-ins that I intend to load. They are two versions of a clock. One is an analog clock, which to be honest was one of the first things I ever saw built in Silverlight back in the day, and the other is a “digital” one. By “digital” I mean that it just writes the time to a TextBlock.
Before looking at the code, I want to mention that I am doing it in Silverlight 4 and using Visual Studio 2010. The reason for this is that I want to be able to run the latest release of MEF when I get around to that part of this series. But to enable a sweet all-in-one download, I have decided to do all the parts in Silverlight 4. I really hope that this plays nicely with CAG… :)
I start off by creating a new Silverlight application project. I chose Silverlight Application over Class Library as I want it to be packaged as a Xap file instead of just being an assembly. This will greatly reduce the size of the download if my plug-in is big and complicated. Why? Well, the Xap is zipped… (I love that expression…) Next I remove the App.xaml and MainPage.xaml files from the project as I do not need them.
The analog clock is mostly made out of Xaml. It consists of three Storyboards, one ellipse and three rectangles.
<ext:ApplicationExtension x:Class="DarksideCookie.ExtensibilityDemo.Custom.Extensions.AnalogClock"
xmlns:ext="clr-namespace:DarksideCookie.ExtensibilityDemo.Custom.Common;assembly=DarksideCookie.ExtensibilityDemo.Custom.Common"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="ControlLoaded"
Width="200" Height="200">
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<Storyboard x:Name="SecondsAnimation" Duration="0:1:0" RepeatBehavior="Forever">
<DoubleAnimation By="360" Storyboard.TargetName="SecondsAngle"
Storyboard.TargetProperty="Angle" Duration="0:1:0" />
</Storyboard>
<Storyboard x:Name="MinutesAnimation" Duration="1:0:0" RepeatBehavior="Forever">
<DoubleAnimation By="360" Storyboard.TargetName="MinutesAngle"
Storyboard.TargetProperty="Angle" Duration="1:0:0" />
</Storyboard>
<Storyboard x:Name="HoursAnimation" Duration="6:0:0" RepeatBehavior="Forever">
<DoubleAnimation By="180" Storyboard.TargetName="HoursAngle"
Storyboard.TargetProperty="Angle" Duration="12:0:0" />
</Storyboard>
</Grid.Resources>
<Canvas Width="1" Height="1">
<Ellipse Width="220" Height="220" Canvas.Left="-110" Canvas.Top="-110"
StrokeThickness="3" Stroke="DarkGray" Fill="LightGray" />
<Ellipse Width="16" Height="16" Canvas.Left="-8" Canvas.Top="-8" Fill="Black" />
<Rectangle Width="6" Height="80" Fill="Black" Canvas.Left="-3" Canvas.Top="-80"
RenderTransformOrigin=".5,1">
<Rectangle.RenderTransform>
<RotateTransform CenterX=".5" CenterY="1" x:Name="HoursAngle" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="4" Height="100" Fill="Black" Canvas.Left="-2" Canvas.Top="-100"
RenderTransformOrigin=".5,1">
<Rectangle.RenderTransform>
<RotateTransform CenterX=".5" CenterY="1" x:Name="MinutesAngle" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Width="2" Height="100" Fill="Black" Canvas.Left="-1" Canvas.Top="-100"
RenderTransformOrigin=".5,1">
<Rectangle.RenderTransform>
<RotateTransform CenterX=".5" CenterY="1" x:Name="SecondsAngle" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
</Grid>
</ext:ApplicationExtension>
As you might have notice if you actually looked at the Xaml, it hooks up the Loaded event to a handler. You might also have noticed that the base class for the control isn’t UserControl, but instead a class called ApplicationExtension. This base class will be used to identify the class as being a plug-in. But before we go into that, let’s see what is in the ControlLoaded method…
protected void ControlLoaded(object sender, RoutedEventArgs e)
{
SecondsAngle.Angle = 360 * (DateTime.Now.Second / 60f);
SecondsAnimation.Begin();
MinutesAngle.Angle = 360 * (DateTime.Now.Minute / 60f);
MinutesAnimation.Begin();
HoursAngle.Angle = 360 * (DateTime.Now.Hour / 12f);
HoursAnimation.Begin();
}
As you can see, there is nothing magical. It sets the initial angles and then starts off the animations.
Now might seem like a good idea to have a look at the ApplicationExtension class, but I won’t. I’ll instead run through the second plug-in’s Xaml and code behind and then look at the base class…
The Xaml for the digital clock is simple. It looks like this
<ext:ApplicationExtension x:Class="DarksideCookie.ExtensibilityDemo.Custom.Extensions.DigitalClock"
xmlns:ext="clr-namespace:DarksideCookie.ExtensibilityDemo.Custom.Common;assembly=DarksideCookie.ExtensibilityDemo.Custom.Common"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="ControlLoaded"
Width="200" Height="200">
<Grid x:Name="LayoutRoot">
<TextBlock FontSize="40" x:Name="ClockText" />
</Grid>
</ext:ApplicationExtension>
Once again, changed base class and a Loaded handler. The rest of the Xaml however is very simple. It is just a TextBlock… This is obviously used by the code behind, which has these two methods to support the functionality
protected void ControlLoaded(object sender, RoutedEventArgs e)
{
SetText();
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromSeconds(1);
tmr.Tick += (o, a) => { SetText(); };
tmr.Start();
}
private void SetText()
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
And yes…I run my clock as a 24h clock like all civilized people do. Come on, that whole AM/PM thing is just confusing. Even the army in countries using AM/PM use 24h times as it is less error prone… anyhow…
So, where is the magic? Well, first of all we have to look at the ApplicationExtension class. I have placed this in a separate project of type Silverlight Class Library. I have then referenced this project from the plug-in project., making sure that that the “Copy Local” property is set to false. This assembly will also be referenced in the main application, and thus already be loaded when the plug-ins are downloaded. So there is no need to add to the size of the plug-in Xap.
This project contains a single class, the ApplicationExtension class, and one enum. It looks like this
public class ApplicationExtension : UserControl
{
public virtual Location Location { get { return Location.Left; } }
}
public enum Location
{
Left,
Right
}
The ApplicationExtension class inherits from UserControl to enable the placement of it in the UI control tree. Next it exposes a single virtual property called Location. This will be used to decide where in the UI to load the control. And to make it less error prone, I have typed the Location as an enum. As you might have noticed, I have not made the class abstract, which is something that I would really have liked. That way I could have forced any inheriting classes to override the Location property. Unfortunately, Blend and the VS designer seem to have issues with none concrete base classes…
So why do I have this custom base class? Well…by having all the plug-ins inherit from this class, I can easily identify what controls are plug-ins. It also makes it possible for the plug-in to give the loading application information, such as in this case where is can tell the loading application where it should be placed.
But let’s skip ahead and have a look at the application that will actually load these plug-ins. It is a regular Silverlight Application project. I start off by referencing the class library project, giving me access to the oh so important ApplicationExtension class. Next I create a simple “shell” that can host the plug-ins. Calling a Grid with 2 columns a shell is pretty impressive, but hey… Here is the Xaml anyway
<UserControl x:Class="DarksideCookie.ExtensibilityDemo.Custom.Shell.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
</Grid>
</UserControl>
So how do I go about loading the actual plug-ins? Well, I started out by creating a static class called ExtensionManager. It exposes a single generic method called FindExtensions<T>(string[] locations). It returns an generic object I have decided to call ExtensionLocator<T>.
public static class ExtensionManager
{
public static ExtensionLocator<T> FindExtensions<T>(string[] locations)
{
return new ExtensionLocator<T>(locations);
}
}
So, as you have probably figured out by now, all the plug-in loading functionality is available through the ExtensionLocator<T> class. As you have seen previously, it takes an array of strings as a parameter to the constructor. It uses these to initiate the download of the files at the other end of the URL. It uses an ordinary WebClient to make the HTTP request.
public ExtensionLocator(string[] locations)
{
foreach (var path in locations)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += DownloadComplete;
wc.OpenReadAsync(new Uri(path,UriKind.RelativeOrAbsolute));
}
}
The handler handling OpenReadCompleted event starts by getting a list of assemblies in the XAP file. Next it runs through one of those assemblies at the time, looking at each class inside that assembly in the search for a class of type T (the type defined when requesting the ExtensionManager to find extensions). As soon as all the assemblies have been searched and all extensions found, it raises an event called ExtensionLocated for each found type, passing along an instance of the type. It looks like this
private void DownloadComplete(object sender, OpenReadCompletedEventArgs e)
{
List<string> assemblies = GetPackagedAssemblies(e.Result);
List<T> matchingTypes = GetMatchingTypes(e.Result, assemblies);
foreach (var item in matchingTypes)
{
OnExtensionLocated(item);
}
}
...
protected virtual void OnExtensionLocated(T ext)
{
if (ExtensionLocated != null)
ExtensionLocated(this, new ExtensionEventArgs<T>(ext));
}
public event EventHandler<ExtensionEventArgs<T>> ExtensionLocated;
The method implementation is quite simple, just as explained. However, you have probably noticed that all it does is call out to other methods, which aren’t include din the code snippet. Let’s look at them one at the time starting with the GetPackagedAssemblies().
It starts off by opening a stream to the application manifest, which is an Xml file containing a list of all assemblies included in the Xap. This is quite easy to do as it is always called AppManifest.xaml. It just requires one to write the following code, which might seem a little complicated, but really isn’t if you break it down into pieces...
var manifestStream = Application.GetResourceStream(
new StreamResourceInfo(fileStream, null),
new Uri("AppManifest.xaml", UriKind.Relative));
Next, I initiate a List<string> object to hold the found assemblies. After that, I create an XmlReader, passing in the stream to the manifest. But before I can start using the XmlReader, I need to know how the manifest looks. By looking in the bin\debug folder of my project, after building it once, and looking at the AppManifest.xaml file, I have found out that it looks something like this
<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
EntryPointAssembly="DarksideCookie.ExtensibilityDemo.Custom.Shell"
EntryPointType="DarksideCookie.ExtensibilityDemo.Custom.Shell.App"
RuntimeVersion="3.0.40818.0">
<Deployment.Parts>
<AssemblyPart x:Name="DarksideCookie.ExtensibilityDemo.Custom.Shell"
Source="DarksideCookie.ExtensibilityDemo.Custom.Shell.dll" />
<!-- other assemblies included in the Xap -->
</Deployment.Parts>
</Deployment>
So I need to run through the Xml and try to find any AssemblyPart elements and then look at their Source. This is not that hard with the XmlReader. I generally like the XDocument as it gives me the ability to run LINQ expression on the Xml. But in this case, I do not want to add a dependency to the System.XMl.Linq assembly to my project and thereby adding to the download size. Besides, the Xml is really simple, so an XmlReader will do fine…
List<string> packagedAssemblies = new List<string>();
XmlReader reader = XmlReader.Create(manifestStream.Stream);
while (reader.Read())
{
if (reader.IsStartElement("AssemblyPart"))
{
reader.MoveToAttribute("Source");
reader.ReadAttributeValue();
packagedAssemblies.Add(reader.Value);
}
}
return packagedAssemblies;
That was the GetPackagedAssemblies() method. The other method I left out before was the GetMatchingTypes(). It takes the list of assembly names that was retrieved from the GetPackagedAssemblies() method and looks through each one of them for classes matching the T type.
private List<T> GetMatchingTypes(Stream fileStream, List<string> assemblies)
{
List<T> matchingTypes = new List<T>();
var assemblyStream = new StreamResourceInfo(fileStream, "application/binary");
foreach (var assembly in assemblies)
{
...
}
return matchingTypes;
}
As you can see, it starts by getting a StreamResourceInfo object. This is basically a class that holds the content of a Xap and making it possible to read parts of its content. As you probably noticed, it was used in the GetPackagedAssemblies() method as well.
But what does the part of the code I left out do? Well, it uses some of the sweet reflection powers in .NET. It starts by getting a Stream to the current assembly. It then creates a new class called AssemblyPart. This can then be used to load an assembly the Stream retrieved earlier. Once I have a reference to the actual assembly, I can get hold of all the exposed types in that assembly by calling GetExportedTypes(). By looping through the returned types, I can easily figure out which once are of the type requested by using the method called IsSubclassOf().
If I find a class that is a subclass of the requested type, I use the assembly reference to create an instance of it. Finally I add it to the list of objects that is returned from the method
var si = Application.GetResourceStream(assemblyStream, new Uri(assembly, UriKind.Relative));
AssemblyPart asmPart = new AssemblyPart();
Assembly asm = asmPart.Load(si.Stream);
foreach (var type in asm.GetExportedTypes())
{
if (type.IsSubclassOf(typeof(T)))
{
matchingTypes.Add((T)asm.CreateInstance(type.FullName));
}
}
That’s it. It doesn’t get more complicated than that. I know that there are a bunch of reflection classes and methods that a lot of people haven’t seen before, but it isn’t that complicated…
Next, we need to use the ExtensionManager. In this example, I will just put the code in the codebehind of the MainPage.xaml file, as it is easier… But it could easily go in a VM.
But before we have a look at that, I just quickly want to look at something else. As you might, or might not, remember from the start of this post, I said that I would use a webservice to find out what extension packages to load. So before we look at the rest of the Silverlight stuff, let’s just quickly see what it does. I create the webservice by adding a “Silverlight enabled Webservice” to my project. This creates a all-in-one webservice prepped for Silverlight. Personally I prefer having the contract as a separate interface, but for this it works fine to have it like it is. Initially it will expose a single method that looks like this
[OperationContract]
public IEnumerable<string> GetCustomExtensionLibraries()
{
DirectoryInfo dir = new DirectoryInfo(HttpContext.Current.Server.MapPath("/ClientBin/CustomExtensions/"));
return dir.GetFiles().Select(fi => "/ClientBin/CustomExtensions/" + fi.Name);
}
As you can see, all it does is looking at all files in the /ClientBin/CustomExtensions folder and pass back the paths to the client…
So let’s return to the MainPage.xaml.cs file. After having added a reference to the newly created webservice, I modify the constructor of the codebehind. I modify it so that it instantiates a new webservice proxy, adds a handler to the GetCustomExtensionLibrariesCompleted event and then call the GetCustomExtensionLibraries() method.
public MainPage()
{
InitializeComponent();
ExtensionService.ExtensionsServiceClient svc = new ExtensionService.ExtensionsServiceClient();
svc.GetCustomExtensionLibrariesCompleted += ExtensionsReceived;
svc.GetCustomExtensionLibrariesAsync();
}
In the eventhandler ExtensionsReceived, I start off by making a quick error check. If the eventargs contains an error, I just return. A very simplistic and not very useful way to handle the error, but this is just a proof of concept, so I will keep it at that…
Next, I call the ExtensionsManager’s FindExtensions<T>() method, passing in the list of paths that was returned from the webservice. As I am looking for classes that inherit from ApplicationExtension, I set T to be of that type that… As the method call returns an ExtensionLocator object, I hook up an eventhandler to that class’ ExtentionLocated event. In the handler, I set the correct column depending on the defined location, and then add it to the LayoutRoot. Boiling all of that down to a short snippet, it looks like this
void ExtensionsReceived(object sender, ExtensionService.GetCustomExtensionLibrariesCompletedEventArgs e)
{
if (e.Error != null)
return;
ExtensionManager
.FindExtensions<ApplicationExtension>(e.Result.ToArray())
.ExtensionLocated += (s, args) => {
Grid.SetColumn(args.Extension, args.Extension.Location == Location.Left ? 0 : 1);
LayoutRoot.Children.Add(args.Extension);
};
}
That’s it! A complete plug-in loading strategy built from scratch. And yes, it lacks a lot of the finesse and functionality that you get from other frameworks, but it does actually do what I need it to in this situation. And on top of that, it is tiny, adding very little to the download for the end-user.
In the next post, I will be looking at doing the exact same thing using MEF (Managed Extensibility Framework). So stay tuned…
Download code: DarksideCookie.ExtensibilityDemo.zip (56.73 kb)