Introduction
The Caliburn Micro framework is one of the first MVVM frameworks to become available for Widows Phone 7 (WP7) and it stands out as very productive thanks to the extensive use of "convention over configuration".
Background
When creating Silverlight applications for Windows Phone 7, you are offered basic MVVM implementations, but most will find these inadequate for any non-trivial application. I do recommend that people learning WP7 Silverlight programming do not use a framework, as this will expose them more to the platform, and make them appreciate more the frameworks they are using.
About the Caliburn Framework
The original Caliburn was created as an Open Source (MIT License) MVVM Framework for WPF and later adopted for Silverlight. At the time of writing of this article, the project has reached version 2.0. And right on the project page, you'll find the following message:
"STOP! If you are new here, we recommend a simpler framework with 90% of the features and power! Check out Caliburn.Micro".
Also, the name "Caliburn" is one of the different spelling/names of "Excalibur".
Enter Windows Phone 7
Now if you are developing for Silverlight and WP7, it is a natural fit to choose a lighter framework, and if you are starting development, why not choose a simpler one as well?
Here's some extra information about the history and creation of this framework: link.
Alternative WP7 MVVM Frameworks
There are a few other popular MVVM frameworks out there:
The Problem We're Solving
I'll demonstrate the benefits of using the Caliburn Micro framework by writing a WP7 client monitoring the statuses of Cruise Control.Net.
If you are familiar with what Cruise Control.Net (CC) does, I recommend following the link above, but the elevator pitch would be "It is an Automated Continuous Integration server, monitoring your source control, running builds, tests, and code analysis, helping maintain your product in a state of successful build and correct function" - it's free, Open Source, and written in .NET.
Using the Caliburn Micro for Windows Phone 7
The download page of Caliburn Micro project contains a single package that you have to download and install that will install the binaries, samples, and Visual Studio templates you'll need.
At the time of writing of this article, this project was not in NuGet, but I'm guessing you'll soon be able to get it that way, too.
Update
April 20 2011: Now there is a Caliburn Micro NuGet package (and here's an article about adding NuGet support for Caliburn Micro).
If you have NuGet 1.2 or later installed, from the Package Manager Console, just type:
Install-Package Caliburn.Micro
Empty Project
As part of the Visual Studio Templates installed, you'll find this project template:
After project creation, the readme.txt file will be opened pointing you to the Caliburn Micro project page, which is currently the best place to look for documentation.
The project tree will look something like this:
Notice a new folder "Framework", new class "AppBootstrapper
", and a Main Page with no code-behind.
The "Framework" folder contains the entire Caliburn Micro source code below it (and there are no extra assembly references):
App and App Bootstrapper
App.xaml has been left almost entirely empty - there's only an AppBootstrapper
reference in the resources and this is because all the logic from this class has been redirected to AppBoostrapper
, which will do a few extra things for us.
IoC/Dependency Injection
As part of a new project, you'll also get a basic Inversion of Control container, but you can replace that container with any other for WP7. To setup the container, AppBootstrapper
contains the basic code in the overridden Configure
method:
protected override void Configure()
{
container = new PhoneContainer(this);
container.RegisterPerRequest(typeof(MainPageViewModel),
"MainPageViewModel", typeof(MainPageViewModel));
container.RegisterInstance(typeof(INavigationService),
null, new FrameAdapter(RootFrame));
container.RegisterInstance(typeof(IPhoneService), null,
new PhoneApplicationServiceAdapter(PhoneService));
AddCustomConventions();
}
Convention Over Configuration
Now, about the AddCustomConventions
method: modern development tends to be a very repetitive and verbose process that we have learned to put up with mostly due to the vastly improved IDEs. XAML platforms (WPF, WF, Silverlight, WP7) are no exceptions. You'll often find yourself repeating common patterns - like this one:
<Button x:Name="Insert"
Content="insert"
IsEnabled="{Binding CanInsert}"
Click="Insert_OnClick"/>
And this is just a tip of the iceberg. All of this is due to the fact that you can set all of these properties and bindings in a variety of ways and due to increased flexibility, we have increased complexity. To deal with complexity, developers tend to use patterns to simplify their mental maps of projects (like in the case above - notice that there's an "Insert" text in every line). Why just not go a step further and say: we'll use these conventions by default and maybe all we need is:
<Button x:Name="Insert"
Content="insert"/>
Caliburn Micro gives you this goodness, thanks to the usage of those conventions and much more:
In the sample above, we had to have a method in the code-behind of the page that would have had to then find the ViewModel and invoke the extra "Insert
" method. With CM, you'll get all of that for free:
- By having a public
Insert
method in your View Model, CM will make sure that a click from the button "Insert" is passed to the ViewModel's Insert
method
- If you have a public method returning a
bool
or a property of type bool
with the name "CanInsert
", CM will bind this to the IsEnabled
property of the button
And this is just the start...
Yes, but!
But what if you don't like conventions that have been setup for you, or you need to override them in some cases?
- Every binding and value that is set before Caliburn Micro attaches to a View Model that will not be changed. This allows to you to override convention bindings.
- There is an API to change conventions, and you can check out an article about it here: link.
Pairing Views and View Models
Since in WP7 you are navigating between pages, they (as views) will be the one that will need to somehow find and initialize the appropriate View Models. Again, you are most likely to have "AddServerPage.xaml" and "AddServerPageViewModel.cs" as a pair so let's allow Caliburn Micro to pair these up for us:
container.RegisterPerRequest(typeof(AddServerPageViewModel),
"AddServerPageViewModel", typeof(AddServerPageViewModel));
By just doing this, you will, whenever you navigate to your AddServerPage
, have your ageView
model ready and waiting in the page's DataContext
.
Base Classes for View Models
It's possible to use just plain-old-CLR-objects (POCO) for your View Models. In most cases, you will want to implement the INotifyPropertyChanged
interface. Caliburn Micro has quite a few base classes for View Models to choose from:
PropertyChangedBase
- is a basic implementation of INotifyPropertyChanged
with a few extras
Screen
- which offers a lot of events that you can hook-up to, like OnActivate
, and the option to reach-out to attached views
Conductor
- and especially Conductor<t>.Collection.OneActive
are pre-assembled View Model kits for when you are working with collections
Using ProperyChangedBase
Let's dig into AddServerPageViewModel
and have it derive from this base class:
public class AddServerPageViewModel : Framework.PropertyChangedBase
{
public AddServerPageViewModel()
{
_fullAddress = new Uri("http://ccnet.wheelmud.net" +
"/XmlServerReport.aspx");
}
Uri _fullAddress;
public string ServerName
{
get { return _fullAddress.Host; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException();
var ub = new UriBuilder(_fullAddress)
{
Host = value,
};
SetFullAddress(ub.Uri);
}
}
public int PortNumber
{
get { return _fullAddress.Port; }
set
{
var ub = new UriBuilder(_fullAddress)
{
Port = value,
};
SetFullAddress(ub.Uri);
}
}
public string FullAddress
{
get { return _fullAddress.ToString(); }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException();
SetFullAddress(new Uri(value));
}
}
void SetFullAddress(Uri uri)
{
_fullAddress = uri;
CanAddServer = _fullAddress.IsAbsoluteUri;
Refresh();
}
}
We have three properties that are all tied together and help with editing of the server URL. Since all three are likely to change together, I am just calling the Refresh
method from PropertyChangedBase
, which will notify the View that all of these properties have changed. Handy. For View, all we need to add to ContentPanel
is:
<StackPanel>
<TextBlock><Run Text="Server DNS:"/></TextBlock>
<TextBox x:Name="ServerName" InputScope="Url" />
<TextBlock><Run Text="Server Port:"/></TextBlock>
<TextBox x:Name="PortNumber" InputScope="Number"/>
<TextBlock><Run Text="Full Uri:"/></TextBlock>
<TextBox x:Name="FullAddress" InputScope="Url"/>
</StackPanel>
Tip: When testing this page in the emulator, instead of typing with mouse on the on-screen keyboard, you can press Page Up on the computer's keyboard to simulate the phone's hardware keyboard.
Using Conductor<t>.Collection.OneActive
Inheriting your View Model from this base class allows for simple creation of View Models for collections and in this variation, it allows and maintains only one active child.
This base class has an Items
property which is matched in the View with a List Box or another Items
control-derived class with x:Name="Items"
. The active item is bound to SelectedItem
.
Problem of Navigation
You might have noticed that pages and behaviours (if using Blend) have the power to navigate to other pages, but this is not really desirable, as we want View Models to be in control of navigation. After navigating to a page, it can access the passed arguments by parsing the query string parameters, but again - this is what we want the View Model to do.
Caliburn Micro solves these two problems by exposing a rich INavigationService
interface and making it easily available to your View Models, thanks to the beauty of Dependency Injection. All you need to get this service in your View Model is to have a constructor that will take this interface as a parameter:
public AddServerPageViewModel(INavigationService navigationService) {}
The second part of the problem (parsing the arguments passed) is handled automatically, thanks to Convention over Configuration. If you are navigating to the URI "/ProjectPage.xaml?projectId=17" and you have a ProjectId
property on the View Model for that page, Caliburn will auto-magically populate the ProjectId
property with the specified parameter value.
If you need more control over the lifetime of the View in your View Model, just inherit from the Screen
base class or one of the more specialized sub-classes.
WP7's Un-Bind-able Application Bar
Part of the WP7 UI is the Application Bar, which in this first version is not bind-able. As a matter of fact, you'll have to use indexes to manipulate buttons, usually directly from your View.
Caliburn Micro for WP7 provides significant improvements in this area - allowing you to bind the App Bar to your View Model.
What you'll need to do is just use CM's AppBarButton
and AppBarMenuItem
inside the standard App Bar and then identify what it needs to bind to by specifying the "Message
" property.
I like to do this from inside of Blend and thankfully you can do that too. Open your project in Blend (you can right-click on the project in Visual Studio to do this), then open the page you want to add an App Bar to, and select the page in the objects list:
Then in Properties, section "Common Properties":
Press the "New" button to create the application bar:
Notice the "Buttons (Collection)" and press the "..." button to edit a collection of buttons that will be in this application bar:
Now "Add another item" has a drop-down button and if you want the WP7 framework's button, you can use this drop-down, but to use Caliburn Micro's improved version of this button, just press the "Add another item" button. Select the Object dialog that will pop-up:
I often find that just typing "AppBar" will quickly filter out unwanted types:
Select AppBarButton
from that list and press the OK button.
The added items will look a lot like the standard button:
- The IconUri drop down allows for quick selection of images, including stock ones, which will also be added to the project, if needed
- Text to be displayed below the button
One extra Message
field is Caliburn Micro's field that maps to the Command
in the View Model.
Not only does this auto-map to the "Message" Command (method), but also to "CanMessage
" (method of property) that will control enabling or disabling of the button.
C# 5 is a Long Way Away
Ever since Silverlight 2, the only way to make Web Service calls was asynchronously. Because we have a multi-threading environment and Microsoft is preventing us from shooting ourselves in the foot by forcing us to do all UI activities on the UI thread, doing network calls in WP7 is slightly painful. C# 5 is going to introduce the "async" keyword which is going to make this much easier, but right now, we have to program without it.
This introduces a few big questions:
- How can we leave as much as possible inside MVVM and keep it clean
- How to offload all the possible processing on non-UI threads to keep an app performing
- How to present the user with good loading/processing feedback and possibly an option to cancel it
One of the trickiest questions for me was:
- How to control animations to indicate busy status from the View Model
And then I found this blog post: link. Caliburn Micro has this great feature that methods which are called to invoke commands could also return IEnumerable<IResult>
. Caliburn will go over the returned results, executing them one by one until they are all completed, one cancels, or throws/returns an error. This by itself is great, but there's more! The IResult
interface looks like this:
public interface IResult
{
void Execute(ActionExecutionContext context);
event EventHandler<resultcompletioneventargs> Completed;
}
public class ActionExecutionContext
{
public ActionMessage Message;
public FrameworkElement Source;
public object EventArgs;
public object Target;
public DependencyObject View;
public MethodInfo Method;
public Func<bool> CanExecute;
public object this[string key];
}
Thanks to the Source
property in ActionExecutionContext
we have access to objects on the actual page. This is good unless you abuse it - so try to keep it simple and no tight coupling. My choice is to use the Visual State Manager to change the state of the page:
public IEnumerable<iresult> AddServer()
{
CanAddServer = false;
Refresh();
yield return new SetVisualState("Adding");
var readServer = _readServerFactory();
readServer.Server.Name = ServerName;
readServer.Server.Uri = FullAddress;
yield return readServer;
_serverDataManager.AddServer(readServer.Server);
yield return new SetVisualState("Default");
CanAddServer = true;
_navigationService.GoBack();
}
This sample is from AddServerPageViewModel
and it will execute these operations asynchronously, but for me, the best part is the fact that I can easily and cleanly manipulate Visual State:
public class SetVisualState : IResult
{
readonly string _state;
public SetVisualState(string state)
{
_state = state;
}
public void Execute(ActionExecutionContext context)
{
VisualStateManager.GoToState((Control)context.View, _state, true);
Completed(this, new ResultCompletionEventArgs());
}
public event EventHandler<resultcompletioneventargs> Completed = delegate { };
}
Conclusion
Caliburn Micro is a fantastic framework for WP7, and it holds the promise of great productivity boost, but still it has some issues that will turn away a lot of people:
- It will probably restructure your code differently than what you might be used to
- Sometimes you'll waste time trying to figure out how to perform an action which looks to you pretty straightforward
- Working with design-time data and Caliburn Micro is possible, but those pages will have to use standard XAML bindings
Notes
- I'm always setting Input Scope on Text Boxes and if Visual Studio does not give you intelli-sense, Blend will.
- Your app should start fast. Don't use a splash screen unless you are certain that you need it.
- You can find some great articles that serve as Caliburn Micro documentation here: http://caliburnmicro.codeplex.com/documentation.
- This project will continue on: http://ccnetwp7.codeplex.com/ to add features like:
- Ability to monitor and control builds from your phone
- Live Tile + Server Notifications
History
- May 31 2011 - Initial version.
- April 4 2011 - Minor clean-ups.