One popular feature in the widely used Prism Framework is the concept of "modules." A prism module is a functional unit that represents a set of related concerns. These concerns can be coded and tested independently and then composed into larger applications. They can also handle dependencies with other modules and be discovered and dynamically loaded.
While the concept worked well for WPF and prior versions of Silverlight, Jounce takes a slightly different approach that recognizes and uses two features that are built into Silverlight. The first is the concept of a XAP file, which is nothing more than a ZIP file with a manifest that lists resources and contains the resources themselves (typically DLLs, images, embedded fonts and data). Jounce treats XAP files as the perfect medium to encapsulate a module within Silverlight. The second feature is the MEF DeploymentCatalog
which allows Silverlight to dynamically load and compose parts from a XAP file.
Creating a Module
To create a module with Jounce is incredibly simple: fire up Visual Studio and create a new Silverlight Application. That is a module!
There will always be exactly one module that is a "host" module. This module must contain the App.xaml.cs
code and the initial reference to the Jounce ApplicationService
in order to start the application and initialize the Jounce framework. Additional modules may be standalone. For those modules, you can simply create a new application and then delete the App.xaml
and MainPage.xaml
files that are auto-generated by the template. Instead, you can begin defining views, view models, or other class that you simply export. When the module is loaded by Jounce, these will be automatically imported and composed into the main application.
An example of this is in the Jounce "DynamicXapCalculator" quickstart. The main module is a typical Silverlight application. It defines a display that contains a row to enter digits, a row of "commands" and an "equals" row. It is designed to function like a calculator, but the buttons are not pre-defined. This is the XAML for the button row:
<ItemsControl Grid.Row="2" HorizontalAlignment="Center" ItemsSource="{Binding Commands}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Button Width="50" Height="50" Margin="5" Content="{Binding Item1}" Command="{Binding Item2}"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
A quick peek at the view model demonstrates how the Managed Extensibility Framework (MEF) is used to compose the commands. The definition is a list of tuples that represent the display and an ICommand
implementation:
[ImportMany(AllowRecomposition = true)] public ObservableCollection<Tuple<string,ICommand>> Commands { get; set; }
The view model itself exports two implementations of this construct: one for addition, and one for subtraction.
[Export] public Tuple<string,ICommand> AddCommand { get { return Tuple.Create("+", (ICommand)new ActionCommand<object>( obj => ExecuteCommand((oldValue, newValue) => oldValue + newValue))); } } [Export] public Tuple<string, ICommand> SubstractCommand { get { return Tuple.Create("-", (ICommand)new ActionCommand<object>( obj => ExecuteCommand((oldValue, newValue) => oldValue - newValue))); } }
The view model itself keeps tracks of values, so the commands simply access those existing values and manipulate those. When you run the program, you are presented with a calculator that you can use to add and subtract.
The idea is that the calculator can be expanded by modules. Other modules might address scientific notation, exponents, factorials, trigonometry functions, or other areas that can be independently built and tested, then merged into the application. For our simple example, the external module supplies code for division and multiplication.
As I wrote earlier, creating the new module (the poorly spelled DyanmicXapCalculatorAdvanced
) started with a new Silverlight application. The App.xaml
and MainPage.xaml
files were deleted and a new class added to export the new commands.
public class AdvancedFunctions { [Import] public Action<Func<int, int, int>> ExecuteCommandImport { get; set; } [Export] public Tuple<string, ICommand> AddCommand { get { return Tuple.Create("*", (ICommand)new ActionCommand<object>( obj => ExecuteCommandImport((oldValue, newValue) => oldValue * newValue))); } } [Export] public Tuple<string, ICommand> SubstractCommand { get { return Tuple.Create("/", (ICommand)new ActionCommand<object>( obj => ExecuteCommandImport((oldValue, newValue) => newValue == 0 ? 0 : oldValue / newValue))); } } }
Notice that the actual "execution" structure that references the values the view model is holding can be imported as a generic action in this module that the exported commands call. This is "bound" by the export in the original module, which exports the method that handles the command executions:
[Export] public Action<Func<int,int,int>> ExecuteCommandExport { get { return ExecuteCommand; } }
The syntax above works because ExecuteCommand
has the signature specified by the action being exported.
Dynamically Loading Modules
The calculator must have some way to bring in the dynamic module. In this case, it will do so directly. It does this by exposing a load button wired to a load command that indicates the user is ready to bring in the additional functionality. There are two simple steps to load a dynamic module in Jounce. First, reference the IDeploymentService
:
[Import] public IDeploymentService Deployment { get; set; }
Second, call the RequestXap
method and pass the URI of the XAP resource. In the example, it is contained in the same ClientBin directory as the host application, so we can use a relative reference. The method has two overrides. One is a "blind" call or "fire and forget." The second will call a delegate once the module is loaded and pass in an Exception
object in case something went wrong. For this example, any errors are ignored. If it succeeds, a loaded flag is set and the command for loading notified, so it can be disabled to prevent the user from trying to load it again (this would just be redundant: Jounce keeps track of dynamic modules and won't try to load the same module more than once).
private void _Load() { Deployment.RequestXap("DyanmicXapCalculatorAdvanced.xap",ex=> { if (ex == null) return; _loaded = true; ((IActionCommand)LoadCommand).RaiseCanExecuteChanged(); }); }
That's all there is to it. Run the application and click the load button. You will see that the new operators instantly appear and allow you to perform division and multiplication as well as addition and subtraction.
Routed Modules
Of course, explicitly loading modules isn't required. It may be that the module load is triggered as a part of navigation. In that case, you can simply route the views that a module contains to the XAP file for the module, and Jounce will automatically load the dynamic module when it is required. An example of this is in the RegionManagement
quick start. This quickstart illustrates how to use Jounce to wire views into regions. In the code-behind for the Square
view, you'll see an exported route that maps the view tagged "Dynamic" with the module "RegionManagementDynamic.xap":
[Export] public ViewXapRoute DynamicRoute { get { return ViewXapRoute.Create("Dynamic", "RegionManagementDynamic.xap"); } }
The view that is tagged "Dynamic" actually exists in a completely separate XAP file (RegionManagementDynamic.xap
). You can also create routes on the fly at runtime. This example will do the same thing:
[Import] public ViewRouter ViewRouter { get; set; } public ConfigureRoute() { ViewRouter.RouteViewInXap("Dynamic", "RegionManagementDynamic.xap"); }
When the navigation event for the view is fired, Jounce will automatically determine whether the module has been loaded and, if not, will fetch it dynamically. This happens in the click event for the dynamic button:
private void ButtonDynamic_Click(object sender, System.Windows.RoutedEventArgs e) { EventAggregator.Publish(new ViewNavigationArgs("Dynamic")); }
Module Initialization
When Jounce loads a dynamic module, it passes it to a special MEF catalog known as a DeploymentCatalog. MEF processes the parts in the module and recomposes any imports that can take advantage of the new exports. In the calculator example, MEF will take the command export from the host application and inject it in the advanced functions type, then find the command exports and bring those back into the calculator view model.
When you run the region management example, you'll notice after clicking the dynamic button, a message pops up indicating the module has loaded. This is done in the dynamic module using the module initializer. Any module in Jounce can provide code to be called when new modules are loaded. To take advantage of this, simply implement IModuleInitializer
in one or more of your classes and export it. The module initializer may be used in the main module as well, and will be called after all of the composition is completed after the application first starts.
The interface declares a property to track if the module is initialized, and a method to call for initialization. Typically, you would implement the method and set the initialized flag to "true." If you want your module to be notified whenever any new module is loaded, however, you can leave the flag unset and it will be called each time until you set the flag. This is useful if your module must inspect certain properties or fields and respond when plug-ins or other extensions are loaded.
The implementation in the region management example is simple:
[Export(typeof(IModuleInitializer))] public class ModuleInit : IModuleInitializer { public bool Initialized { get; set; } public void Initialize() { JounceHelper.ExecuteOnUI(()=>MessageBox.Show("Dynamic module loaded.")); Initialized = true; } }
As you can see, it simply displays a message and then sets the flag.
Summary
The Prism concept of modules is extremely powerful and provides multiple means to configure and discover modules, as well as multiple methods to load them. You can read the Prism documentation about modules here. Jounce takes a more simple and native Silverlight approach by encapsulating modules in XAP files and using the deployment service to load the modules. This is intended to make it quick and easy, but provide powerful flexibility. For example, you could easily expose a web service that provides names for plug-ins and then use the deployment service to dynamically load them, or scan a directory for plug-ins on the server, inject the names in the initParams
collection and create routes at runtime to allow the user to select and load modules. Providing any "bootstrap" code you need for your module is as easy as implementing the IModuleInitializer
interface and exporting it.