When my “Build Your Own MVVM Framework” talk was chosen for Mix10, I put a temporary hold on this series of blog posts. I wanted to dedicate significant time to working on a sample framework and demo that would make a top notch Mix presentation. After giving the talk, I received a ton of positive feedback on the techniques/framework that I had demonstrated. I was approached by several companies and individuals who expressed an interest in a more “official” version of what I had shown. That, coupled with the coming of Windows Phone 7, impressed upon me a need to have a more “lean and mean” framework. As a result, I’ve spent my spare time over the last six weeks building just that. My vision was to take 90% of Caliburn’s features and squash them into 10% of the code. I also hoped I could fit it in about 50k, give or take. I started with the Mix sample framework, then used an iterative process of integrating Caliburn v2 features, simplifying and refactoring. I continued along those lines until I had what I felt was a complete solution that mirrored the full version of Caliburn v2, but on a smaller scale. I call this new framework Calburn.Micro.
Caliburn.Micro consists of one ~50k assembly that builds for WPF4, SL4 and WP7*. It has a single dependency, System.Windows.Interactivity, which you are probably already using regularly in development. Caliburn.Micro is about 2,000 LOC total, so you can easily go through the whole codebase in an afternoon and hold it in your head. That’s less than 10% of the size of Caliburn v2, which is running around 27,000 LOC and is a lot harder to grasp in a short time. So, it looks like I was able to meat my code quantity and size goals. But the best part is that I believe I was able to put something together that contains all of the features I consider most important in Caliburn. Here’s a brief list:
- ActionMessages – The Action mechanism allows you to “bind” UI triggers, such as a Button’s “Click” event, to methods on your View-Model or Presenter. The mechanism allows for passing parameters to the method as well. Parameters can be databound to other FrameworkElements or can pass special values, such as the DataContext or EventArgs. All parameters are automatically type converted to the method’s signature. This mechanism also allows the “Action.Target” to vary independently of the DataContext and enables it to be declared at different points in the UI from the trigger. When a trigger occurs, the “message” bubbles through the element tree looking for an Action.Target (handler) that is capable of invoking the specified method. This is why we call them messages. The “bubbling” nature of Acton Messages is extremely powerful and very helpful especially in master/detail scenarios. In addition to invocation, the mechanism supports a “CanExecute” guard. If the Action has a corresponding Property or Method with the same name, but precede by the word “Can,” the invocation of the Action will be blocked and the UI will be disabled. Actions also support Coroutines (see below). That’s all fairly standard for existing Caliburn users, but we do have a few improvements in Caliburn.Micro that will be making their way into the larger framework. The Caliburn.Micro implementation of ActionMessages is built on System.Windows.Interactivity. This allows actions to be triggered by any TriggerBase developed by the community. Furthermore, Caliburn.Micro’s Actions have full design-time support in Blend. Code-centric developers will be happy to know that Caliburn.Micro supports a very terse syntax for declaring these ActionMessages through a special attached property called Message.Attach.
- Action Conventions – Out of the box, we support a set of binding conventions around the ActionMessage feature. These conventions are based on x:Name. So, if you have a method called “Save” on your ViewModel and a Button named “Save” in your UI, we will automatically create an EventTrigger for the “Click” event and assign an ActionMessage for the “Save” method. Furthermore, we will inspect the method’s signature and properly construct the ActionMessage parameters. This mechanism can be turned off or customized. You can even change or add conventions for different controls. For example, you could make the convention event for Button “MouseMove” instead of “Click” if you really wanted.
- Binding Conventions – We also support convention-based databinding. This too works with x:Name. If you have a property on your ViewModel with the same name as an element, we will attempt to databind them. Whereas the framework understands convention events for Actions, it additionally understands convention binding properties (which you can customize or extend). When a binding name match occurs, we then proceed through several steps to build up the binding (all of which are customizable), configuring such details as BindingMode, StringFormat, ValueConverter, Validation and UpdateSourceTrigger (works for SL TextBox and PasswordBox too). Finally, we support the addition of custom behaviors for certain scenarios. This allows us to detect whether we need to auto-generate a DataTemplate or wire both the ItemsSource and the SelectedItem of a Selector based on naming patterns.
- Screens and Conductors – The Screen, ScreenConductor and ScreenCollection patterns enable model-based tracking of the active or current item, enforcing of screen lifecycles and elegant shutdown or shutdown cancellation in an application. Caliburn.Micro’s implementation of these patterns is an evolution of the one found in Caliburn and supports conducting any type of class, not just implementations of IScreen. These improvements are being introduced back into Caliburn. You’ll find that Caliburn.Micro’s screen implementation is quite thorough and even handles asynchronous shutdown scenarios with ease.
- Event Aggregator – Coming in at about 75LOC, Caliburn.Micro’s EventAggregator is simple yet powerful. The aggregator follows a bus-style pub/sub model. You register a message handler with the aggregator, and it sends you any messages you are interested in. You declare your interest in a particular message type by implementing IHandle<TMessage>. References to handlers are held weakly and publication occurs on the UI thread.
- Coroutines – Any action can optionally choose to return IResult or IEnumerable<IResult>, opening the door to a powerful approach to handling asynchronous programming. Furthermore, implementations of IResult have access to an execution context which tells them what ActionMessage they are executing for, what FrameworkElement triggered the messsage to be sent, what instance the ActionMessage was handled by (invoked on) and what the View is for that instance. Such contextual information enables a loosely-coupled, declarative mechanism by which a Presenter or View-Model can communicate with it’s View without needing to hold a reference to it at any time.
- ViewLocator – For every ViewModel in your application, Caliburn.Micro has a basic strategy for locating the View that should render it. We do this based on naming conventions. For example, if your VM is called MyApplication.ViewModels.ShellViewModel, we will look for MyApplication.Views.ShellView. Additionally, we support multiple views over the same View-Model be attaching a View.Context in Xaml. So, given the same model as above, but with a View.Context=”Master” we would search for MyApplication.Views.Shell.Master. Of coarse, all this is customizable.
- WindowManager – This service provides a View-Model-centric way of displaying Windows (ChildWindow in SL and Window in WPF). Simply pass it an instance of the VM and it will locate the view, wrap it in a Window if necessary, apply all conventions you have configured and show the window.
- PropertyChangedBase and BindableCollection – What self respecting WPF/SL framework could go without a base implementation of INotifyPropertyChanged? The Caliburn.Micro implementation enables string and lambda-based change notification. It also ensures that all events are raised on the UI thread. BindableCollection is a simple collection that inherits from ObservableCollection<T>, but that ensures that all its events are raised on the UI thread as well.
- Bootstrapper – What’s required to configure this framework and get it up and running? Not much. Simply inherit from Bootsrapper and add an instance of your custom bootstrapper to the Application’s ResourceDictionary. Done. If you want, you can override a few methods to plug in your own IoC container, declare what assemblies should be inspected for Views, etc. It’s pretty simple. You’ll see how that works in my next blog post.
- Logging – Caliburn.Micro implements a basic logging abstraction. This is important in any serious framework that encourages Convention over Configuration. All the most important parts of the framework are covered with logging. Want to know what conventions are or are not being applied? Turn on logging. Want to know what actions are being executed? Turn on logging. Want to know what events are being published? Turn on logging. You get the picture.
- MVVM and MVP – In case it isn’t obvious, this framework enables MVVM. MVVM isn’t hard on it’s own, but Caliburn.Micro strives to go beyond simply getting it done. We want to write elegant, testable, maintainable and extensible presentation layer code…and we want it to be easy to do so. That’s what this is about. If you prefer using Supervising Controller and PassiveView to MVVM, go right ahead. You’ll find that Caliburn.Micro can help you a lot, particularly it’s Screen/ScreenConductor implementation.*** If you are not interested in any of the goals I just mentioned, you’d best move along. This framework isn’t for you.
Just to be clear, this isn’t a toy framework. I’m using it on two Silverlight 4 applications that I’m building right now. The experience has been good and I haven’t had a need for anything beyond what it provides. As I said, I really focused on supporting the core and most commonly used features from Caliburn v2. In fact, Caliburn.Micro is going to be my default framework moving forward and I recommend that if you are starting a new project you begin with the Micro framework. I’ve been careful to keep the application developer API consistent with the full version of Caliburn. In fact, some of the improvements I made in Caliburn.Micro are going to be folded back into Caliburn v2 in the coming months. What’s the good part about that? You can start developing with Caliburn.Micro, then if you hit edge cases or have some other need to move to Caliburn, you will be able to do so with little or no changes in your application.**
In the coming months I’ll be blogging extensively about Caliburn.Micro. In fact, I’m planning to take you on a tour of the entire codebase. Keeping with the spirit of “Build Your Own…” I want developers to understand how this little framework works, inside and out. I’ve intentionally chosen Mercurial for source control, because I want developers to take ownership. While I’ve done some work to make the most important parts of the framework extensible, I’m hoping to see many Forks, each with their own customizations unique to their application’s needs.
*When compressed into a .xap, Caliburn.Micro is about 24k. Also, it can be built for WPF3.5 and SL3. Though the SL3 version will be short one feature at runtime (ActionMessage Parameters databound to Elements) due to unfinished work in the SL3 DependencyObject/DepencenyProperty system. That’s actually true for the current WP7 build as well.
**What are the differences between Caliburn and Caliburn.Micro? Caliburn has more features and supports a lot of edge cases. Also, the configuration and extensibility models are different. I’ll be talking about this in more detail in the future.
***The whole ScreenConductor piece is actually a Supervising Controller implementation…shshshsh…don’t tell anyone…but I almost always use a combination of MVVM and Supervising Controller (and other patterns). I like to use the best tool for the job.
07-04-2010 4:45 PM
Filed under: WPF, Xaml, databinding, WPF/e, Caliburn, Featured, Silverlight, RIA, Tutorial, MVVM, UI Architecture, Caliburn Micro