Tuesday, May 27, 2008

WPF Composite Application with Caliburn - Part One

This is the first part of the serie I'll be posting about how to create a composite application with WPF. Along the way, we'll use best practices and frameworks such as :

  • Caliburn for WPF Composite Application
  • Castle Windsor container (for Dependency Injection)
I'll use a Customer - Order model and implement a domain model with LINQ To SQL framework and try to show as much detail as I can. This may sound boring to some experts but I think it'd be usable for others with lower level of knowledge and expertise.

So here we go...

Project Structure
Let's create a basic project structure with a shell (WPF Application) and two business module. I also add a project to contain our Domain model.

Configuring Caliburn
Before we discuss how to create our shell, let's talk about Caliburn's configuration. There are components that needs to be configured before running a Caliburn app. Fortunately, the framework comes with a default configuration and component implementations, but of course we can provide custom implementations.

First we need to choose an IoC Container to use. Caliburn can work with various DI containers like Windsor, Structure Map and Spring.NET. You can setup the container in the configuration, as easily as :

public CaliburnConfiguration() : this(new WindsorContainer(new XmlInterpreter("path to your windsor.config")))

Component registration can be done both in code and in container's configuration file. Here's a list of components that we need to configure, and a brief description of what each one does :

  • EventRegistrationFacility : Setups up the Event Brokering facility.
  • IPlatformCapabilities : Provides functions like MessageBox and system dialogs.
  • IViewManager : manages action messages between the View and Presenter.
  • IPresenterManager : manages binding information between Views and Presenters
  • ICompositeManager : manages composite parts of the view.
  • IModuleManager : manages loading of modules.
  • IModuleLocator : indicates where modules should be loaded from (used by IModuleManager)
  • IShellState : provides Shell state persistent service.
You can use the default implementation of each component that is already made available to you out of the box. The last thing to do here is to provide a Presenter object for our Shell window (also registered in the Container). This presenter is where the logic of our shell goes into (see the highlighted line). So here's how our configuration would look like with the default implementation of each component :

Shell Window
According to Wikipedia : "The shell is the container inside of which the entire user interface is presented". This main window (i.e. "Shell") is typically a Form with some extension points, where other modules inject their pieces of user interface, e.g. an empty Main Menu where a module injects its menu items when loaded.

Our shell's user inteface will consist of a Left-Side panel containing means to create a Customer and an Order (also available via Main Menu), a main area which will host our views dynamically. Some extension points are made available in the Shell Window, which we'll use to add our UIElements. Here's a draft of what it is going to look like :

Let's edit the Xaml code of our Shell window. To define a extension point with Caliburn, we need to give the element a "Composite Key". I've defined two extension points named "MainMenu" and "CommandsPanel". I've also added a TabControl to display our views like a Tabbed-MDI application. This is how it should look like so far :

Injecting UI Elements
To inject some UI to the shell, we'll need to create them first. For this demo, I'll create a StackPanel with two buttons to be injected in the "CommandsPanel" to provide Customer related functionalities (The same thing we'll be used for Order). To spice it up, we'll replace this panel with a custom control later. The thing to notice here is that our UI Elements are loose xaml files and will have no logic behind them, just like a resource dictionary (there's no x:Class tag) :

To load our UIElements, we can use the Caliburn's Element class generic methods. There are several ways to store our UIElements and later to load them :

  • Store them in a folder : you can store them as .xaml files in a folder (loose xaml), this way the file can be easily modified in other apps (e.g. Expression Blend), or exchanged between designer / developer, but since it is easy to access, application users can also delete or modify it, resulting missfunction. You can use Element.FromFile method to read UIElement from the file system.

  • Store them as Embedded Resource : The file is at a safer place, but it might be harder to open it in the designer application of your choice (I haven't tested this). Also, you need to Load the assembly at some point as it might not have been loaded when you need to access the stored element.

  • Create them from strings : Yes! you can even provide string Xaml code to Element.FromXaml(...) method to create your element!

To load our UI elements, we need to create modules. At runtime Modules are automatically found by IModuleLocator and loaded by IModuleManager, and that's when we can inject UI elements to the shell. A module needs to implement an IModule interface which provides means to Load / Unload the elements.

In Load method of the module, we can access the extension points made in our Shell's UI, vai ICompositeManager. With this snippet we can load our UIElement and add it to the shell's placeholder.

To access the ICompositeManager (as well as other registered components), we can use a Helper class named "DI" which provides the access to underlying IoC container. Caliburn, provides access to our IoC Container via implementation of IContainer interface. As said before, there are implementations of IContainer for Windsor (WindsorContainerAdapter), for StructureMap (StructureMapAdapter) and for Sprint.NET (SpringAdapter) :

Before running our application, we need to setup module assemblies in a way that IModuleLocator can find them. The default implementation of IModuleLocator (FolderBasedModuleLocator) searches for all the .dll files in "Modules" folder (can be any other folder, though) beside the main executable and finds all the types implementing the IModule interface (To copy the assemblies, you can benefit the project's Build Events and do a xcopy). What I've done here, is to create another implementation based on "FolderBasedModuleLocator" to load the modules from the application folder.

As this behavior shows, we do NOT need to couple our main assembly with the Modules assemblies. FolderBasedModuleLocator can even monitor folder changes later when the application is running and load the assmblies as we deploy them.

Finally, we need to specify when exactly the modules should be loaded. Since modules require to manipulate the Shell, we'll place the code after Show method of our Startup method. Here's our application's Startup Method :

That's it. Now if you run the application, you'll see that the UIElement is injected at the right place :

Download the project file of the first part here.

Submit this story to DotNetKicks Shout it


Rob said...

Looks great! There's one specific thing I'd like to note. There is a change in modules between Beta 1 and the current trunk. If using the current trunk the IModule interface has been slightly extended. Also, to load modules, you would get an instance of IModuleLoader and call the Load method.

Joshua McKinney said...

Your blog makes the images containing source code very difficult to read as it crops the right hand side.

Joshua McKinney said...

In addition any chance of a full text rss feed rather than just the snippet?

Anonymous said...

Download project file: HTTP 404 Not Found?

Anonymous said...

Yeah, 404...

Hadi Eskandari said...

I've fixed the file URL. I've also change the template of the blog. Hope that pictures are more readable and are not clipped anymore.

jim said...

Thanks for the series!

I'll look forward to them in the future.

josh said...

Cool, thanks for the readability fix.

Anonymous said...

nothing to download,
and no pictures in the article...

wilzad said...

hi, no pictures are visible, also the project link is broken, pls fix it so that we can make of some use