Contents
Introduction
Calcium is a WPF composite application toolset that leverages the Composite Application Library.
It provides much of what one needs to rapidly build a multifaceted and sophisticated modular application.
Calcium consists of a client application and server based WCF services, which allow interaction and communication between clients.
Out of the box, Calcium comes with a host of modules and services, and an infrastructure that is ready to use in your next application.
I have deployed Calcium to a demonstration server so you can see it in action. My server has some connectivity issues due to its firewall, so you'll need to download the project to see all of its features.
Diagram: Calcium overview.
The following is a list of some of Calcium’s main features:
- Duplex messaging services for interacting with the user from the client or server using the same API. Interact with the user using Message Boxes from the server!
- Module Manager for enabling or disabling of modules at runtime.
- A User Affinity module that assists collaboration with other users of the application.
- A Command Service to associate WPF
ICommands
with content interfaces that only become active when an active view or viewmodel implements the interface. - Region Adapters for
ToolBars
and Menus
. - Client-server logging ready to work out-of-the-box.
- Includes modules, such as a Web Browser, Text Editor, Output Window, and many more.
- Tabbed interface with dirty file indication (reusable across modules).
- And many more!
Diagram: Calcium Desktop Environment
We’ve got a lot of ground to cover. For that reason I’ve decided to break this article up into a series of three, maybe four articles.
- Introduction to Calcium, Module Manager. (this article)
- Message Service, WebBrowser module, Output Module
- File Service, View Service, Rebranding Calcium
- TBA
In this article you will learn how to:
- use Prism to load controls into UI regions;
- define regions;
- create a custom Prism IModuleCatalog in order to customize how Prism locates assemblies;
- customize module initialization to deal with exceptions;
- create a custom Module Manager to allow the user to selectively disable or enable modules;
Some of the content in this series of articles are not at an advanced level and will suit even those that are new to Prism.
While others, such as the messaging system, will suit more advanced readers. Hopefully there are things here to learn for everyone.
This article is, in some respects, an introduction to some areas of Prism. Although, if you are completely new to Prism, you may find yourself somewhat overloaded at times, and I would recommend taking a look at some beginner Prism articles before you tackle Calcium.
Background
Building large applications is a challenge. As the size of an application grows, and as the size of the team grows,
the difficulty in managing that growth becomes exponential. Team members may find themselves increasingly
stepping on one another’s toes. Prism provides the means to construct an application using discrete pieces,
known as modules, which interact with one another via events, commands, and services.
The advantages of modular development are not limited to team and developer independence; even a small team can benefit
from this approach. Modules are discovered at runtime and can be selectively enabled and disabled.
Modular development can improve maintainability and testability. Also, modularization allows for licensing based on features, where a particular group of modules may be associated with e.g., a standard version, while a greater set may form a professional version.
While Prism comes with some great example applications, it still takes some time to get up to speed and create a usable shell. And there is certainly a lot more infrastructure required besides Prism. This is where Calcium comes in. It is an example application, but it is also a starter-kit with substantial infrastructure consisting of a host of services, utilities, and UI components.
I’m sending Calcium out into the world, so that others may gain a head start in developing their own composite applications.
Components and Dependencies
We begin this article by briefly looking at the composition of the client and server applications. Calcium is comprised of a set of dedicated client-side, dedicated server-side, and shared components. The following, albeit busy, diagram provides an overview of some of the main types used in Calcium
Diagram: Calcium Component Diagram
The Calcium DesktopClient
project has a number of required modules contained within the project, while the remainder is located externally to the main DesktopClient
project.
Application Startup
The entry point for the application is the Client.Launcher.App
class. A launcher project is used to cater for differing deployment scenarios. This approach can be rather useful in cases such as when using e.g., ClickOnce, where multiple deployment settings are required for different environments. We can provide different settings by using a launcher project for each ClickOnce scenario (e.g., Development/Release).
Our Launcher entry point piggybacks the AppStarter
class in the primary Client project. It is the AppStarter
that displays the splash screen, loads resources, runs the Bootstrapper
, and displays the shell or main window.
Diagram: Appliication initialization and startup
Bootstrapper
The Bootstrapper’s
task is to initialize the environment for the application. This includes registering types to be resolved by the Dependency Injection container (Unity), providing Prism with its required IModuleCatalog
instance so that it can load our modules. It is also tasked with creating the application’s main window or shell in the Prism parlance.
DesktopShell
The DesktopShell
is the main window for our Desktop CLR version of Calcium. The shell in Calcium implements an interface called IShell
, which is located in the Client project.
Diagram: DesktopShell
regions
The DesktopShell has four regions that may be populated:
- Tools (left)
- Workspace (center)
- Properties (right)
- Footer (bottom)
The StandardMenu
and StandardToolBarTray
controls also play host to a number of regions, which can be used to add module specific Menus
, MenuItems
, and ToolBars
.
Content Interface Approach
One of the challenges when creating a multi-view application in WPF is to allow RoutedCommands
to be properly activated when the command target may not be the view that has focus. The approach I have taken is to allow ICommands
to be arbitrarily associated with particular content interfaces, and registered with the shell. Thus, we can use custom logic to evaluate when a command should become active.
Certain tasks should be performed the same way, in all situations, by all parts of an application. These include such things as saving files or printing documents. Thus we require a mechanism to have tasks such as these delegated by a centralized facility. For this we make use of a Command Service. We will go into further detail and see how this is performed when we look at the WebBrowser module in the next article in the series.
Modularity
In Prism, we define scaffolding, consisting of the shell B
ootstrapper
etc., to which we add modules, which are the building blocks of an application.
In order to accomplish runtime discovery of modules, Prism makes use of an IModuleCatalog
implementation, which must be defined in our Bootstrapper
. Calcium’s CustomModuleCatalog
is based on Prism’s DirectoryModuleCatalog
. One difference, however, is that we allow modules to be loaded from assemblies that have already been loaded into the AppDomain
, as is the case with our core modules: ModuleManager, Communication, Output, and WebBrowser modules. These are all located in the Client project.
Modules
This release of Calcium comes with several modules.
- ModuleManager
Provides the user with the capability to enable and disable modules. - WebBrowser
A simple web browser module, which includes an address bar that becomes active when a browser window is displayed. - Output
Displays output messages dispatched via a composite event. - TextEditor
- A notepad like module that demonstrates the content command associability.
- UserAffinity
A module that provides the Calcium client with awareness of other users using the application. - Communication
Provides duplex messaging support so that the server may directly interact with the user during WCF calls.
Dependency Injection
- Prism provides support for dependency injection (DI) with its Unity extensions. It also, however, allows Unity to be replaced with another DI container if we so wish. As with most things related to configuring our applications runtime environment, configuring the Unity container is done in the
Bootstrapper
. The following excerpt demonstrates how we use both the app.config
file for configuring the container as well as well known core services.
protected override void ConfigureContainer()
{
var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(Container);
Container.AddNewExtension<StaticFactoryExtension>();
Container.RegisterType<ILoggerFacade, CompositeLogAdapter>();
Container.RegisterInstance<IChannelManager>(ChannelManagerSingleton.Instance);
Container.RegisterInstance<Dispatcher>(Dispatcher.CurrentDispatcher);
Container.RegisterType<IModuleLoadErrorStrategy, ModuleLoadErrorStrategy>();
Container.RegisterType<IModuleInitializer, CustomModuleInitializer>();
Container.RegisterInstance<IViewService>(new ViewService());
Container.RegisterInstance<IMessageService>(new MessageService());
Container.RegisterInstance<IFileService>(new FileService());
UnitySingleton.Initialize(Container);
base.ConfigureContainer();
}
While I have provided out of the box support for config file container configuration, I myself generally try to avoid using config for container configuration, and only do so when there is a known requirement for the extra flexibility that it affords. I do this because config files filled with type mappings can become a maintenance headache, and I also prefer compile-time verification that my type names are correct. May I recommend that if the reader does decide to use mostly config for your application, be sure that there is a unit test defined for all required type mappings. When one refactors or renames something, it can be easy to forget about the container config.
Region Adaptation
Refresher on Prism Regions
To Prism, views are merely objects, which can be placed in containers known as regions, using out-of-the-box or custom Region Adapters. Region Adapters implement IRegionAdapter
and decouple the method for populating regions. When building a Prism based application, it is generally the custom to decorate the shell or custom controls with an attached property that defines locations within the shell as regions. In the following excerpt we see how this RegionName
attached property declares the tabControl_Right
as being the Region with the unique name Properties
.
<TabControl x:Name="tabControl_Right" MinWidth="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
IsSynchronizedWithCurrentItem="True" BorderThickness="0,0,0,0"
cal:RegionManager.RegionName="{x:Static Desktop:RegionNames.Properties}"
…
/>
Once we have a Region defined, we can populate it with view objects from anywhere, but most usefully from our modules. The following excerpt is taken from the ModuleManagerModule
and shows how the Region Manager is retrieved from the Unity container, the Properties region is located by name, and then a new instance of the ModuleManagerView is added to the region.
public void Initialize()
{
var regionManager = UnitySingleton.Container.Resolve<IRegionManager>();
regionManager.Regions[RegionNames.Properties].Add(new ModuleManagerView());
}
Regions are at the heart of Prism’s UI composition infrastructure.
They enable the module author to associate content with known areas within the UI,
without specific knowledge about the container within the shell, nor the actual location of the region within the shell.
With that, we gain flexibility so that we can avoid reengineering modules if the layout implementation changes.
Calcium Region Adapters
Now that we have a solid understanding of how regions work, let’s examine the method by which Prism is able to take a UI element and place it correctly into a region. The difficulty here is that adding to a region where the host container is of a different type, must normally be done by a different means. For example, we can’t add items to a ToolBar
in the same way we would for a TabControl
.
In Calcium we have a number of custom IRegionAdapters
. These include the ToolBarTrayRegionAdapter
, and the CustomItemsControlRegionAdapter
. The first ToolBarTrayRegionAdapter
allows us to create a ToolBar
in a module, and to then slot it into a ToolBarTray
at runtime. The second, CustomItemsControlRegionAdapter
, can be used for most ItemsControls
, but is used explicitly for MenuItems
and Menus
. With it we are able to define a Menu
or MenuItem
as being a region. When we add to the region, child Menus
and MenuItems
are seen.
In order to provide Prism with our custom IRegionAdapter
implementations, we overload the ConfigureRegionAdapterMappings
in the Bootstrapper
, as the following excerpt demonstrates:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings() ?? Container.Resolve<RegionAdapterMappings>();
mappings.RegisterMapping(typeof(ToolBarTray), Container.Resolve<ToolBarTrayRegionAdapter>());
mappings.RegisterMapping(typeof(Menu), Container.Resolve<CustomItemsControlRegionAdapter>());
mappings.RegisterMapping(typeof(MenuItem), Container.Resolve<CustomItemsControlRegionAdapter>());
return mappings;
}
Region Adapters provide as with not only control over how a UI element is placed in a region’s host container, but also affords us the opportunity to wire up event handlers etc.,
to suit whatever purpose.
Calcium M-V-VM
Prism is very flexible when it comes to the composition of views. In fact it is oblivious to the structure of our views. Calcium, on the other hand, is as agnostic to view composition as Prism, however Calcium’s IShell
implementation has a number of expectations about its views. While they are not requirements, they assist the shell in performing its tasks. One such expectation is that a view placed in a Prism region, shall implement the IView
interface, shown below.
public interface IView
{
IViewModel ViewModel { get; }
}
Each view associates itself with a particular IViewModel
instance. A view’s IViewModel
is generally used as its DataContext
.
The following excerpt shows the IViewModel
interface.
public interface IViewModel
{
object TabHeader { get; }
}
The design requires that a view knows about its viewmodel, but not vice versa.
Diagram: IView
, IViewModel
, and inheritors.
Generally speaking, it’s not necessary for the ViewModel to interact with the View directly, and some may argue that it shouldn’t at all. It is, in fact, somewhat of a contentious issue. Yet, having knowledge of the view from the viewmodel can be useful in some scenarios, and perhaps isn’t so bad if the view can be easily mocked for testing; and interaction is done exclusively via an interface. So, we see in Calcium that the base IView
implementation (ViewControl
) has an IViewModel
, and likewise the base IViewModel
(ViewModelBase
) has an IView
.
Diagram: ViewControl
and ViewModelBase
class hierarchies.
The following excerpt shows the ViewModelBase
class in its entirety, and shows how an IView
instance is required when it is itself instantiated.
public abstract class ViewModelBase : DependencyObject,
IViewModel, INotifyPropertyChanged
{
readonly IView view;
protected ViewModelBase(IView view)
{
ArgumentValidator.AssertNotNull(view, "view");
notifier = new PropertyChangedNotifier(this);
this.view = view;
}
public IView View
{
get
{
return view;
}
}
#region TabHeader Dependency Property
public static DependencyProperty TabHeaderProperty
= DependencyProperty.Register(
"TabHeader", typeof(object), typeof(ViewModelBase));
[Description("The text to display on a tab.")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public object TabHeader
{
get
{
return (object)GetValue(TabHeaderProperty);
}
set
{
SetValue(TabHeaderProperty, value);
}
}
#endregion
#region Property Changed Notification
readonly PropertyChangedNotifier notifier;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
notifier.PropertyChanged += value;
}
remove
{
notifier.PropertyChanged -= value;
}
}
protected void OnPropertyChanged(string propertyName)
{
notifier.OnPropertyChanged(propertyName);
}
#endregion
}
The supplied TabItemDictionary
ResourceDictionary
uses the TabHeader
property from the view’s ViewModel to display it in the UI. The following shows the DataTemplate
for the TabHeader
. The styles have been excluded for clarity.
<DataTemplate x:Key="TabHeaderDataTemplate">
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock x:Name="textBlock"
Text="{Binding TabHeader}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock Text="*"
Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button x:Name="button"
Command="ApplicationCommands.Close" CommandParameter="{Binding Path=View}"
Template="{DynamicResource CloseTabButtonControlTemplate}" ToolTip="Close" />
</StackPanel>
</DataTemplate>
The two key points to notice with this template are:
Firstly, the binding to the Content.Dirty
property of the TextBlock
that is used to display an asterisk if the file is dirty and requires saving.
Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}"
Notice the use of the FallbackValue
specifier. It is used to hide the TextBlock
if the dependency property Content.Dirty
can’t be found. If we didn’t use this, the TextBlock
visibility would default to Visible
if not found.
Secondly, the close button on the tab uses the ApplicationCommands.Close
command, and uses the View as the CommandParameter
. This enables us to resolve the view to be closed when the button is clicked.
The TabItem
template, which references the previous TabHeaderDataTemplate
binds the Header
property to the View’s ViewModel as shown in the following excerpt.
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding ViewModel}" />
<Setter Property="HeaderTemplate"
Value="{DynamicResource TabHeaderDataTemplate}" />
<Setter Property="Template">
…
</Style>
The IShell
implementation DesktopShell
capitalizes on this usage pattern, enabling module creators to specify content interfaces in views or view-models.
What is a content interface? A content interface in Calcium is used to provide shell level functionality,
such as saving files or printing, in a standard way; and one that allows the shell to enable or disable the functionality depending on the presence of such content. For example, if a view implements ISavableContent
, then the shell will automatically enable/disable the ApplicationCommands.Save
and SaveAs
commands.
We’ll cover this in more detail in the third article of this series.
Module Manager
The Module Manager is itself a Prism module that allows the user to selectively disable and enable modules from within the UI. This capability does not come with Prism out of the box.
In order to accomplish the disabling and enabling of modules, we require several enhancements to Prism. How do we customize module loading with Prism? The answer is that we provide a customized Microsoft.Practices.Composite.Modularity.
IModuleCatalog
. The purpose of the IModuleCatalog
is to provide Prism with an application’s modules, and to determine the dependencies between modules.
Our implementation of IModuleCatalog
is DanielVaughan.Calcium.Client.Modularity.CustomModuleCatalog
. It, in fact, extends the ModuleCatalog
base class from the Prism Composite.Desktop project, and is based on the Prism’s DirectoryModuleCatalog
, but with some important differences. In particular, our CustomModuleCatalog
class makes use of a number of lists exposed by the Calcium’s ModuleManagerSingleton
: FailedModules
and NonStartupModules
. During startup, when Prism uses the custom IModuleCatalog
to locate the modules to be loaded, the custom module catalog excludes those modules in these lists, and all of their dependent modules.
For obvious reasons, the ModuleManager module itself cannot be disabled from the UI.
Diagram: Screenshot of the ModuleManagerView
tab within Calcium.
Handling Module Load Exceptions
When a module is initialized it can fail. Prism won’t help us much in this regard, and in fact, if one doesn’t provide a custom implementation of Microsoft.Practices.Composite.Modularity.IModuleInitializer
, and a module throws an exception during initialization, an application crash may ensue. In the previous version of Prism, there existed no extensibility point for handling module initialization errors. Now, fortunately, we are able to do so by providing a custom implementation of IModuleInitializer
. Our CustomModuleInitializer
class does just this, and delegates the handling of initialization exceptions to an IModuleLoadErrorStrategy
as can be seen in the following excerpt.
public override void HandleModuleInitializationError(
ModuleInfo moduleInfo, string assemblyName, Exception exception)
{
var errorStrategy = UnitySingleton.Container.Resolve<IModuleLoadErrorStrategy>();
if (errorStrategy != null)
{
errorStrategy.HandleModuleLoadError(moduleInfo, assemblyName, exception);
return;
}
base.HandleModuleInitializationError(moduleInfo, assemblyName, exception);
}
The IModuleLoadErrorStrategy
informs the ModuleManagerSingleton
that the module has failed, and dispatches a Prism CompositeEvent
for good measure; as the following excerpt shows.
class ModuleLoadErrorStrategy : IModuleLoadErrorStrategy
{
public void HandleModuleLoadError(
ModuleInfo moduleInfo, string assemblyName, Exception exception)
{
ArgumentValidator.AssertNotNull(moduleInfo, "moduleInfo");
ModuleManagerSingleton.Instance.AddFailedModuleName(moduleInfo.ModuleName);
var moduleException = new ModularityException(
moduleInfo.ModuleName, exception.Message, exception);
Log.Error(moduleException.Message, moduleException);
var messageService = UnitySingleton.Container.Resolve<IMessageService>();
string moduleLoadUserMessage = string.Format(
"Module {0} failed to load and has been disabled.", moduleInfo.ModuleName);
messageService.ShowError(moduleLoadUserMessage);
var moduleLoadError = new ModuleLoadError { AssemblyName = assemblyName,
Exception = exception, ModuleInfo = moduleInfo };
var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
var moduleLoadErrorEvent = eventAggregator.GetEvent<ModuleLoadErrorEvent>();
moduleLoadErrorEvent.Publish(moduleLoadError);
}
}
This places the module’s name in a list of failed modules, and will prevent the module and dependent modules from being loaded the next time the application starts.
ModuleManager Module Implementation
The Module Manager consists of the IModule
implementation ModuleManagerModule
class, the view (ModuleManagerView
) and viewmodel (ModuleManagerViewModel
).
<Gui:ViewControl x:Class="DanielVaughan.Calcium.Client.Modules.ModuleManager.ModuleManagerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Gui="clr-namespace:DanielVaughan.Calcium.Client.Gui">
<DockPanel x:Name="ContentPanel">
<DockPanel.Resources>
<DataTemplate x:Key="checkBoxTemplate">
<Border>
<CheckBox IsChecked="{Binding Path=Enabled, Mode=TwoWay}"
IsEnabled="{Binding Path=UserCanEnable, Mode=OneWay}" />
</Border>
</DataTemplate>
</DockPanel.Resources>
<ListView HorizontalContentAlignment="Stretch"
ItemsSource="{Binding ApplicationModules}" FontSize="8">
<ListView.View>
<GridView>
<GridViewColumn
CellTemplate="{DynamicResource checkBoxTemplate}" Header="Enabled"/>
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Path=ModuleName}" Width="75"/>
<GridViewColumn Header="State"
DisplayMemberBinding="{Binding Path=State}" />
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Gui:ViewControl>
Notes on Modularity
One thing that we have not addressed in this article is the question of how to deal with those modules that achieve only partial initialization. For example, during initialization a module may successfully add a menu item with an associated command to the shell, yet it may throw an exception before it completes initialization, and may be in an inconsistent state. As there exists no automatic mechanism for removing the UI menu item, it will become orphaned and the module may not be capable of actionning the command when it is invoked. To address this issue we could enhance our IModuleInitializer
implementation to monitor the state of common UI components for changes during the initialization process, so that if a module should fail, it could be automatically unloaded; views added to regions could be removed etc. I have also done some work on an Undoable Action infrastructure that I will integrate at a later stage. This may also assist in safely undoing a modules partial initialization. Until we have these in place it is the module author that is responsible for detecting and properly cleaning up when his or her module fails.
Future enhancement of the Module Manager
At the moment, disabling and enabling of modules requires an application restart. We can imagine an enhancement to Calcium which would enable dynamic unloading without a restart. I see this as perhaps being accomplished through an implementation of e.g. IUnloadableModule
interface at the module level. Each module would know how to unload itself and its views etc. Of course dependencies would also need to be analyzed to prevent the sudden failure of dependent modules.
Conclusion
In this article we have seen how Calcium can be used as a head start for creating a multifaceted modular application. In particular we have seen an overview of Calcium’s Bootstrapper
and shell implementations, examined the M-V-VM approach employed in Calcium, explored the content interface approach to command delegation, and looked at developing a Module Manager for enabling or disabling of modules at runtime.
In our next article we shall examine the duplex messaging system, which provides interaction capability with the user from the client or server, using the same API. We can interact with the user using Message Boxes instigated from the server! We will also look at some of Calcium’s other modules, such as the Web Browser and Output Module. We still have a lot to cover, and I hope you will join me for our next installment.
I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.
History
May 2009