Actions, Coroutines and Conventions tend to draw the most attention to Caliburn.Micro, but the Screens and Conductors piece is probably most important to understand if you want your UI to be engineered well. It’s particularly important if you want to leverage composition. The terms Screen, Screen Conductor and Screen Collection have more recently been codified by Jeremy Miller during his work on the book "Presentation Patterns" for Addison Wesley. While these patterns are primarily used in CM by inheriting ViewModels from particular base classes, its important to think of them as roles rather than as View-Models. In fact, depending on your architecture, a Screen could a be a UserControl, Presenter or ViewModel. That’s getting a little ahead of ourselves though. First, let’s talk about what these things are in general.
This is the simplest construct to understand. You might think of it as a stateful unit of work existing within the presentation tier of an application. It’s independent from the application shell. The shell may display many different screens, some even at the same time. The shell may display lots of widgets as well, but these are not part of any screen. Some screen examples might be a modal dialog for application settings, a code editor window in Visual Studio or a page in a browser. You probably have a pretty good intuitive sense about this.
Often times a screen has a lifecycle associated with it which allows the screen to perform custom activation and deactivation logic. This is what Jeremy calls the ScreenActivator. For example, take the Visual Studio code editor window. If you are editing a C# code file in one tab, then you switch to a tab containing an XML document, you will notice that the toolbar icons change. Each one of those screens has custom activation/deactivation logic that enables it to setup/teardown the application toolbars such that they provide the appropriate icons based on the active screen. In simple scenarios, the ScreenActivator is often the same class as the Screen. However, you should remember that these are two separate roles. If a particular screen has complex activation logic, it may be necessary to factor the ScreenActivator into its own class in order to reduce the complexity of the Screen. This is particularly important if you have an application with many different screens, but all with the same activation/deactivation logic.
Once you introduce the notion of a Screen Activation Lifecycle into your application, you need some way to enforce it. This is the role of the ScreenConductor. When you show a screen, the conductor makes sure it is properly activated. If you are transitioning away from a screen, it makes sure it gets deactivated. There’s another scenario that’s important as well. Suppose that you have a screen which contains unsaved data and someone tries to close that screen or even the application. The ScreenConductor, which is already enforcing deactivation, can help out by implementing Graceful Shutdown. In the same way that your Screen might implement an interface for activation/deactivation, it may also implement some interface which allows the conductor to ask it “Can you close?” This brings up an important point: in some scenarios deactivating a screen is the same as closing a screen and in others, it is different. For example, in Visual Studio, it doesn’t close documents when you switch from tab to tab. It just activates/deactivates them. You have to explicitly close the tab. That is what triggers the graceful shutdown logic. However, in a navigation based application, navigating away from a page would definitely cause deactivation, but it might also cause that page to close. It all depends on your specific application’s architecture and it’s something you should think carefully about.
In an application like Visual Studio, you would not only have a ScreenConductor managing activation, deactivation, etc., but would also have a ScreenCollection maintaining the list of currently opened screens or documents. By adding this piece of the puzzle, we can also solve the issue of deactivation vs. close. Anything that is in the ScreenCollection remains open, but only one of those items is active at a time. In an MDI-style application like VS, the conductor would manage switching the active screen between members of the ScreenCollection. Opening a new document would add it to the ScreenCollection and switch it to the active screen. Closing a document would not only deactivate it, but would remove it from the ScreenCollection. All that would be dependent on whether or not it answers the question “Can you close?” positively. Of course, after the document is closed, the conductor needs to decide which of the other items in the ScreenCollection should become the next active document.
There are lots of different ways to implement these ideas. You could inherit from a TabControl and implement an IScreenConductor interface and build all the logic directly in the control. Add that to your IoC container and you’re off and running. You could implement an IScreen interface on a custom UserControl or you could implement it as a POCO used as a base for Supervising Controllers. ScreenCollection could be a custom collection with special logic for maintaining the active screen, or it could just be a simple IList<IScreen>.
These concepts are implemented in CM through various interfaces and base classes which can be used mostly(1) to build ViewModels. Let’s take a look at them:
In Caliburn.Micro we have broken down the notion of screen activation into several interfaces:
- IActivate – Indicates that the implementer requires activation. This interface provides an Activate method, an IsActive property and an Activated event which should be raised when activation occurs.
- IDeactivate – Indicates that the implementer requires deactivation. This interface has a Deactivate method which takes a bool property indicating whether to close the screen in addition to deactivating it. It also has two events: AttemptingDeactivation, which should be raised before deactivation and Deactivated which should be raised after deactivation.
- IGuardClose – Indicates that the implementer may need to cancel a close operation. It has one method: CanClose. This method is designed with an async pattern, allowing complex logic such as async user interaction to take place while making the close decision. The caller will pass an Action<bool> to the CanClose method. The implementer should call the action when guard logic is complete. Pass true to indicate that the implementer can close, false otherwise.
In addition to these core lifecycle interfaces, we have a few others to help in creating consistency between presentation layer classes:
- IHaveDisplayName – Has a single property called DisplayName
- INotifyPropertyChangedEx – This interface inherits from the standard INotifyPropertyChanged and augments it with additional behaviors. It adds an IsNotifying property which can be used to turn off/on all change notification, a NotifyOfPropertyChange method which can be called to raise a property change and a Refresh method which can be used to refresh all bindings on the object.
- IObservableCollection<T> – Composes the following interfaces: IList<T>, INotifyPropertyChangedEx and INotifyCollectionChanged
- IChild<T> – Implemented by elements that are part of a hierarchy or that need a reference to an owner. It has one property named Parent.
- IViewAware – Implemented by classes which need to be made aware of the view that they are bound to. It has an AttachView method which is called by the framework when it binds the view to the instance. It has a GetView method which the framework calls before creating a view for the instance. This enables caching of complex views or even complex view resolution logic. Finally, it has an event which should be raised when a view is attached to the instance called ViewAttached.
Because certain combinations are so common, we have some convenience interfaces and base classes:
- PropertyChangedBase – Implements INotifyPropertyChangedEx (and thus INotifyPropertyChanged). It provides a lambda-based NotifyOfPropertyChange method in addition to the standard string mechanism, enabling strongly-typed change notification. Also, all property change events are automatically marshaled to the UI thread.(2)
- BindableCollection – Implements IObservableCollection<T> by inheriting from the standard ObservableCollection<T> and adding the additional behavior specified by INotifyPropertyChangedEx. Also, this class ensures that all property change and collection change events occur on the UI thread.(2)
- IScreen – This interface composes several other interfaces: IHaveDisplayName, IActivate, IDeactivate, IGuardClose and INotifyPropertyChangedEx
- Screen – Inherits from PropertyChangedBase and implements the IScreen interface. Additionally, IChild<IConductor> and IViewAware are implemented.
What this all means is that you will probably inherit most of your view models from either PropertyChangedBase or Screen. Generally speaking, you would use Screen if you need any of the activation features and PropertyChangedBase for everything else. CM’s default Screen implementation has a few additional features as well and makes it easy to hook into the appropriate parts of the lifecycle:
- OnInitialize – Override this method to add logic which should execute only the first time that the screen is activated. After initialization is complete, IsInitialized will be true.
- OnActivate – Override this method to add logic which should execute every time the screen is activated. After activation is complete, IsActive will be true.
- OnDeactivate – Override this method to add custom logic which should be executed whenever the screen is deactivated or closed. The bool property will indicated if the deactivation is actually a close. After deactivation is complete, IsActive will be false.
- CanClose – The default implementation always allows closing. Override this method to add custom guard logic.
- OnViewLoaded – Since Screen implements IViewAware, it takes this as an opportunity to let you know when your view’s Loaded event is fired. Use this if you are following a SupervisingController or PassiveView style and you need to work with the view. This is also a place to put view model logic which may be dependent on the presence of a view even though you may not be working with the view directly.
- TryClose – Call this method to close the screen. If the screen is being controlled by a Conductor, it asks the Conductor to initiate the shutdown process for the Screen. If the Screen is not controlled by a Conductor, but exists independently (perhaps because it was shown using the WindowManager), this method attempts to close the view. In both scenarios the CanClose logic will be invoked and if allowed, OnDeactivate will be called with a value of true.
So, just to re-iterate: if you need a lifecycle, inherit from Screen; otherwise inherit from PropertyChangedBase.
As I mentioned above, once you introduce lifecycle, you need something to enforce it. In Caliburn.Micro, this role is represented by the IConductor interface which has the following members:
- ActiveItem – A property that indicates what item the conductor is currently tracking as active.
- GetConductedItems – Call this method to return a list of all the items that the conductor is tracking. If the conductor is using a “screen collection,” this returns all the “screens,” otherwise this only returns ActiveItem.
- ActivateItem – Call this method to activate a particular item. It will also add it to the currently conducted items if the conductor is using a “screen collection.”
- CloseItem – Call this method to close a particular item. It will also remove it from the currently conducted items if the conductor is using a “screen collection.”
- ActivationProcessed – Raised when the conductor has processed the activation of an item. It indicates whether or not the activation was successful.(3)
- INotifyPropertyChangedEx – This interface is composed into IConductor.
You may have noticed that CM’s IConductor interface uses the term “item” rather than “screen” and that I was putting the term “screen collection” in quotes. The reason for this is that CM’s conductor implementations do not require the item being conducted to implement IScreen or any particular interface. Conducted items can be POCOs. Rather than enforcing the use of IScreen, each of the conductor implementations is generic, with no constraints on the type. As the conductor is asked to activate/deactivate/close/etc each of the items it is conducting, it checks them individually for the following fine-grained interfaces: IActivate, IDeactive, IGuardClose and IChild<IConductor>. In practice, I usually inherit conducted items from Screen, but this gives you the flexibility to use your own base class, or to only implement the interfaces for the lifecycle events you care about on a per-class basis. You can even have a conductor tracking heterogeneous items, some of which inherit from Screen and others that implement specific interfaces or none at all.
Out of the box CM has two implementations of IConductor, one that works with a “screen collection” and one that does not. We’ll look at the conductor without the collection first.
This simple conductor implements the majority of IConductor’s members through explicit interface mechanisms and adds strongly typed versions of the same methods which are available publicly. This allows working with conductors generically through the interface as well as in a strongly typed fashion based on the items they are conducting. The Conductor<T> treats deactivation and closing synonymously. Since the Conductor<T> does not maintain a “screen collection,” the activation of each new item causes both the deactivation and close of the previously active item. The actual logic for determining whether or not the conducted item can close can be complex due to the async nature of IGuardClose and the fact that the conducted item may or may not implement this interface. Therefore, the conductor delegates this to an ICloseStrategy<T> which handles this and tells the conductor the results of the inquiry. Most of the time you’ll be fine with the DefaultCloseStrategy<T> that is provided automatically, but should you need to change things (perhaps IGuardClose is not sufficient for your purposes) you can set the CloseStrategy property on Conductor<T> to your own custom strategy.
This implementation has all the features of Conductor<T> but also adds the notion of a “screen collection.” Since conductors in CM can conduct any type of class, this collection is exposed through an IObservableCollection<T> called Items rather than Screens. As a result of the presence of the Items collection, deactivation and closing of conducted items are not treated synonymously. When a new item is activated, the previous active item is deactivated only and it remains in the Items collection. To close an item with this conductor, you must explicitly call its CloseItem method.(4) When an item is closed and that item was the active item, the conductor must then determine which item should be activated next. Be default, this is the item before the previous active item in the list. If you need to change this behavior, you can override DetermineNextItemToActivate.
There are two very important details about both of CMs IConductor implementations that I have not mentioned yet. First, they both inherit from Screen. This is a key feature of these implementations because it creates a composite pattern between screens and conductors. So, let’s say you are building a basic navigation-style application. Your shell would be an instance of Conductor<IScreen> because it shows one Screen at a time and doesn’t maintain a collection. But, let’s say that one of those screens was very complicated and needed to have a multi-tabbed interface requiring lifecycle events for each tab. Well, that particular screen could inherit from Conductor<T>.Collection.OneActive. The shell need not be concerned with the complexity of the individual screen. One of those screens could even be a UserControl that implemented IScreen instead of a ViewModel if that’s what was required. The second important detail is a consequence of the first. Since all OOTB implementations of IConductor inherit from Screen it means that they too have a lifecycle and that lifecycle cascades to whatever items they are conducting. So, if a conductor is deactivated, it’s ActiveItem will be deactivated as well. If you try to close a conductor, it’s going to only be able to close if all of the items it conducts can close. This turns out to be a very powerful feature. There’s one aspect about this that I’ve noticed frequently trips up developers. If you activate an item in a conductor that is itself not active, that item won’t actually be activated until the conductor gets activated. This makes sense when you think about it, but can occasionally cause hair pulling.
Not everything in CM that can be a screen is rooted inside of a Conductor. For example, what about the your root view model? If it’s a conductor, who is activating it? Well, that’s one of the jobs that the Bootstrapper performs. The Bootstrapper itself is not a conductor, but it understands the fine-grained lifecycle interfaces discussed above and ensures that your root view model is treated with the respect it deserves. The WindowManager works in a similar way(5) by acting a little like a conductor for the purpose of enforcing the lifecycle of your modal (and modeless - WPF only) windows. So, lifecycle isn’t magical. All your screens/conductors must be either rooted in a conductor or managed by the Bootstrapper or WindowManager to work properly; otherwise you are going to need to manage the lifecycle yourself.
If you are working with WP7 or using the Silverlight Navigation Framework, you may be wondering if/how you can leverage screens and conductors. So far, I’ve been assuming a primarily ViewModel-First approach to shell engineering. But the WP7 platform enforces a View-First approach by controlling page navigation. The same is true of the SL Nav framework. In these cases, the Phone/Nav Framework acts like a conductor. In order to make this play well with ViewModels, the WP7 version of CM has a FrameAdapter which hooks into the NavigationService. This adapter, which is set up by the PhoneBootstrapper, understands the same fine-grained lifecycle interfaces that conductors do and ensures that they are called on your ViewModels at the appropriate points during navigation. You can even cancel the phone’s page navigation by implementing IGuardClose on your ViewModel. While the FrameAdapter is only part of the WP7 version of CM, it should be easily portable to Silverlight should you wish to use it in conjunction with the Silverlight Navigation Framework.
Ok. That’s a ton of theory and API explanation without any samples. Unacceptable. But, I’m tired of writing this article. So, you’ll just have to wait for the next part. I’ve got several screens/conductors samples to show, each one a little more complex (and interesting) than the one before. Stay tuned. It won’t be long.
1. These classes can also be used very easily to create a SupervisingController or even a PassiveView design if so desired. Normally, using these classes as view models will suffice, but since the base implementations Screen, Conductor<T> and Conductor<T>.Collection.OneActive all implement IViewAware, it’s easy to get a reference to the view and take one of these alternative approaches. Additionally, since all Caliburn.Micro code depends only on the interfaces, you could easily implement IConductor on top of a docking window manager or some other complex control or service.
2. Even though these classes do automatic UI thread marshalling, they are still safe to use in a unit test. Under the covers, these classes use CM’s Execute.OnUIThread utility method. This method calls a custom action to do the thread marshalling which is setup by the Bootstrapper. If you are working with these classes without running the Bootstrapper (as would be the case with testing), a default action is used which does not do any marshalling.
3. If activation would cause the closing of the ActiveItem, such as it would with Conductor<T>, and the active item’s CanClose returned false, then activation of the new item would not succeed. The current item would remain active.
4. Or if the item inherits from Screen, you can call the Screen’s TryClose method, which actually just calls Parent.CloseItem passing itself as the item to be closed.
5. In fact the WPF version of the Bootstrapper uses the WindowManager internally to show your MainWindow. I’ll have a whole article on the WindowManager coming soon.
10-08-2010 4:59 PM
Filed under: WPF, WPF/e, Caliburn, Featured, Silverlight, RIA, Tutorial, MVVM, UI Architecture, Caliburn Micro, WP7