Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / WPF

Caliburn - Modules, Windows, and Actions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
10 Mar 2010CPOL6 min read 50.5K   1.1K   49   4
This article demonstrates breaking out of the shell with module development and using Caliburn actions.

emx

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.

project_shell.gif

project_shared.gif

Shell Project
Shared Project

project_module_a.gif

project_module_b.gif

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.

C#
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).

C#
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 Assemblys 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.

C#
protected override void ConfigurePresentationFramework(PresentationFrameworkModule module)
{
    module.UsingWindowManager<WindowManager>();
}

This call customizes the Window Manager to use the configuration in the class WindowManager:

C#
//Display a view in a dialog (modal) window 
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();
}

//Display a view in a popup (non-modal) window 
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.

XML
<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:

XML
<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.

C#
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) ObservableCollections 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:

XML
<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.

C#
[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).

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Encore Software
Australia Australia
Contractor in Desktop and Web applications.
Gold Coast, Queensland.

Comments and Discussions

 
GeneralThanks for the nice write-up. Pin
Razan Paul (Raju)8-Nov-10 12:54
Razan Paul (Raju)8-Nov-10 12:54 
GeneralRe: Thanks for the nice write-up. Pin
Nic_Roche10-Nov-10 9:04
professionalNic_Roche10-Nov-10 9:04 
GeneralCaliburn Pin
SR Sakar29-Oct-10 4:56
SR Sakar29-Oct-10 4:56 
GeneralRe: Caliburn Pin
Nic_Roche10-Nov-10 8:57
professionalNic_Roche10-Nov-10 8:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.