I recently released a new open source project called simply UltraLight.mvvm. The purpose of this project is to make it easier to build MVVM-based applications that support tombstoning (a must) on Windows Phone 7. The DLL is 22KB and the source is less than 300 lines of code.
With that, the framework supports:
- Commands
- Command binding for buttons (with parameters)
- Support for binding commands to application buttons on the application bar
- Dialogs, both notification and confirmation
- Messaging using the event aggregator publisher/subscriber model
- Service location
- Design-time friendly view models
- Tombstone-friendly view models with control hooks for tombstone events
- Decoupled navigation support from the view model
- Decoupled visual state support from the view model
- Notify property changed using expressions instead of magic strings
- Dispatcher helper for UI thread access
Why not Jounce?
I think Jounce is overkill for the phone. Jounce specifically and explicitly relies on the Managed Extensibility Framework which is a phenomenal tool for building modular line of business applications for Silverlight. While the phone is Silverlight, and while there are certainly business applications on the phone, there are several reasons why I think it requires a completely different framework. These include:
- The importance of the back button behavior that creates a very tight coupling to the navigation framework that is built-in
- The fact that the applications are never launched from the web, but always executed as a first-class application on the phone
- The limited resources available on the phone
- The lack of certain key features that MEF leverages such as Reflection.Emit on the phone
- The unique requirements created by tombstoning
For this reason, I wanted something quick and easy that would get me 90% of the way without much fuss. That's what this framework is.
I created a sample application to demonstrate the use of the framework. The application is bundled with the source and of this writing there is not a formal release, only the source with two projects: the framework itself and a sample phone application. You can download this from the CodePlex site.
Getting Started: View Models
The first step is the view model. Some people won't like this but I created an interface with the "base" properties for the view model that I can use for my design, and then another interface that includes the framework's IViewModel
to apply to the runtime. Why won't everybody like this? Because it really ends up being an empty interface that intersects two other interfaces. However, it makes it easier to build my design view model without having to stub out properties in the base view model that the designer doesn't need. Here are the interfaces:
public interface IMainBase { string Text { get; } IActionCommand<object> EventCommand { get; } IActionCommand<object> StateCommand { get; } IActionCommand<object> DialogCommand { get; } IActionCommand<object> NavigateCommand { get; } } public interface IMainViewModel : IMainBase, IViewModel { }
With that, I can create a design-time view model with stubbed out properties:
public class DesignMainViewModel : IMainBase { public string Text { get { return "This is some design-time text."; } } public IActionCommand<object> EventCommand { get; set; } public IActionCommand<object> StateCommand { get; set; } public IActionCommand<object> DialogCommand { get; set; } public IActionCommand<object> NavigateCommand { get; set; } }
In this case I really only had to mock one thing, the text field. In the XAML for the main page, I can then reference the design view model using the design-time extensions like this:
<Grid d:DataContext="{d:DesignInstance design:DesignMainViewModel, IsDesignTimeCreatable=True}"/>
This will show up nicely in the designer. The rest of the layout is simple. I wrap a border and add some visual states to make the border red or yellow to show how the framework works with the Visual State Manager. I have a text box for you to enter text and show it bound both in the text box and in a text block. I have a button to change states, to send the text via the event aggregator, and to navigate to another page. Finally, there is an application button that allows you to "delete" (clear) the text after answering a dialog.
You can see the button/command binding in the button that changes the visual state - notice it passes the target state as a parameter:
<Button Grid.Row="4" Content="Red Border" MVVM:ButtonCommand.Command="{Binding StateCommand}" MVVM:ButtonCommand.CommandParameter="RedBorder" HorizontalAlignment="Center"/>
ActionCommand and Changing States
Let's take a look at the view model and how this is implemented with the ActionCommand
provided by the framework. The command to change the state is valid if a non-empty string is passed. The base class is wired to the visual state manager, so the view model can simply call the GoToVisualState
method. The command is wired like this:
StateCommand = new ActionCommand<object>( s => { GoToVisualState(s.ToString(), true); _lastViewState = s.ToString(); }, s=>s is string && !string.IsNullOrEmpty((string)s));
That's it - you can databind the command parameter, or, like I did, pass in a literal. When you click the button, the state is changed and you can see the transitions as it fades from one color to the next. I save the state for tombstoning - more on that later.
Binding the View Model
Of course, for that to work, the view model has to get bound. I don't mind a little code-behind to get the job done if it's straightforward and easy. First, in my application start up, I need to register the view model. I'm telling the framework to handle exactly one copy of my view model and give it back whenever I ask for it (if you want new ones, you can just create new ones - no problem there). When the application starts, I call a simple method that registers the view model:
private static void _RegisterViewModels() { UltraLightLocator.Register<IMainViewModel>(new MainViewModel()); }
Now it is set up to return that instance anytime I ask for it.
The framework provides an extension method to binding the view model to the view. I decided to go with a method that would take a framework element. Some people won't like this, but I have a good reason. The binding can affect how the datacontext is propagated through the hierarchy, and some controls used as the "root" control can behave differently when they are bound too soon. By allowing the element to be passed, you have more control over where in the visual tree it is bound and how. Most often, you'll do what the example does and simply bind it to the root grid, often called LayoutRoot
. Here is the code-behind for the main page:
public MainPage() { InitializeComponent(); this.BindToViewModel<IMainViewModel>(LayoutRoot); }
That's it - I just pass the contract for the view model and the element to bind to. Behind the scenes, the framework will attach to the loaded event of that element and wait to actually data-bind until it is fully loaded. If you are working with a control like the pivot control, which will throw errors if you bind to it too soon, this will avoid that problem. It will also set up a delegate to call into the Visual State Manager, and wire in the navigation context so the view model can raise navigation events. When you bind to a user control, it will walk the visual tree to find the host page in order to grab a valid navigation context.
Navigation
That takes us to navigation. The navigation button fires a navigation command. The command is wired to call a method, and the method looks like this:
private void _Navigate() { RequestNavigation("/SecondPage.xaml"); }
Just request navigation and pass the view. The framework handles casting this to a URI and then passing it to the navigation context. You can also call GoBack()
to cancel or go to a previous page.
The Event Aggregator
I used the same code for the event aggregator that I use in Jounce as it is lightweight and robust. The command to send the event is wired to publish whatever text has been entered. It is set up like this:
EventCommand = new ActionCommand<object>( o => UltraLightLocator.EventAggregator.Publish(Text), o=>!string.IsNullOrEmpty(Text));
Notice how easy it is to send a message - just grab the instance for the aggregator and publish it. It can be any type. To receive a message, you simply implement IEventSink<T>
where T is the type of message you want to listen for. In this case, the same view model also listens for its own message (hey, this is a demo, right?) You can see in the definition the IEventSink<string>
which requires you to create a method that is called with the message.
To subscribe, you pass an instance of the class that has implemented the interface. Because this is set up for only one message, the type doesn't have to be specified as it can be inferred from the interface:
UltraLightLocator.EventAggregator.SubscribeOnDispatcher(this);
Notice you have the option to subscribe directly, or if you know you'll be interacting with the UI thread you can subscribe on the dispatcher and the method will be called from the UI thread. If you implement multple interfaces, you just indicate the type of message you are subscribing for like this:
UltraLightLocator.EventAggregator.SubscribeOnDispatcher<string>(this);
Dialogs
I created a simple interface to abstract the dialog mechanism. It takes a title, a message, and a boolean to determine if it is a notification (you can only click OK) or a confirmation (OK or Cancel). When the message is received, a notification is sent in the receiving method:
public void HandleEvent(string publishedEvent) { Dialog.ShowMessage("Received Text", publishedEvent, false); }
Application Button Binding
One aspect of the phone I don't like is the fact that the application buttons are not directly bindable. There are some interesting solutions there but I decided to just make it simple: if you specify three buttons, return an array of three commands that map to the buttons. In this case, we have a "delete" button that clears the text field. It only makes sense to call it if there is text, so the command is set up like this:
DialogCommand = new ActionCommand<object>( o => _RequestDelete(), o => !string.IsNullOrEmpty(Text));
The delete request uses the dialog as well, this time to confirm the request:
private void _RequestDelete() { if (Dialog.ShowMessage("Confirm Delete","Click OK to Clear the Text.", true)) { Text = string.Empty; } }
Finally, the command is bound to the button by returning it in the button bar list:
public override IEnumerable<IActionCommand<object>> ApplicationBindings { get { return new[] { DialogCommand }; } }
If you have nothing to bind, you can just return an empty list.
Tombstoning
The final element to cover is tombstoning support. The interface ITombstoneFriendly
is provided to indicate the view model will explicitly support tombstoning. The pattern for tombstoning can be simplified to this:
- When tombstoned, save data
- When activated, load data
- When the application exits, clear the saved data so the application "starts fresh"
The tombstone interface provides two methods to save and then restore data. In the example, the text is saved, as well as the last view state. If you make the border red and tombstone, it will still be red when you return to the application. (Hint: using Sterling makes this a whole lot easier).
Here is the implementation of the interface for the sample application:
public void Deactivate() { var settings = IsolatedStorageSettings.ApplicationSettings; settings["ViewState"] = _lastViewState; settings["Text"] = Text; } public void Activate() { var settings = IsolatedStorageSettings.ApplicationSettings; if (settings.Contains("ViewState")) { _lastViewState = (string) settings["ViewState"]; GoToVisualState(_lastViewState, false); } if (settings.Contains("Text")) { Text = (string) settings["Text"]; } }
Also, in the application object, when the application exits normally, I clear the settings:
private void Application_Closing(object sender, ClosingEventArgs e) { var settings = IsolatedStorageSettings.ApplicationSettings; if (settings.Contains("ViewState")) { settings.Remove("ViewState"); } if (settings.Contains("Text")) { settings.Remove("Text"); } }
There is a special process to bind the view to the view model for the tombstone process. The code-behind for the main page adds two overrides that look like this:
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { this.DeactivatePage<IMainViewModel>(); base.OnNavigatedFrom(e); } protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { LayoutRoot.ActivatePage<IMainViewModel>(); base.OnNavigatedTo(e); }
Wiring these events causes the framework to cast the view model to ITombstoneFriendly
and call the appropriate method. Notice that the deactivate command extends the page and uses this
, but the activate command extends a framework element. Why is that? Once again, if you are using a pivot control or a panorama control, the navigation event happens before the control is fully loaded. Binding too soon will result in the controls throwing an exception. By extending from the element itself (i.e. if you use a pivot, you use the name of the pivot control instead of the layout root to call ActivatePage
) the framework knows to wait for that element to load. Only when the element is loaded will it call the tombstone method. It will also dispatch this on the UI thread to queue it behind any pending requests, allowing for a seamless transition from the tombstoned state.
Conclusion
As you can see, it is a very small framework but does quite a bit. It's by no means a magic "pull the lever and I have a full blown Windows Phone 7" application, but I believe it is solid enough to help anyone hit the ground running and small enough that you can modify or extend it to fit your specific needs. I look forward to any feedback and suggestions.