Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Convert a Silverlight Navigation Application to use MVVM using Cinch V2

0.00/5 (No votes)
8 Sep 2010 1  
How to convert a Silverlight Navigation Application to use MVVM using Cinch V2.

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:

    New folders

    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.

    New References

    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:

App.xaml.cs

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 Hyperlinks, 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" });
  //add any more pages here, note: seperator is only hidden on the first link

  // Setup the commands
  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();
    // note: we could use the Cinch IChildWindowService for the error window
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here