WPF Application with Caliburn - Part Two
In this part of the tutorial, we’ll enhance our shell view to display other presenters and add Save and Print support for presenters supporting it. You can use generalize this example and learn how to implement generic features in your shell.
Caliburn has several existing classes that can be used as a base class for our presenters. Let’s explain some of them and see where do they fit:
- Presenter: This class implements IPresenter interface and provides life time methods such as Initialize, Shutdown, Active and Deactive. You can use this class for other views that are going to be displayed in the Shell View.
- Presenter Manager: Using a presenter manager, as the name implies, you can host and display a single instance of a Presenter. It only allows displaying of one Presenter only so you have only one view displayed to the user when using it.
- MultiPresenterManager: Same as PresenterManager but can host multiple Presenters at the same time, but only one of them is Active at a time.
When we display a Presenter, the PresenterManager will call the lifetime methods on the Presenter instance. The following diagram shows this flow:
For now, it is obvious that we need to change the base class of our ShellViewModel to PresenterManager (too keep it simple for the time being), but how should we change the xaml code to host the views (other Presenters)? Your first guess would be to bind CurrentPresenter property of the PresenterManager to a ContentControl in the Shell.xaml, but you don’t need to do that as Caliburn will do the plumbing for us, just add a ContentControl to the Shell view where you want the views to be displayed and name it as “CurrentPresenter”.
To make Caliburn bind Views to the content control, you need to enable additional Conventions that are not enabled by default. Placing the following snippet in Caliburn configuration will do the trick:
var binder = (DefaultBinder)Container.GetInstance<IBinder>();
The bare bone of the shell view that displays a single presenter is this:
Now to handle user interaction with the shell and the open presenter, we need some kind of a Toolbar / Menu control. WPF comes with a very basic menu and toolbar control, but to show how to use custom controls and see how Caliburn facilitates this, let’s use DevExpress BarManager control in this example.
DevExpress, among other nice things, provides presentation layer controls for WPF such as Editors, Bars (MenuBar, ToolBar and soon Ribbon), Grid and Printing. One other thing they’ve also done is that they also have skinned native WPF controls so if used together with DevExpress controls, you’ll have a seemless look and feel.
To add the BarManager control to our ShellView, you need to download DevExpress WPF controls (trial version here) and install it. The add the necessary reference to devexpress WPF assemblies and import the xml namespaces. Here’s the snippet that creates the BarButtonItems which is equivalent of a ToolbarItem. (Get the accompanied zip for complete source code):
Same as Button object, BarButtonItems provide a Command property which you can bind to an ICommand instance on your ViewModel, but this is rather limited, don’t you think? What if you want to bind another event (other then Click) to your ViewModel?
Caliburn provides Actions and Messages with which you can bind a regular Event on your View and Controls to a method on your ViewModel, without using ICommand. You can even run the event handler (on your VM) asynchronously! There are a few ways to do this but the best way, in my opinion, is to this format:
cal:Message.Attach="[Event EventNameOnControl] = [Action ActionNameOnViewModel()]"
Note that you can actually bind two actions to one event, just like you can have two methods listening to a regular event. Let’s change the snippet above and add the messaging support:
<dxb:BarButtonItem Name="bSave" cal:Message.Attach="[Event ItemClick] = [Action SavePresenter()]"/>
Note: Unlike a regular Button, BarButtonItem has no “Click” event and provides an “ItemClick” event instead. Clearly, we need the specified methods on our ViewModel, so let’s add them too:
public class ShellViewModel : PresenterManager, IShellViewModel
At this point, you can run the shell and click the tool bar items, which will call the specified method on the view model. Now the question is, what if a presenter is not in a suitable state to be saved (dirty checking, already saved, etc.). What if a presenter does not even support printing? How can we disable the BarButtonItem responsible for an action, if the presenter does not support it?
With Conventions already enabled, you can have additional Can methods acting as a Filter for your actions. These filters will be checked on runtime for returning false value indicating the action can not be performed, or true value for allowed actions. Caliburn will try to disable the piece of UI through an
IAvailabilityEffect interface. Let’s see this in more dept.
As mentioned, Caliburn checks for availability of an action automagically if it finds a pairing Can filter method on the presenter. So let’s add those and see how it works:
public virtual bool CanExit()
When you run the application and set a break point on CanSave and Save methods, you’ll notice two things:
A. While CanPrint method returns False value, UI element (BarButtonItem) is not disabled!
B. By clicking the BarButtonItem, the Save method will not be called.
So there seems to be a problem…Why isn’t BarButtonItem disabled? Wasn’t Caliburn supposed to do that for us? Well…The default “Disable” effect, only works for
BarButtonItem is actually a
FrameworkContentElement. Those are two different beasts and Caliburn can not handle our UI control. The solution is easy though. You can create your own
IAvailabilityEffect that works with
public class DisableMenuEffect : IAvailabilityEffect
…and set it along with the action on the View:
<dxb:BarButtonItem cal:Message.Attach="[Event ItemClick] = [Action SavePresenter()]" cal:Message.AvailabilityEffect="DisableMenuEffect" />
There’s one last trick to make this work. Caliburn has no way to convert the specified string to an IAvailabilityEffect instance! The existing ValueCovertor, asks our Container for any object named “DisableMenuEffect”. The final piece of code would be to register all instances of IAvailabilityEffects you might want to use on the container:
When running the application, and there’s no open view, the toolbar items are disabled.
To show how it works, let’s add a new View and ViewModel to our application, and also add a button to the shell that allows us to open this view. As mentioned before, to do this, you need to create a UserControl for you View and a class deriving from Presenter base class for your ViewModel.
public class PersonViewModel : Presenter
Now create a UserControl for PersonView using DevExpress editors looking like this:
To support saving, let’s add ISaveablePresenter interface to our ViewModel and check if DateOfBirth property of the current person is valid before saving:
public bool CanSave
You can observe that CanSave property will never get executed! The reason is that when application starts, Caliburn checks if any of the Action methods are available by checking their respective Filters (here CanSavePresenter and CanPrintPresenter), but it never checks them again. In other words, by default action filters (Can* methods) are only called once. How can we change this behavior?
There’s an attribute called AutoCheckAvailability that you can place on your action methods. This will tell WPF Command Manager to look out for availability of the bound action. Doing this will call the action filters periodically, so make sure if you really need it before actually using it. So apply this to our ViewModel, let’s get back to our ShellViewModel class and place the attribute on our actions:
You need to place this on your actions, not on filters.
Our shell now can host other views and has generic actions that is automagically enabled / disabled for views supporting it.
In the next post, I’ll show you how to change our existing shell to display a Tabbed interface and display multiple views at the same time. The attached zip file contains the complete source code.