Introduction
Caliburn is a CodePlex project that provides UI patterns for WPF and Silverlight development in an integrated framework. The patterns include MVVM, MVC, MVP, Commands, etc. It encourages Test Driven Development (TDD), and provides an easy to use Dependency Injection container. The project is coordinated by Rob Eisenburg, who is very active in resolving issues and evolving the framework. He has inspired many other talented developers to assist in adding patches and foster the product's growth. This article assumes a knowledge of Dependency Injection and Inversion of Control. I will cover each section lightly, but offer links for more depth to the subject matter.
Background
Currently, I am working on a large BackOffice system that uses Prism in a WPF context. The Dependency Injection and loosely coupled architecture appealed to me, and I developed some unrelated projects using Prism. It wasn't long before I was introduced to Ninject, and the whole infrastructure was soon cutover to using Ninject patterns. As the applications iterated, a few scenarios were not handled well with Ninject. Some singleton scoped objects were retaining private field values (even though they were constructed each time), and the framework (much like Prism) was based on a single shell. I needed modules having their own windows (concurrently with other modules) and modal dialogs being launched from multiple contexts.
Through looking at alternative WPF UI frameworks, I came across Caliburn. Using Caliburn, the singleton state persistence is not an issue, and it provides a Window Manager which is customisable. The framework supports far more functionality than the demo exhibits. The included project has a very simple module scenario, where the main menu launches module UIs in separate windows, or embeds them in the shell. Further, modal item editors are launched and the Caliburn actions are introduced. There are assorted demo projects in the samples of the Caliburn release, and added to the issue tracker at CodePlex, but I could not spot any that demonstrates this combination.
|
|
Shell Project
|
Shared Project
|
|
|
Module A Project
|
Module B Project
|
Using the Code
The code has been organized for readability rather than best practice OO design. The solution has been structured to include a shell, two modules, and a shared library. The general developer workflow will be outlined, and particular attention will be shown to processes more central to this particular scenario. The following code relates to the RTW V1 branch. The V2 trunk source has changed somewhat.
The Shell
App.xaml inherits CaliburnApplication
so that some configuration and initialization can take place. This can also be done manually through the CaliburnFramework
class. The methods we are concerned with are CreateRootModel
, SelectAssemblies
, and ConfigurePresentationFramework
.
protected override object CreateRootModel()
{
var binder = (DefaultBinder)Container.GetInstance<IBinder>();
binder.EnableMessageConventions();
binder.EnableBindingConventions();
return Container.GetInstance<IShellViewModel>();
}
This method returns the root application model, which is ShellViewModel
(which implements IShellViewModel
).
protected override System.Reflection.Assembly[] SelectAssemblies()
{
return new System.Reflection.Assembly[] { Assembly.GetExecutingAssembly(),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.modules.moduleA.ModuleAModule)),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.modules.moduleB.ModuleBModule)),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.shared.Actions.DialogResultAction))
};
This method selects an array of Assembly
s which Caliburn will be able to inspect for components, views, etc. This includes classes that have been declared for Dependency Injection and classes that are configured by DefaultViewStrategy
(mapping ViewModels to Views via namespace conventions), among others.
protected override void ConfigurePresentationFramework(PresentationFrameworkModule module)
{
module.UsingWindowManager<WindowManager>();
}
This call customizes the Window Manager to use the configuration in the class WindowManager
:
public new bool? ShowDialog(object rootModel, object context,
Action<ISubordinate, Action> handleShutdownModel)
{
var window = base.CreateWindow(rootModel, context, handleShutdownModel);
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.WindowStyle = WindowStyle.ToolWindow;
window.ResizeMode = ResizeMode.NoResize;
window.Title = ((IPresenter)rootModel).DisplayName;
return window.ShowDialog();
}
public new void Show(object rootModel, object context,
Action<ISubordinate, Action> handleShutdownModel)
{
var window = base.CreateWindow(rootModel, context, handleShutdownModel);
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.Title = ((IPresenter)rootModel).DisplayName;
window.ResizeMode = ResizeMode.NoResize;
window.Show();
}
These methods configure the windows used for ShowDialog
and Show
.
So, as the application starts, ShellViewModel
is the initial root model, and ShellView
is bound to it because of the namespace conventions.
ShellView.xaml has a very simple interface, though it does use basic actions. These are highly customizable, and are documented here.
<Button
cal:Message.Attach="ShowModuleA(HostComboBox.SelectedIndex)"
Content="Module A - Persons"/>
The attached properties to the button route the default event (Click
) to a method in the ViewModel. The markup for hosting the loaded view in the Shell is:
<Controls:TransitionPresenter x:Name="CurrentPresenter">
<cal:Action.TargetWithoutContext >
<shared.actions:CRUDAction />
</cal:Action.TargetWithoutContext >
</Controls:TransitionPresenter>
This mechanism is similar to declaring a region in Prism. What is important here is the attached property cal:Action.TargetWithoutContext
which maps any actions in the hosted views to a class (without assigning the datacontext of the view to it) in the shared library called CRUDAction
, which will be discussed later.
ShellViewModel.cs is the ViewModel for the discussed view. It has the method mapped in the cal:Message.Attach
property.
public void ShowModuleA(int index)
{
Host<emx.tcp.caliburn.loading.modules.moduleA.ViewModels.PersonListViewModel>(index);
}
This method receives the selected index of the combo box, and routes the Module A ViewModel and the index to a generic method for either embedding the view or showing in a window.
The Modules
The modules are very similar in function, hosting Views, ViewModels, Models, and Services. Module B has some additional Controls, Converters, and Formatters which could have been packaged in another library, if necessary.
The ViewModels are injected with services via the constructor.
The data Services fetch data from the serialized (for demonstration purposes only) ObservableCollection
s of the POCO in the Models namespace. A simple Singleton parameter service is used for passing parameters between class instances; a more robust contract based approach is recommended for anything other than demos.
The Views also map actions to classes in the shared library; this declaration can be done for the whole UserControl though. Also of note are the parameters that are passed to the external actions:
<Button
Content="Add"
cal:Message.Attach="AddAction($datacontext)"
Style="{StaticResource ButtonStyle}"/>
This introduces a new concept of special parameters. Some of the supported parameters are:
$dataContext
- the datacontext of the view, which may be the ViewModel.
$value
- the value of the source element.
$source
- the source element.
$eventArgs
- the arguments in the event signature.
The Shared Library
The shared library contains some of the reusable interfaces and classes used in the modules. It has mainly been included to show how dependencies can be injected and actions referenced from external libraries. The Action classes warrant special mention.
CRUDAction.cs is referenced by the "List" views in Module A and Module B. This class executes the actions from the view (Add, Edit, Delete, Save) and sets the states of the buttons that are the sources of the actions. Each action can be decorated with a Preview Filter. This has similar functionality to Execute
, CanExecute
in Commands
.
[Preview("CanAddAction")]
public void AddAction(IActionViewModel actionViewModel)
{
_actionViewModel = actionViewModel;
actionViewModel.ParameterService.AssignParameter("Action", "Add");
actionViewModel.ParameterService.AssignParameter("CurrentItem",
actionViewModel.EditableCollectionView.AddNew());
if ((bool)ShowDialog(actionViewModel.GetEditorRootModel()))
{
actionViewModel.EditableCollectionView.CommitNew();
RaisePropertyChangedEventImmediately("CanSaveAction");
}
else
{
actionViewModel.EditableCollectionView.CancelNew();
}
}
public bool CanAddAction(IActionViewModel actionViewModel)
{
return actionViewModel.EditableCollectionView.CanAddNew;
}
Although the parameter is passed through as $datacontext
from the view, by typing the parameter IActionViewModel
, which both "List" ViewModels implement, the actions and preview filters can work generically.
DialogResultAction.cs is referenced by the "AddEdit" views in Module A and Module B. This class executes the actions from the view (OK, Cancel).
public void OKAction(RoutedEventArgs e)
{
Window hostWindow = FindParent((Button)e.OriginalSource);
hostWindow.DialogResult = true;
}
public void CancelAction(RoutedEventArgs e)
{
Window hostWindow = FindParent((Button)e.OriginalSource);
hostWindow.DialogResult = false;
}
The parameter passed through was $eventArgs
, so the parameter could be used to get a reference to the programmatically generated window, which hosted the View modally. Then, the DialogResult
is able to be set.
Points of Interest
The binding via namespace conventions and the variety of ways to use actions and commands makes the amount of code very light compared with alternative approaches. This also fosters separation of concerns and loose coupling. From what I have seen, although the API has had a major reshuffle in version 2, the new naming conventions are more pertinent.
Links
History
- 2010-03-10 - HTML modifications.
- 2010-03-04 - Initial submission.