Ok...I'm back with the final post about extension/plug-in loading in Silverlight. Well, at least I think it is the last as I have tried all obvious ways. I have previously showed how to it manually here and then how to do it "automatically" using MEF here. Both these have some upsides. The custom solution is tiny from a download perspective and offers a lot of control. MEF on the other hand automates a few pieces of the solution, still offering a lot of manual control. The download size does however grow a lot compared to the benefits gained. So, so far, I would say that the custom way is the winner. At least as long as the requirements are as simple as they are in this case...
This last post is all about doing it according to the Composite Application Guidance (CAG). CAG is a set of guidelines and patterns for building composite applications in WPF and Silverlight. The CAG is implemented by using the Composite Application Library or PRISM framework. CAG talks about how to do it, while CAL consists of assemblies helping the developer to actually do it. At least that is the way that I have understood it.
So, where do we start? Well, step number one is to get hold of the CAL assemblies. CAL is available for download here. This download will "install" a bunch of demo projects, documentation and reference libraries that helps you to get started.
After having downloaded these, it is time to open up VS2010 and keep going with the solution I started 2 posts ago. I start out by creating a new Silverlight Application project. It does not need to be hosted by a web as it will just be a container for the extensions. When the project has been created, I go ahead and delete the App.xaml file as well as the MainPage.xaml. These will not be needed. After that, I create 2 new usercontrols, one called AnalogClock and one called DigitalClock. Do you see where this is headed? :)
I copy the XAML for the controls from one of the previous implementations. If you haven't got them, they look like this
AnalogClock.xaml
<UserControl x:Class="DarksideCookie.ExtensibilityDemo.CAG.Extensions.AnalogClock"
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>
</UserControl>
DigitalClock.xaml
<UserControl x:Class="DarksideCookie.ExtensibilityDemo.CAG.Extensions.DigitalClock"
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>
</UserControl>
I then copy the functionality for the clocks from the previous implementations. However, I only copy and wire up the ControlLoaded() method and the SetText() method for the digital clock. I add no attributes or other properties to the implementation.
So how do I load the controls if there is no attribute or properties? Well...I add a new class, which I call ExtensionsModule. This will be what CAG calls a module. A module is an extension for an application. I could have put the different clocks in different modules, but for simplicity, I kept them in the same.
The ExtensionsModule class needs to implement Microsoft.Practices.Composite.Modularity.IModule. That interface is available in one of the assemblies that came with the CAL download. The assembly in question is called Microsoft.Practices.Composite.dll. So I add a reference to that assembly and then set Copy Local to false.
public class ExtensionsModule : IModule
{
}
Next, I add a constructor. The constructor takes a single parameter of type IRegionManager. This is sort of optional, but it gives the module access to the IRegionManager, making it possible to add the usercontrols to the actual UI. CAL uses Unity and dependency injection. So by just adding a parameter to the constructor of the required interface type, causes Unity to inject an implementation for us.
private readonly IRegionManager _regionManager;
public ExtensionsModule(IRegionManager regionManager)
{
this._regionManager = regionManager;
}
Next, I need to implement the IModule interface. It has a single method defined. It is called Initialize(), takes no parameters and returns void. Inside this method is where I am supposed to initialize my extensions. So what I need to do is, create an instance of the required extensions and then tell the IRegionManager to put them inside a specific region.
I will show off regions a bit more later in the post, but for now you only need to know that it is a specific area in the UI and that each region has a name. Normally you would place the names in an external library instead of having "magic strings". But for this demo I have stuck with "magic strings" as there are only 2 regions.
public void Initialize()
{
this._regionManager.Regions["LeftRegion"].Add(new AnalogClock());
this._regionManager.Regions["RightRegion"].Add(new DigitalClock());
}
That's it for the extensions. No attributes of properties. So instead of exposing the placement as an attribute or property, this is defined by the IModule.
Oh...yeah...I forgot to mention that I also use a post built event script to copy the Xap file to the correct location. I have set this up for all of the extension Xaps in the solution. I might however forgotten to mention it...it looks something like this
xcopy $(TargetDir)DarksideCookie.ExtensibilityDemo.CAG.Extensions.xap $(SolutionDir)DarksideCookie.ExtensibilityDemo.Web\ClientBin\CAGExtensions\ /y
Next, I create another Silverlight Application project. This time I link it to the existing web application, as it will actually be used directly in the browser. But before I can go any further with the implementation, I need to modify the ExtensionService webservice to support my CAG implementation as well. Due to different things, such as the way that you define a module, I can't just return a list of Xaps. I need for example to define what class implements the IModule interface. It would be nice if I didn't have to, and instead had CAL code reflect it and find it automatically. But it is what it is. So instead, I have actually hard coded the module to load and return it using a simple DTO.
[OperationContract]
public IEnumerable<ModuleDefinition> GetCAGExtensionLibraries()
{
List<ModuleDefinition> modules = new List<ModuleDefinition>();
modules.Add(new ModuleDefinition() { Name = "CAGExtensionModule", Ref = "CAGExtensions/DarksideCookie.ExtensibilityDemo.CAG.Extensions.xap", Type = "DarksideCookie.ExtensibilityDemo.CAG.Extensions.ExtensionsModule, DarksideCookie.ExtensibilityDemo.CAG.Extensions, Version=1.0.0.0" });
return modules;
}
[DataContract]
public class ModuleDefinition
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Ref { get; set; }
[DataMember]
public string Type { get; set; }
}
After I have got this modification done, I can add a reference to the service from my CAG application. After that, I need to add a bunch of references
Microsoft.Practices.Composite
Microsoft.Practices.Composite.Presentation
Microsoft.Practices.Composite.UnityExtensions
Microsoft.Practices.Composite.ServiceLocation
Microsoft.Practices.Composite.Unity
All of them are included in the CAL download.
After that, I can finally get started with the coding. I start off by creating a Bootstrapper. A BootStrapper is a class that inherits from UnityBootstrapper and is responsible for starting up the application. So I create a new class that I call...wait for it...wait for it...Bootstrapper. I set it up to inherit from UnityBootstrapper.
It needs to override two of the base class' methods, one called GetModuleCatalog() and one called CreateShell(). The CreateShell() method is basically responsible for starting up the application. It needs to create whatever root visual we want, set it as the root visual and then return it. So I create a new instance of MainPage, which was included when I created the project, set it as the current application's RootVisual and then return it.
public class Bootstrapper : UnityBootstrapper
{
...
protected override DependencyObject CreateShell()
{
MainPage mainPage = new MainPage();
Application.Current.RootVisual = mainPage;
return mainPage;
}
}
The GetModuleCatalog() is responsible for creating a catalog that will contain all of the application's modules. In our case, I need to do some "magic" with this catalog as I intend to load the modules based on information from the webservice. So I create a new ModuleCatalog, store a reference to it and return it.
ModuleCatalog _catalog = new ModuleCatalog();
protected override IModuleCatalog GetModuleCatalog()
{
return _catalog;
}
Ok...so where does the "magic"/module loading come in? Well, I have added an extra method to my Boostrapper, which I call LoadModules(). It calls the ExtensionsService webservice and uses the response populate the ModuleCatalog. For each returned module definition, I add a new module to the ModuleCatalog by calling one of its Add() method overloads passing in the name, type, ref and InitializationMode. The name and type is pretty obvious what they are, the name of the module and the type inside the module that implements the IModule interface. The InitializationMode defines when the module is initialized, which in our case is as soon as it is available. The Ref is a string Uri to where the Xap is located. When all the modules have been added, I call the UnityBootstrapper's InitializeModules() to get them initialized.
public virtual void LoadModules()
{
ExtensionService.ExtensionsServiceClient svc = new ExtensionService.ExtensionsServiceClient();
EventHandler<ExtensionService.GetCAGExtensionLibrariesCompletedEventArgs> handler = null;
handler = delegate(object sender, ExtensionService.GetCAGExtensionLibrariesCompletedEventArgs e)
{
svc.GetCAGExtensionLibrariesCompleted -= handler;
foreach (var module in e.Result)
{
_catalog.AddModule(module.Name, module.Type, module.Ref, InitializationMode.WhenAvailable);
}
InitializeModules();
};
svc.GetCAGExtensionLibrariesCompleted += handler;
svc.GetCAGExtensionLibrariesAsync();
}
Once the Bootstrapper is done, it is time to use it. So I open the App.xaml.cs file and modify the Application_Startup() method. I remove all the code inside it and insead do the following. I create a new Bootstrapper instance, call its LoadModules() method and then the Run() method. The Run() method will set everything up and start the application calling among other things GetModuleCatalog() and CreateShell().
private void Application_Startup(object sender, StartupEventArgs e)
{
Bootstrapper boot = new Bootstrapper();
boot.LoadModules();
boot.Run();
}
That is actually all that is needed to handle the loading of the modules. The only thing left to do before pressing F5 and leaning back in the chair is to get the actual UI going. So I open up MainPage.xaml and create two columns in the Grid that is already defined. I then add two ItemsControl, one in each column and with their VerticalAlignment set to Center. These will be responsible for showing the clocks. CAL uses an adapter to place the modules in the UI controls, and luckily there is already one defined for ItemsControl. But if you were using another type of panel that didn't have one, building one isn't that hard...
Next I define another namespace in the Xaml, it points to the CLR namespace Microsoft.Practices.Composite.Presentation.Regions in the Microsoft.Practices.Composite.Presentation assembly. This namespace contains the RegionManager, which defines an attached property called RegionName. So I use this to set the RegionName of the two ItemsControls. This will register them with the RegionManager, making it possible for it to pair up the regions with the modules that we load.
<UserControl x:Class="DarksideCookie.ExtensibilityDemo.CAG.Shell.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;assembly=Microsoft.Practices.Composite.Presentation">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ItemsControl cal:RegionManager.RegionName="LeftRegion" VerticalAlignment="Center" />
<ItemsControl cal:RegionManager.RegionName="RightRegion" VerticalAlignment="Center" Grid.Column="1" />
</Grid>
</UserControl>
Now...now I can just press F5 and see it all unfold before my very eyes. Not that there is a lot unfolding... It will look identical to what I have already built twice before. But this time I didn't have to take care of all the plumbing of getting the extension controls into the UI.
Just as with the MEF example, CAG doesn't really show of its big power in an example this simple, but it shows a little bit at least. You will just have to imagine the glorious power it possesses...or read a bit about it...
So...how does it stick up against the other implementations. Well, as you have already seen from the code, it makes everything very easy. And it also contains loads of plumbing for IoC through Unity and other sweet features. But, just as with MEF, it comes at a price. The shell Xap for the CAG example weighs in at 208kb, compared to MEF's 115kb and the custom implementation at 11kb. The Xap with the module is the same size as the other though...just a single kb heavier than the custom version...
So, once again I would say that I would probably build something custom for something this simple. For anything more complex I would definitely have a look at either MEF or CAG. And considering how much plumbing comes for free with CAG, I would probably go for that...Especially if I take into consideration that I generally add both Unity and CommonServiceLocator to my projects. Two things that I get for "free" when using CAG, while I would "pay for" if I added it to my custom implementation...
I hope that this series of blog posts has given you some information about how you can handle loading extensions and what different ways you can choose from. There are probably more, but these are the most common in my eyes...
As usual, the code is available here: DarksideCookie.ExtensibilityDemo.zip (354.60 kb)
Cheers!