In my previous post I talked about the benefits of using co-routines in Caliburn Micro to ease any interactions with the View from the View Model. In that case it was the use of the Visual State Manager; in this post we’ll cover managing storyboards and animation.
We’ll use code from an older post around how to create one off event handlers. What I’ve done is encapsulate that logic into an extension method ToObservable.
public static IObservable<IEvent<EventArgs>> ToObservable(this Storyboard storyboard)
{
if(storyboard == null)
throw new ArgumentNullException("storyboard");
return Observable.FromEvent((EventHandler<EventArgs> e) => new EventHandler(e),
e => storyboard.Completed += e,
e => storyboard.Completed -= e);
}
In the BeginStoryboardResult we verify the view is a FrameworkElement (and therefore can contain Resources). We then load the Storyboard from the Resources collection. Using the extension method we wire the Completed event of the Storyboard to the completion of the IResult.
public class BeginStoryboardResult : ResultBase
{
private readonly string storyboardName;
public BeginStoryboardResult(string storyboardName)
{
this.storyboardName = storyboardName;
}
public string StoryboardName
{
get { return storyboardName; }
}
public override void Execute(ActionExecutionContext context)
{
if(!(context.View is FrameworkElement))
throw new InvalidOperationException("View must be a framework element to use BeginStoryboardResult");
var view = (FrameworkElement)context.View;
if(!view.Resources.Contains(StoryboardName) || !(view.Resources[StoryboardName] is Storyboard))
throw new InvalidOperationException(String.Format("View doesn't the contain a Storyboard with the key {0} as a resource", StoryboardName));
var storyboard = (Storyboard)view.Resources[StoryboardName];
storyboard.ToObservable().Take(1)
.Subscribe(e => OnCompleted());
storyboard.Begin();
}
}
After that it’s pretty much just starting the actual storyboard.
Again one of the main benefits of result an IResult like this is we still maintain separation between the view model and the view, we can now create unit tests that test how the view model plays storyboards without requiring the actual storyboard.
Well it's been quite a while since I've posted anything having spent the last five weeks in Canada getting married and St Lucia on our honeymoon. As usual a lot happens while you're away, with Mango getting closer to release (and the marketplace opening for Mango apps), the unveiling of Windows 8 at Build and other smaller news. With all this there's been a lot to catch up but after five weeks away from development I've been itching to get back to it. I’m currently working on Mango updates for Left to Spend and To Do Today, I’m taking the opportunity to rebuild bits of To Do Today with some of the techniques I’ve learnt in the last year so I’ll hopefully be discussing some of those as I go.
One of the cool and often over looked features in Caliburn Micro is the IResult interface and its use in Coroutines, there's some excellent documentation on the Caliburn Micro website so I won't go into a lot of detail regarding how they work. One of the aspects I really like (besides helping to simplify asynchronous code) is that they better enable View, View Model separation even when you need to interact directly with the View.
A great example of this is the Visual State Manager, the visual state of a screen is something that there's a definite use case to control from the View Model as it’s a smooth way to have animated states within the page. Previously my approach has been to expose a string "State" property on the View Model and bind this to a custom attached dependency property. This works but I feel it has a few weaknesses, a page can be in multiple states at any one time due to Visual State Groups (the page will be in a state for each group). This isn't represented well by the State property and in reality really hides that fact. It also doesn’t control the need to turn on and off state transitions.
Because the ActionExecutionContext passed to the IResult has a reference to the View we can manipulate the Visual State Manager directly in the IResult without worrying about bindings. We’ve also preserved the distinction between the View and View Model so we can unit test our View Model changes the state without execution of the IResult.
So what does the code look like, pretty simple really, I have a ResultBase class to handle some of the plumbing in IResult. From this class I create my VisualStateResult that takes the appropriate parameters. On Execute we verify the View is a Control and then invoke GoToState.
public abstract class ResultBase : IResult
{
public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
protected virtual void OnCompleted()
{
OnCompleted(new ResultCompletionEventArgs());
}
protected virtual void OnError(Exception error)
{
OnCompleted(new ResultCompletionEventArgs
{
Error = error
});
}
protected virtual void OnCompleted(ResultCompletionEventArgs e)
{
Caliburn.Micro.Execute.OnUIThread(() => Completed(this, e));
}
public abstract void Execute(ActionExecutionContext context);
}
public class VisualStateResult : ResultBase
{
private readonly string stateName;
private readonly bool useTransitions;
public VisualStateResult(string stateName, bool useTransitions = true)
{
this.stateName = stateName;
this.useTransitions = useTransitions;
}
public string StateName
{
get { return stateName; }
}
public bool UseTransitions
{
get { return useTransitions; }
}
public override void Execute(ActionExecutionContext context)
{
if(!(context.View is Control))
throw new InvalidOperationException("View must be a Control to use VisualStateResult");
var view = (Control)context.View;
VisualStateManager.GoToState(view, StateName, UseTransitions);
OnCompleted();
}
}
yield return new VisualStateResult("Complete", useTransitions: false);
One of the recommendations from Microsoft for better performance from your WP7 application is to begin work on your page in the first LayoutUpdated event after OnNavigatedTo.
In MVVM frameworks this wasn't the easiest to achieve, Ben Gracewood wrote an awesome post on how you can achieve this in Caliburn Micro 1.0 by creating your own FrameAdapter. I recommend reading this post for this as well as its discussion on co-routines (one of the lesser used features of Caliburn Micro).
In the 1.1 release the process becomes a little easier, the class ViewAware (from which Screen derives) has a method called OnViewReady that will be called on the first layout updated event after OnNavigatedTo.
protected internal virtual void OnViewReady(object view);
There's one small problem this this method, it's no a co-routine, something I'm making more and more use of in my projects. So how can we go about extending this?
Thankfully it's pretty easy to invoke Actions in CM, so on the OnViewReady method we invoke an action of the same name passing in the view (this is important since the results in the co-routine may want to interact with the view).
We can then create a virtual OnViewReady co-routine method that does nothing. All done!
public abstract class ViewModelBase : Screen
{
protected override void OnViewReady(object view)
{
Caliburn.Micro.Action.Invoke(this, "OnViewReady", (DependencyObject)view);
}
public virtual IEnumerable<IResult> OnViewReady()
{
yield break;
}
}
On a personal note, I'll be out of the country for the next five weeks getting married to a fantastic canadian girl so the blog will obviously be a little quiet. I may need to sneak back for a post or two since a semi secret Windows Phone project I've been working on is going to be launched by the client in the next month.
One of the best things I find in the MVVM framework Caliburn Micro is the convention based binding for properties and methods. They allow a lot of binding between your view and view model to happen "auto-magically" based on the same of elements in your view.
If you haven't read the documentation I highly recommend reading All about Conventions as it explains a lot of the internal plumbing that makes the conventions work.
You can add more conventions really easily through the ConventionManager class, these are usually set up in the Configure method of your Bootstrapper. For instance if we wanted to add the standard convention for buttons we'd use the following code.
AddElementConvention<ButtonBase>(ButtonBase.ContentProperty, "DataContext", "Click");
The first parameter is the property on the element to bind to if we match against a property on the view model. For our button example if we had a string property on the ViewModel it would be the text of the Button.
The second is the property to use if we refer to the element as a parameter of an action, currently this isn't possible in Windows Phone at the moment (this looks to be changing in Mango but I haven't tested it yet) so can be ignored for now.
The third is the name of the event to trigger any actions specified on the element or for ones matched by convention, for our Button it makes sense that the Click event should trigger the action.
One other nice part of the ConventionManager in Caliburn is that you can provide conventions for base types and any that inherit from that type will also inherit it's conventions. This makes writing up all the conventions for a library considerably easier and that some of the base conventions in Caliburn will already suffice.
By default Caliburn has a lot of conventions built in, you can see most of them in the article linked above. What it's missing are conventions for more of the phone only controls, including the Silverlight Toolkit. This makes sense as we shouldn't expect to have Caliburn depend upon those assemblies just for some conventions.
I thought I'd provide some of the conventions I've used in projects recently.
// Phone Controls (from the Caliburn Sample
ConventionManager.AddElementConvention<Pivot>(ItemsControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
if(ConventionManager.GetElementConvention(typeof(ItemsControl)).ApplyBinding(viewModelType, path, property, element, convention))
{
ConventionManager.ConfigureSelectedItem(element, Pivot.SelectedItemProperty, viewModelType, path);
ConventionManager.ApplyHeaderTemplate(element, Pivot.HeaderTemplateProperty, viewModelType);
return true;
}
return false;
};
ConventionManager.AddElementConvention<Panorama>(ItemsControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
if(ConventionManager.GetElementConvention(typeof(ItemsControl)).ApplyBinding(viewModelType, path, property, element, convention))
{
ConventionManager.ConfigureSelectedItem(element, Panorama.SelectedItemProperty, viewModelType, path);
ConventionManager.ApplyHeaderTemplate(element, Panorama.HeaderTemplateProperty, viewModelType);
return true;
}
return false;
};
// Silverlight Toolkit
ConventionManager.AddElementConvention<AutoCompleteBox>(AutoCompleteBox.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
ConventionManager.ConfigureSelectedItem(element, AutoCompleteBox.SelectedItemProperty, viewModelType, path);
return true;
};
ConventionManager.AddElementConvention<DateTimePickerBase>(DateTimePickerBase.ValueProperty, "Value", "ValueChanged");
ConventionManager.AddElementConvention<PerformanceProgressBar>(PerformanceProgressBar.IsIndeterminateProperty, "IsIndeterminate", "Loaded");
ConventionManager.AddElementConvention<ToggleSwitch>(ToggleSwitch.IsCheckedProperty, "IsChecked", "Checked");
ConventionManager.AddElementConvention<MenuItem>(ItemsControl.ItemsSourceProperty, "DataContext", "Click");
// Maps Conventions
ConventionManager.AddElementConvention<MapItemsControl>(ItemsControl.ItemsSourceProperty, "DataContext", "Loaded");
ConventionManager.AddElementConvention<Pushpin>(ContentControl.ContentProperty, "DataContext", "MouseLeftButtonDown");
A common usage pattern for a ListBox is to use it as a navigation list, when you select the item the application navigates to a new page. This usually involves either wiring into the ListBox SelectionChanged event, or using some sort of GestureTrigger.
Why not build the navigation functionality directly into the ListBox itself. Each item bound the NavigationList will expose a property with the uri. Much like the charting controls we'll then supply a Binding to the list that will be used for each item when it's selected.
public class ApplicationViewModel : PropertyChangedBase
{
private int id;
private string name;
public ApplicationViewModel(int id, string name)
{
this.id = id;
this.name = name;
}
public int Id
{
get { return id; }
set
{
id = value;
NotifyOfPropertyChange("Id");
NotifyOfPropertyChange("Uri");
}
}
public Uri Uri
{
get
{
return new Uri("/Views/NavigationListBox/DetailsView.xaml?Id=" + Id, UriKind.Relative);
}
}
public string Name
{
get { return name; }
set
{
name = value;
NotifyOfPropertyChange("Name");
}
}
}
<toolkit:NavigationListBox x:Name="Apps" NavigateUriBinding="{Binding Uri}">
<toolkit:NavigationListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Style="{StaticResource PhoneTextLargeStyle}"/>
</DataTemplate>
</toolkit:NavigationListBox.ItemTemplate>
<toolkit:NavigationListBox.EmptyContent>
<TextBlock Text="No Apps" Style="{StaticResource PhoneTextLargeStyle}"/>
</toolkit:NavigationListBox.EmptyContent>
</toolkit:NavigationListBox>
Our NavigationList will obviously inherit from the ListBox we created in the last post. We'll then expose a property NavigateUriBinding, this property is actually of type Binding. It's also important that it's not a DependencyProperty, just a regular property, otherwise when we set the binding in xaml we'll create a binding to the property rather than setting the property to the binding (a slightly confusing concept I know). The best way to explain it is that while we're setting the binding once on the list we'll we be using it on each item in the list. This means that we need to have actual Binding object rather than a DependencyProperty bound to a value.
We'll create an attached dependency property at the same time, this is where we'll be setting the NavigateUri for each ListItem. We'll then override the PrepareContainerForItemOverride, this is where the ListBox creates the ListItem for each item bound, we'll then set the binding of our attached property to the binding set in NavigateUriBinding.
public class NavigationListBox : ListBox
{
public static readonly DependencyProperty NavigateUriProperty =
DependencyProperty.RegisterAttached("NavigateUri", typeof(Uri), typeof(NavigationListBox), null);
public NavigationListBox()
{
SelectionChanged += OnSelectionChanged;
}
public Binding NavigateUriBinding
{
get; set;
}
public static Uri GetNavigateUri(DependencyObject obj)
{
return (Uri)obj.GetValue(NavigateUriProperty);
}
public static void SetNavigateUri(DependencyObject obj, Uri value)
{
obj.SetValue(NavigateUriProperty, value);
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var frameworkElement = element as FrameworkElement;
if(frameworkElement == null || NavigateUriBinding == null)
return;
frameworkElement.SetBinding(NavigateUriProperty, NavigateUriBinding);
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(SelectedItem == null || ItemContainerGenerator == null)
return;
var container = ItemContainerGenerator.ContainerFromItem(SelectedItem);
var uri = GetNavigateUri(container);
SelectedItem = null;
var frame = Application.Current.RootVisual as PhoneApplicationFrame;
if(frame == null || uri == null)
return;
frame.Navigate(uri);
}
}
There isn't an override for the SelectionChanged event so we'll create a handler in the constructor of the control. If an item has been selected we'll retrieve the value of our attached property and it's not null we'll navigate to that control. Easy!
In a bid to learn Mercurial at the same time I've uploaded the source for the controls so far (with a few other little bits and pieces) to Bitbucket, you find it all at Compiled Experience Toolkit. Sorry for the delay.
Download the source.