Introduction
Although Sacha Barber had been great at explaining in detail how Cinch works in his various articles (Search for Cinch articles here on CodeProject), there has been very little coverage of Cinch for line of business (LOB) applications using Silverlight. Most, if not all, LOB applications require some form of navigation between pages, and as the Silverlight team has created some project templates for navigation, I thought it would be useful to show one way of converting a navigation project to use the MVVM pattern. In particular, we are using Cinch V2, although it would be very easy to tweak this to work with other frameworks such as MVVM Light.
Background
As I said, most, if not all, LOB applications in Silverlight require some sort of navigation between pages (Views). So let’s see how easy it is to use Cinch V2 to convert a navigation project to use MVVM for its main page and navigation framework. Something I certainly struggled with at first and had to leave, and come back to once I was more familiar with Cinch and MVVM.
For documentation on Cinch, start with Sacha Barber's posts on how to use the framework.
I won't go into why you should be using the MVVM pattern, as there are countless articles both here on CodeProject and elsewhere on the web. So if you are not interested in MVVM, stop reading right here.
I looked at various MVVM frameworks, but decided on Cinch because Sacha documents all aspects of his framework so well, it uses MEF, has the ability to show design time data as well as at runtime, and uses a view first approach. Plus, it also has the added bonus of working with WPF as well.
So what’s so important about MEF? Well, for a start, go and read the following 10 Reasons to use the Managed Extensibility Framework, then go to Marlon Crech's site and read up about MEFedMVVM (which drives the MEF parts in Cinch). I think you will then see the great benefits this brings to building large-scale Silverlight applications.
Getting Started
First, go off to the CodePlex site and download the source for Cinch V2 and build the solution to get the two DLLs you will need: Cinch.SL.dll and MEFedMVVM.SL.dll.
Now in Visual Studio 2010, take the following steps:
- Create a new Silverlight Navigation Application using the navigation project template
- Add these four folders to the application:
- Controls
- Libs
- Models
- ViewModels
As shown below:
Add the two DLLs mentioned above to the libs folder in Windows Explorer, and then add references to them and the following DLLs using the Add References dialog:
- Cinch.SL
- MEFedMVVM.SL
- System.ComponentModel.Composition
- System.ComponentModel.Composition.Initialization
The references section of your Silverlight application in the Solution Explorer should now look like the image below.
Now open up App.xaml.cs and add these using
clauses, then change the Application_Startup
method as below, leaving the Application_UnhandledException
method in its original state.
using Cinch;
using System.ComponentModel.Composition;
using System.Reflection;
namespace CinchNavigation
{
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
this.RootVisual = new MainPage();
}
The start of App.xaml.cs should now look like:
So what is happening in the code above? Well, the CinchBootStrapper boots up the Cinch system, and we pass in a list of Assemblies for MEF to find the Exports and Imports to marry up. More on this later. In this case, we are passing in the current assembly (XAP) file. But we could extend this list to include other XAP files, if for instance we were dynamically downloading extra XAP files. See the Cinch documentation for how to do this.
Compile and run the application to check there are no errors. (You won't see anything different yet.)
So what do we need to do to convert the application to use MVVM?
- Create ViewModels for the Main, Home, and About pages, and link them to the relevant pages (Views in MVVM speak).
- In the MainPageViewModel, create a method we can hook up to the
Navigated
event on the Navigation Frame in the MainPage.
- In the MainPageViewModel, create a method we can hook up to the
NavigationFailed
event on the Navigation Frame in the MainPage.
- In the
Navigated
method, update the hyperlink buttons so they change state to either the 'ActiveLink' or 'InactiveLink' states.
The first three items are not a problem, as we can use the Cinch EventToCommandTrigger
behaviour to bind to our events in XAML. But the last item is the main stumbling block, as in good MVVM style (no tight coupling), we should not have any links to the View (Page), and as it stands, we would need to access the View to set the Hyperlinks
state property after navigation has taken place. So what is the answer?
The Solution
Well, we need a way of dynamically creating a list of hyperlinks, but with an extra property of CurrentState
. We then need to bind values to the Hyperlink
properties of Uri
, Content
, CurrentState
from our ViewModel. We will also need to create a separator between the Hyperlink
s, and show this for all links except the first in the list.
My way of achieving this goal was as follows:
- Extend the
Hyperlink
class by creating a new control called MvvmHyperlink
which extends the Hyperlink
control by adding a new dependency property of CurrentState
.
- Create a class (
NavItemInfo
) for the properties we will bind to our MvvmHyperlink
controls.
- Replace the list of hyperlinks in the XAML of the main page with an
ItemsControl
and data template to hold our new controls.
- In the constructor of the
MainPageViewModel
, create an observable collection of our new NavItemInfo
items and bind these to the ItemsControl
.
- Wire up the
Navigated
and NavigationFailed
events to Commands in the ViewModel.
- Finally, delete the event handlers in the
MainPage
code-behind file.
Let’s start with the new Hyperlink control. Add a new class file to the Controls folder called MvvmHyperlink
. Then add the following code to it:
public class MvvmHyperlink : HyperlinkButton
{
public static readonly DependencyProperty CurrentStateProperty =
DependencyProperty.Register("CurrentState", typeof(object),
typeof(MvvmHyperlink),
new PropertyMetadata(CurrentStatePropertyChanged));
public object CurrentState
{
get { return (object)GetValue(CurrentStateProperty); }
set { SetValue(CurrentStateProperty, value); }
}
private static void CurrentStatePropertyChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
MvvmHyperlink hyp = o as MvvmHyperlink;
if (hyp != null)
{
hyp.OnCurrentStatePropertyChanged((object)e.NewValue);
VisualStateManager.GoToState(hyp,(string)e.NewValue, true);
}
}
private void OnCurrentStatePropertyChanged(object newValue)
{
CurrentState = newValue;
}
}
This is not really the place to go into the details of dependency properties, and I will leave it up to the reader to research it themselves. But it will create our CurrentState
property that we can bind to.
Next, we need our new NavItemInfo
class. So under the Models folder, create a new class called NavItemInfo
. Then add the following code:
using Cinch;
namespace CinchNavigation.Models
{
public class NavItemInfo : ViewModelBase
{
public bool SeperatorVisible { get; set; }
public string PageUri { get; set; }
public string ButtonContent { get; set; }
public string CurrentState { get; set; }
}
}
Now we need to start work on our MainPage
XAML. First off, add some references at the top of the XAML:
xmlns:controls="clr-namespace:CinchNavigation.Controls"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.SL"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
meffed:ViewModelLocator.ViewModel="MainPageViewModel"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
So what is happening here? Well, first we have a reference to our Controls folder. Then the following two references are for Cinch and MEF. Then, the MEFfed line hooks up our Page (View) with our ViewModel. Finally, we reference the Interactivity DLL for our behaviours that will link events to commands.
Now we need to alter the navigation frame XAML to add our behaviours for the Navigated
and NaviagationFailed
event handlers, using the Cinch built-in behaviour for hooking up UI events to commands in our ViewModel.
<navigation:Frame x:Name="ContentFrame"
Style="{StaticResource ContentFrameStyle}"
Source="/Home">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Navigated">
<CinchV2:EventToCommandTrigger Command="{Binding Navigated}" />
</i:EventTrigger>
<i:EventTrigger EventName="NavigationFailed">
<CinchV2:EventToCommandTrigger Command="{Binding NavigationFailed}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<navigation:Frame.UriMapper>
<uriMapper:UriMapper>
<uriMapper:UriMapping Uri=""
MappedUri="/Views/Home.xaml" />
<uriMapper:UriMapping Uri="/{pageName}"
MappedUri="/Views/{pageName}.xaml" />
</uriMapper:UriMapper>
</navigation:Frame.UriMapper>
</navigation:Frame>
Now we need to alter the links area to have an ItemsControl
, with a template to display our Hyperlink control and bind it to our list of links, which will be built in our ViewModel.
<Border x:Name="LinksBorder"
Style="{StaticResource LinksBorderStyle}">
<StackPanel x:Name="LinksStackPanel"
Style="{StaticResource LinksStackPanelStyle}">
<ItemsControl x:Name="NavItems"
ItemsSource="{Binding Path=NavItemsInfo}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Name="LayoutRoot"
Orientation="Horizontal">
<Rectangle Name="separator"
Style="{StaticResource DividerStyle}"
Visibility="{Binding SeperatorVisible}" />
<controls:MvvmHyperlink x:Name="hlink"
Style="{StaticResource LinkStyle}"
NavigateUri="{Binding PageUri}"
TargetName="ContentFrame"
Content="{Binding ButtonContent}"
CurrentState="{Binding CurrentState}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
Now we are ready to code our MainPageViewModel
. First we need some using
clauses.
using System.ComponentModel.Composition;
using System.Windows.Navigation;
using Cinch;
using MEFedMVVM.ViewModelLocator;
using System.Collections.ObjectModel;
using CinchNavigation.Models;
Next we need to mark up our class with some attributes that Cinch using MEFedMVVM needs to marry our view (MainPage
) with our ViewModel (MainPageViewModel
).
[ExportViewModel("MainPageViewModel")]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MainPageViewModel : ViewModelBase
ExportViewModel
will allow MEF to load this into memory, and the PartCreation of Shared
will create a singleton. NonShared
will create a new instance on every call. But as we want only one instance for our main page, Shared
is ideal. Finally, we inherit our class from ViewModelBase
, a Cinch ViewModel base class. Then we need some public properties.
public SimpleCommand<Object, EventToCommandArgs> Navigated { get; private set; }
public SimpleCommand<Object, EventToCommandArgs> NavigationFailed { get; private set; }
public ObservableCollection<NavItemInfo> NavItemsInfo { get; private set; }
First, our commands that will be fired by our behaviours, and finally our collection of NavItemInfo
items. Now in our constructor, we will setup our links, and create our commands.
public MainPageViewModel()
{
NavItemsInfo = new ObservableCollection<NavItemInfo>();
NavItemsInfo.Add(new NavItemInfo { ButtonContent = "home",
PageUri = "/Home", SeperatorVisible = false,
CurrentState = "ActiveLink" });
NavItemsInfo.Add(new NavItemInfo { ButtonContent = "about",
PageUri = "/About", SeperatorVisible = true,
CurrentState = "InActiveLink" });
Navigated = new SimpleCommand<Object, EventToCommandArgs>(ExecuteNavigatedCommand);
NavigationFailed = new SimpleCommand<Object,
EventToCommandArgs>(ExecuteNavigationFailedCommand);
}
As you can see, we have setup two links here: Home and About, and set their initial states. Then we go on to create the new commands and hook these up to two private methods. The code for these two are:
private void ExecuteNavigatedCommand(EventToCommandArgs args)
{
NavigationEventArgs navArgs = (NavigationEventArgs)args.EventArgs;
foreach (var link in NavItemsInfo)
{
if (link.PageUri.ToString().Equals(navArgs.Uri.ToString()))
{
link.CurrentState = "ActiveLink";
}
else
{
link.CurrentState = "InActiveLink";
}
}
}
private void ExecuteNavigationFailedCommand(EventToCommandArgs args)
{
NavigationFailedEventArgs navArgs = (NavigationFailedEventArgs)args.EventArgs;
navArgs.Handled = true;
ChildWindow errorWin = new ErrorWindow(navArgs.Uri);
errorWin.Show();
}
Taking these one at a time. ExecuteNavigatedCommand
takes an EventToCommandArgs
parameter which contains the EventArgs
, which in this case is NavigationEventArgs
, one of the properties of which is the URI of the page that has been navigated to. With this, we can loop through the NavitemsInfo
list and update the state. Now, because we are using an ObservableCollection
which implements NotifyPropertyChanged
, our UI will be updated! ExecuteNavigationFailedCommand
gets a NavigationFailedEventArgs
object back, which again has a URI, this time of the page that failed. From this, we have replicated the existing code.
Now you can finally delete or comment out the event handlers in the code-behind file of MainPage
. Providing you have not made any typos, your program should now run.
Finally
The source project has also wired up ViewModels for both the Home and About pages. These follow the same pattern as we have used with the MainPage, so I will leave it as an exercise for the reader to implement this themselves, or grab the source! Plus, it also includes a very basic test project, just to show you how to get started unit testing with MVVM. Suggestions for improvements would be very welcome; or if you feel my approach is completely wrong and there is a much better approach, please let me know.