Click here to Skip to main content
Click here to Skip to main content

Magellan: An MVC-powered Navigation Framework for WPF

, 7 Sep 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
An introduction to Magellan, an Open Source navigation framework for WPF.

Introduction

When you create a new WPF project in Visual Studio, you're generally greeted with a blank canvas; a minimal MainWindow.xaml and App.xaml, and some empty code-behind files. As an application developer, you need to decide how to structure your projects, where to put UI code, where to put business code, and how to navigate from one View to the next. It's up to you to find a way to do that which is robust, maintainable, scalable, and testable. In short, WPF has no out-of-the-box pit of success for you to fall into.

Magellan's goal is to create a pit of success for WPF applications. The goal of this article is to describe what the pit looks like, how it works, and why you'd even want to fall in.

Magellan: falling into the pit of success

Prerequisites

To follow along with this tutorial, you'll need a copy of Visual Studio 2010, as the current build of Magellan is .NET 4.0 only. The core Magellan code should compile under .NET 3.5, so if you're interested in a 3.5 build, leave some feedback on the Magellan-friends discussion group.

What is Magellan?

Magellan is an Open Source navigation framework for WPF. It's heavily inspired by ASP.NET MVC, and it should look and feel familiar to anyone who has worked with ASP.NET MVC before. Magellan's primary features are:

  • A URI routing engine, that lets you map Views to URIs
  • A Model-View-Controller and Model-View-ViewModel framework
  • Forms and master page controls that make it easier to create consistent user experiences

Creating the Sample Application

This article is going to explore the sample application that comes as part of the Magellan project templates. Before we start, you'll need to install the template.

  1. Launch a new instance of Visual Studio 2010
  2. Click Tools->Extension Manager...
  3. Change to the Online Gallery page, and search for "magellan"
  4. Click the Download button to install the template

Once the template is installed, you can create the sample application.

  1. Click File->New->Project...
  2. Under Installed Templates, expand Visual C#->Magellan
  3. Create a new Magellan MVC Project

To keep your system clean, the Visual Studio 2010 extension just deploys the project template, but it doesn't deploy the Magellan runtime binary. You'll need to download that and add a reference to it manually.

  1. Go to the Magellan downloads page and download the latest binaries ZIP
  2. Extract the ZIP file to a known location - I like to put it in a "lib" folder inside my source control tree
  3. Right-click the project in Visual Studio Solution Explorer, and click Add reference...
  4. Change to the Browse tab
  5. Browse to the Magellan.dll file that you extracted and add the reference

At this point, you should be able to hit F5 to run the application. Spend a few minutes playing with it. I'll wait...

Exploring the Sample Application

Now that you've had a chance to run the application, let's explore it together. Note that this sample is just an example of how I'd structure a Magellan project, but it works pretty well for me.

Project structure

In ASP.NET MVC, you'll typically have top-level folders like Models, Views, and Controllers. You can do the same thing with Magellan, but I personally find it doesn't scale too well - once I get up to 10 Controllers, I'm constantly scrolling up and down in the Solution Explorer. I have explained this in a bit more detail on my blog.

The sample application is oriented around the concept of a "feature", with one folder per Controller. Inside the folder where the Controller lives, I'll create sub-folders for Views, Models, Service Proxies, and other types. I find this works better, because it's likely that if I'm writing code in the TaxController, I'm more likely to want to edit one of the Tax-related Views or View Models, than I am to suddenly want to edit the HomeController.

A screenshot of the Visual Studio solution explorer

You'll notice something about this structure - it makes it obvious that each Controller has multiple Views, and each View has a corresponding ViewModel.

The Controllers

This sample uses Magellan's MVC framework, and it's designed to look very similar to ASP.NET MVC. It has two Controllers - a HomeController, for the general screens (a Home page, and an About dialog), and a TaxController for guiding the user through the tax estimation process. Let's look at HomeController first:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return Page("Index", new IndexViewModel());
    }

    public ActionResult About()
    {
        return Dialog("About", new AboutViewModel());
    }
}

There are a few rules to writing Controllers:

Page and Dialog are methods from the Controller base class that create action results that know how to locate and render a page or dialog, respectively.

The string passed to each method is used by Magellan as the name of the View to locate. Technically, as with ASP.NET MVC, you can leave it out, and Magellan will default to the name of the action. Magellan will try to find a few different variations of the View - for example, instead of IndexView.xaml, you could call it Index.xaml or IndexPage.xaml. Those are just conventions that you can override.

The second (optional) parameter passed to each action result is the View Model. In WPF, this will be set as the DataContext of the View when it is rendered. Normally, these might have properties that you would set based on some information fetched by the controller - such as search results from a search Web Service.

The TaxController is a little more interesting. It presents two pages - one where the user enters their tax details, and the other where the tax estimate is presented.

public class TaxController : Controller
{
    private readonly ITaxEstimatorSelector _estimatorSelector;

    public TaxController(ITaxEstimatorSelector estimatorSelector)
    {
        _estimatorSelector = estimatorSelector;
    }

    public ActionResult EnterDetails()
    {
        return Page("EnterDetails", new EnterDetailsViewModel());
    }

    public ActionResult Submit(TaxPeriod period, decimal grossIncome)
    {
        var situation = new Situation(grossIncome);
        var estimator = _estimatorSelector.Select(period);
        var estimate = estimator.Estimate(situation);

        return Page("Submit", new SubmitViewModel(estimate));
    }
}

The EnterDetails action simply presents the page, but the Submit action is a little more complex. It takes two input parameters that were gathered on the EnterDetails page, and it defers to the ITaxEstimatorSelector to produce the estimate, before presenting the estimate back to the user.

The job of a Controller is to act as a coordinator of external services and the user interface. It doesn't know exactly how to render a tax estimate, nor does it know precisely how to calculate it. It just knows how to bridge the gap.

ITaxEstimatorSelector is an example of a service, and a controller will typically make use of different kinds of services:

  • Classes that talk to a SQL/NoSQL database
  • Classes that perform calculations or complex processing
  • Classes that interact with the file system
  • Classes that invoke remote WCF services

We'll look at how these services are provided to the controllers in the Wiring it up section, and again in the Dependency Injection section.

Views and ViewModels

Now that we've seen how the controllers work, you might be able to guess how the Views and View Models work. I'm going to cherry pick a couple of examples to look at.

The models that your controllers return don't have to derive from anything special, but there are some special behaviors you get if you do:

  • IViewAware: if your View Model implements this interface, you'll be notified when a View has been bound to your ViewModel. That's useful for implementing a Model-View-Presenter or VM-first pattern
  • INavigationAware: implementing this gives you access to the INavigator that you can use for navigating elsewhere. We'll discuss this in more detail later.

The easiest way to implement these interfaces is to simply derive from Magellan's ViewModel base class, as the EnterDetailsViewModel does:

public class EnterDetailsViewModel : ViewModel
{
    public EnterDetailsViewModel()
    {
        Submit = new RelayCommand(SubmitExecuted);
    }

    public ICommand Submit { get; private set; }

    [Display(Name = "Gross income")]
    public decimal GrossIncome { get; set; }

    public TaxPeriod Period { get; set; }

    private void SubmitExecuted()
    {
        Navigator.Navigate<TaxController>(x => x.Submit(Period, GrossIncome));
    }
}

This View Model is pretty typical of VMs:

  • It exposes properties (GrossIncome) to make state available to the UI.
  • To respond to user interface events (clicking the Submit button), it uses ICommands.

These two attributes are all that is needed to implement the Model-View-ViewModel pattern.

ICommands usually require writing a custom class for every UI command, but Magellan borrows a trick used in nearly every MVVM framework - it provides a RelayCommand which accepts a delegate (SubmitExecuted, in this example) and implements ICommand for you. As we'll see in a moment, the UI contains a <Button /> which is bound to the Submit property, and when clicked, the SubmitExecuted method will be invoked.

In SubmitExecuted, we submit the data that the user entered by executing a navigation request. I'm going to talk more about this in detail later, but for now, let's just say that this is how we navigate from EnterDetails to Submit. This navigation management support is something that isn't well supported by most MVVM frameworks, and is a good example of why Magellan isn't just another MVC/MVVM framework.

EnterDetailsView.xaml is our actual View. It's quite short:

<Page 
    x:Class="MyMagellanApp.Features.Tax.Views.EnterDetails.EnterDetailsView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    d:DesignHeight="391" d:DesignWidth="728"
    Title="Home"
    Style="{DynamicResource Page.Normal}"
    >
    <Layout>
        <Zone ZonePlaceHolderName="ContentZone">
            <StackPanel Margin="7">
                <TextBlock Margin="7" Text="Enter your tax details" 
                     Style="{DynamicResource Text.Heading}" />

                <Form>
                  <Field Margin="7" For="{Binding Path=GrossIncome}" />
                  <Field Margin="7" For="{Binding Path=Period}" 
                    Description="Select the financial year in which the income was earned." />
                  <Field Margin="7">
                    <Button Command="{Binding Path=Submit}" 
                       Content="Submit" HorizontalAlignment="Left" Width="100" />
                  </Field>
                </Form>
            </StackPanel>
        </Zone>
    </Layout>
</Page>

If you've done any WPF work before, you might recognize the Page and StackPanel elements, but the Layout, Zone, Form, and Field elements will be new. These are custom Magellan controls, and they're designed to help you create more consistent user experiences with less XAML.

Layouts are Magellan's equivalent of ASP.NET Master Pages. They're pretty simple, but useful for those recurring UI needs. You can read more about them in the Layouts section of the project documentation.

Forms and Fields are a little more advanced. They take care of creating the standard Label, TextBox, and other input controls needed for data entry fields, and positioning them in a standard way. Fields can use the For binding to reflect on the target property and "figure out" what to render. For example, if you bind to a string, it will render a TextBox. If you bind to an enum, it will generate a ComboBox. You'll remember that the EnterDetailsViewModel has a GrossIncome property with a [Display(Name = "Gross income")] attribute - that's what the Field uses for a Label. These are just conventions, and the project documentation describes how to override them.

All up, Views in Magellan look exactly like Views in any standard WPF project, apart from a couple of extra controls. There's no base class to derive from, and no attached properties or behaviors to use.

Wiring it up

Now that we have controllers and views, the last piece of the puzzle is to wire it all up.

Magellan is a navigation framework, so it needs some kind of navigator object that can accept navigation requests. These navigation requests need to be mapped to some kind of handler, which is the job of a routing engine. That handler abstracts some kind of presentation pattern - in this case, MVC - so we need to configure that too.

Magellan's setup code normally lives in App.xaml.cs. I like to override the OnStartup method:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var controllerFactory = new ControllerFactory();
    controllerFactory.Register("Home", () => new HomeController());
    controllerFactory.Register(
      "Tax", () => new TaxController(TaxSelectorFactory.CreateSelector()));

    var routes = new ControllerRouteCatalog(controllerFactory);
    routes.MapRoute("{controller}/{action}/{id}", 
                    new { controller = "Home", action = "Index", id = "" });

    var factory = new NavigatorFactory("tax", routes);
    var mainWindow = new MainWindow(factory);
    mainWindow.MainNavigator.Navigate<HomeController>(x => x.Index());
    mainWindow.Show();
}

Working from the bottom up, we have a NavigatorFactory which produces Navigators that know how to execute navigation requests. A NavigatorFactory needs routes to match navigation requests to, so it accepts any object derived from IRouteResolver, which ControllerRouteCatalog does. ControllerRouteCatalog needs an IControllerFactory to locate and instantiate controllers, so we configure that too.

The default controller factory uses lambdas to instantiate controllers. Personally, I tend to throw this out in favor of an Inversion of Control container, which we'll talk about in the Dependency Injection section, but it's useful for demonstrations. Each controller is registered with a name, which by convention is normally the name of the class minus the "Controller" suffix. Writing your own controller factory is a pretty common extension point.

A key concept in Magellan is that every view should be accessible by a nicely formatted URI. This mapping from URIs to executing controllers or alternative handlers is done through the routing engine.

Hold on; why do we even have URIs? Isn't WPF a desktop technology?

There have been many cases where URIs have been useful to me, even if they aren't shown to the user. The most important reason is that they provide a loosely coupled navigation mechanism - Page A can link to page B via a URI, without any direct code dependency. Of course, there's also the benefit that you can give the URI to the user. Instead of saying "click here, go there, click that", you can just mail them a URI, like hr://training/forms/courses/DEV123/request, and it's a single click for them.

The ControllerRouteCatalog allows routes to be mapped to MVC controllers, using the IControllerFactory that you pass to it. In this example, it uses a convention such that the first part of the URL is assumed to be the controller name, the second part is the action name, and the third part is the value of an ID parameter.

Using this route specification, the following URIs would map to the following controller action methods:

tax:// HomeController.Index()
tax://Home HomeController.Index()
tax://Customers CustomersController.Index()
tax://Customers/List CustomersController.List()
tax://Customers/Show/123 CustomersController.Show(id = 123)
tax://Customers/Show/123?revision=3 CustomersController.Show(id = 123, revision = 3)

Where did that tax:// prefix come from? That's the URI scheme parameter given to NavigatorFactory's constructor. If you navigate from within another view, Magellan will automatically generate a URI using your route specifications.

Extending it

Well, that's the sample application as-is. It's a good example of how I'd structure a simple application, but is Magellan limited to simple applications?

In this section, we'll look at some issues that are important for larger, more complicated WPF applications.

Dependency Injection

When your application grows, and if you're following good object oriented practices, you'll eventually find that constructing an object like an MVC controller involves instantiating or locating a deep graph of objects which work together to achieve your goals. To deal with this complexity, we use Dependency Injection or Inversion of Control containers, an important topic that I could never do credit to in this article.

While Magellan doesn't require an IOC container to work, it assumes you probably will want to use one, and it has plenty of extension points to make it possible.

As we discussed in the Wiring it up section, the ControllerRouteCatalog, which sends routes to MVC controllers for handling, requires an IControllerFactory. It's a simple interface and looks similar to the same version in ASP.NET MVC:

public interface IControllerFactory
{
    IController CreateController(ResolvedNavigationRequest request, string controllerName);
}

Using an IOC container like Autofac, we can implement this interface. The simplest way to do it might be:

public class AutofacControllerFactory : IControllerFactory
{
    private readonly IContainer _container;

    public AutofacControllerFactory(IContainer container)
    {
        _container = container;
    }

    public IController CreateController(ResolvedNavigationRequest request, 
                                        string controllerName)
    {
        var controller = _container.ResolveNamed<IController>(controllerName);
        return new ControllerFactoryResult(controller);
    }
}

In this case, we defer to the Autofac IContainer implementation to resolve our controllers. Our wire up code could then be:

var builder = new ContainerBuilder();
builder.RegisterType<HomeController>().Named<IController>("Home");
builder.RegisterType<TaxController>().Named<IController>("Tax");
// Register other types...

var container = builder.Build();

var routes = new ControllerRouteCatalog(new AutofacControllerFactory(container));

Now when Magellan receives a navigation request for the Tax controller, it will defer to Autofac to resolve it, using the name as the key in the container. Autofac will automatically look at the dependencies the TaxController has, and figure out how to resolve those, eventually building up a nice graph of dependencies for you.

This is a relatively simple use of an IOC container, but it probably covers 90% of where you'd use IOC in a Magellan application. The Magellan documentation lists other areas that can be extended to support IOC. I'll come back to disposal of components and scope management a little later.

Action filters

Let's go back to the EnterDetails action on the TaxController for a moment:

public ActionResult EnterDetails()
{
    return Page("EnterDetails", new EnterDetailsViewModel());
}

Suppose we get a new requirement: if the user isn't a Power User, they should be redirected to a different page. We might modify the action to read:

public ActionResult EnterDetails()
{
    if (!Security.HasPermission("Power User"))
    {
        return Redirect(new { controller = "Security", action = "NoPermission" });
    }
    return Page("EnterDetails", new EnterDetailsViewModel());
}

This kind of code is a good example of a cross-cutting concern. Nearly every action may have a different set of security restrictions, and copying and pasting the same code into each action gets tedious.

Magellan borrows ASP.NET MVC's approach to this problem by implementing the concept of action filters. An action filter is applied declaratively to your action, a bit like this:

[RequirePermission("Power User")]
public ActionResult EnterDetails()
{
    return Page("EnterDetails", new EnterDetailsViewModel());
}

Not only have we reduced the code in each action, but we've also moved to a more declarative model. The implementation of the RequirePermission attribute would be:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequirePermissionAttribute : Attribute, IActionFilter
{
    private string _requiredPermission;

    public RequirePermissionAttribute(string requiredPermission) 
    {
        _requiredPermission = requiredPermission;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!Security.HasPermission(_requiredPermission))
        {
            context.OverrideResult = new RedirectResult(
               new { controller = "Security", action = "NoPermission" });
        }
    }
}

As in ASP.NET MVC, action filters are a very powerful way of handling these cross cutting concerns. Here are some other things you might decide to do with an action filter:

  • Ensure any action parameter that implements IDataErrorInfo is valid; if not, cancel the navigation request.
  • Logging and tracing.
  • Wrapping the action in a database transaction, or Raven DB/NHibernate session.
  • Exception handling policies.
  • Caching and reusing of pages.

Background threading

The controller in MVC generally deals with navigation and coordinating external services. Sometimes, invoking those services can be slow, and we don't want to freeze the UI while it happens. In other WPF frameworks, switching between background and foreground threads becomes tricky, and makes testing difficult.

Magellan comes with the ability to automatically execute controller actions on a background thread. When the action is complete and the UI is ready to render a view, Magellan automatically dispatches it back to the UI thread. This means that the controller is completely unaware that it's being executed on a background thread.

For example, suppose our tax calculations took a very long time:

public ActionResult Submit(TaxPeriod period, decimal grossIncome)
{
    Thread.Sleep(10000);

    var situation = new Situation(grossIncome);
    var estimator = _estimatorSelector.Select(period);
    var estimate = estimator.Estimate(situation);

    return Page("Submit", new SubmitViewModel(estimate));
}

We can configure Magellan's support for asynchronous controllers in a few different ways. If you are using the out-of-the-box ControllerFactory, you can switch it to the also-out-of-the-box AsyncControllerFactory:

var controllerFactory = new AsyncControllerFactory();
controllerFactory.Register("Home", () => new HomeController());
controllerFactory.Register("Tax", 
   () => new TaxController(TaxSelectorFactory.CreateSelector()));

If you are using a custom implementation of IControllerFactory, you just need to set the ActionInvoker property of any controller you resolve (this is all AsyncControllerFactory does).

public IController CreateController(ResolvedNavigationRequest request, 
                                    string controllerName)
{
    var controller = _container.ResolveNamed<IController>(controllerName);
    if (controller is ControllerBase)
    {
        ((ControllerBase)controller).ActionInvoker = new AsyncActionInvoker();
    }
    return new ControllerFactoryResult(controller);
}

If you run the application now, you'll find that the 10-second sleep we put in the Submit action does indeed take 10 seconds, but the UI is still responsive. However, there's no feedback to the user that something is happening.

To notify the user that we are busy, we can implement INavigationProgressListener. In MainWindow.xaml.cs, we might change it to:

public partial class MainWindow : Window, INavigationProgressListener
{
    public MainWindow(INavigatorFactory navigation)
    {
        InitializeComponent();

        navigation.ProgressListeners.Add(this);
        MainNavigator = navigation.CreateNavigator(MainFrame);
    }

    public INavigator MainNavigator { get; set; }

    public void UpdateProgress(NavigationEvent navigationEvent)
    {
        Dispatcher.Invoke(new Action(
            delegate
            {
                if (navigationEvent is BeginRequestNavigationEvent)
                {
                    Cursor = Cursors.Wait;
                }

                if (navigationEvent is CompleteNavigationEvent)
                {
                    Cursor = Cursors.Arrow;
                }
            }));
    }
}

Now when you click Submit, the mouse cursor will switch to a busy cursor for the 10 seconds until the action completes. Instead of changing the cursor, you might display an animated icon, disable the screen, or find some other way of providing feedback, but the extension point should be the same.

Summary

Magellan borrows many ideas from the successful ASP.NET MVC framework, and combines them with a generous mix of MVVM and other UI patterns, to create a framework that helps you to fall into the pit of success. Magellan is oriented around navigation, and encourages the use of the Model-View-Controller pattern to manage external services, and the MVVM pattern to manage state for views. It's well documented and tested, and is used today in production applications.

This article took a broad look at a number of Magellan features, to give you an idea of whether it will be useful to you. The next steps would be to check out the project documentation, and explore some of the sample applications that come with the source code download. If you have any questions about Magellan, fire an email to the magellan-friends Google Group.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Paul Stovell
Octopus Deploy
Australia Australia
My name is Paul Stovell. I live in Brisbane and develop an automated release management product, Octopus Deploy. Prior to working on Octopus I worked for an investment bank in London, and for Readify. I also work on a few open source projects. I am a Microsoft MVP for Client Application Development.
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralA few questions PinmvpSacha Barber9-Sep-10 4:01 
GeneralRe: A few questions PinmemberPaul Stovell9-Sep-10 4:10 
GeneralRe: A few questions PinmvpSacha Barber9-Sep-10 4:15 
Cool thanks for that, very interesting
Sacha Barber
  • Microsoft Visual C# MVP 2008-2010
  • Codeproject MVP 2008-2010
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

GeneralMy vote of 5 PinmemberEric Xue (brokensnow)9-Sep-10 0:23 
GeneralMy vote of 5 PinmemberCésar de Souza8-Sep-10 9:43 
GeneralRe: My vote of 5 PinmvpSacha Barber9-Sep-10 3:08 
GeneralRe: My vote of 5 PinmemberCésar de Souza9-Sep-10 4:45 
GeneralRe: My vote of 5 PinmemberPaul Stovell9-Sep-10 5:24 
GeneralRe: My vote of 5 PinmvpSacha Barber9-Sep-10 6:28 
GeneralMy vote of 5 PinmemberMarcelo Ricardo de Oliveira8-Sep-10 5:06 
GeneralMy vote of 5 PinmemberSushant Joshi8-Sep-10 4:39 
GeneralThe disciples are indeed proud of you PinmvpSacha Barber8-Sep-10 1:05 
GeneralMy vote of 5 Pinmembersbussinger7-Sep-10 8:45 
GeneralNice job Paul PinmvpPete O'Hanlon7-Sep-10 7:58 
GeneralMy vote of 5 Pinmemberegozi137-Sep-10 4:57 
GeneralMy vote of 5 Pinmemberindyfromoz7-Sep-10 3:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 7 Sep 2010
Article Copyright 2010 by Paul Stovell
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid