Activating a Conductor in Caliburn Micro
Working with Caliburn.Micro today I discovered something around the UI composition in the framework.
Briefly: Caliburn offers a number of interfaces to describe the behaviour of your application's components (ViewModels), such as IActivate, IDeactivate, and more. Handily it also gives us the Screen class that implements these and a few other interfaces (like the IScreen interface, for example!) such that we can build ViewModels that have lifecycle - an important part of application UI composition.
Additionally, we have the concept of screen conductors, that is classes whose responsibility is to co-ordinate the lifecycle of various pieces (typically, we co-ordinate screens, although the framework does not constrain us to this). An interesting point with screen conduction is that in order to control the lifecycle of items/screens being conducted, the conductor needs to be active itself.
From the documentation:
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, its 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.
Hair pulling, indeed, especially when invoking ActivateItem() on the conductor will actually bring the view for that item up on the UI, it just won't activate it - its IsActive flag remains false, and the lifecycle events - specifically OnDeactivated - won't fire.
OK, so in order to get the framework to hit my OnDeactivated override on my screen, I need to activate the conductor. Gotcha.
Now... where's that API for actually activating the conductor? Have a little rummage in the doco, and we come up with...
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
Now, my bootstrapper is bootstrapping my ShellViewModel and that contains a few top-level pieces, like HeaderViewModel and MainViewModel. Within the MainViewModel I have another couple of pieces, including my WorkspacesViewModel, which is a Conductor<IScreen>.Collection.OneActive - basically, the Workspaces area is the big part of the UI used to show the current 'module' and if I'm trying to show, for example, the customers module, I add a CustomersViewModel to the Items in WorkspacesViewModel and call ActivateItem() passing in that CustomersViewModel instance as a parameter.
What I had to do, to get this working, is make the ShellViewModel a Conductor<object> and activate the MainViewModel; I then made the MainViewModel a Conductor<object> too, and activated the WorkspacesViewModel - basically, used the fact that the Bootstrapper activates the shell and cascaded that activation down to the part where I needed it.
I'm unconvinced by this approach but it certainly works, and from the documentation I can see why. I will, however, update this post if I can work out how to, as Rob suggests, 'manage the lifecycle myself'
Update: Many thanks to Rob, who points out 3 ways of skinning this cat far more cleanly than my clumsy attempt...
By far the nicest is option 3, where we write
ScreenExtensions.TryActivate(this);
in the constructor of the screen conductor to activate it.


Comments
Rob
If you don't actually need the features of conductors at all levels, remember that you can just implement individual interfaces. So, you could implement IActivate on your ShellViewModel and the Bootstrapper would call that. Then, in your implementation of Activate, you could just call Activate on your WorkspacesViewModel. There's an extension method to do this called, TryActivate. Alternatively, could set up a relationship between the ShellViewModel and the WorkspacesViewModel by calling workspacesViewModel.ActivateWith(shellViewModel) That method links the activation of one view model to another. They both need to implement IActivate for it to work. As a third solution, which doesn't involve implementing IActivate and is probably the simplest solution, you could just call TryActivate in the ctor of the WorkspacesViewModel. It's an extension method, so you have to do it like this: this.TryActivate().
Ian Randall
Thanks so much, Rob - the
TryActivatemethod is exactly the piece I was looking for...Just a minor point for other folks on the syntax, however - it isn't an extension method, so
should actually be
Rob
Oops. It seams I forgot how to use my own framework ;)