A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

Back to: Magellan Home

Magellan is all about navigation, an important part of which is showing views. In Magellan, a view can be a Window, Dialog or Page. With the Composite WPF extension, a view can also be any user control that can be added to a Region. The system is also extensible - see the page on Windows Forms support for an example on writing your own view engine.

View Results

As described previously, controller actions return an Action Results. These contain the logic for prosecuting the request. Derived from ActionResult are three view types:

Magellan ViewResult-derived classes

Each of the derived classes defer to the ViewResult base class to handle the execution. The internal execution code looks like this:

protected override void ExecuteInternal(ControllerContext controllerContext)
{
    ViewEngineResult = _viewEngines.FindView(controllerContext, ViewParameters, viewName);

    if (ViewEngineResult.Success)
    {
        ViewEngineResult.Render();
    }
    else
    {
        throw new ViewNotFoundException(...);
    }
}

_viewEngines is a collection of objects that implement the IViewEngine interface. The ViewParameters property provides additional information to the view engines, such as limitations on the type returned. Each of the derived types populate these - PageResult specifies that the view engines should only locate pages, WindowResult asks only for Windows, and so on.

View Engines

As we've seen above, View Results defer to View Engines to locate the view. As you can guess, View Engines are quite simple, though their job is a little complicated:

public interface IViewEngine
{
    ViewEngineResult FindView(
        ControllerContext controllerContext, 
        ParameterValueDictionary viewParameters, 
        string view);
}

When a View Engine figures out which view to show, it returns a custom object derived from ViewEngineResult, which encapsulates how to render the view. This way, the View Engine is only concerned with locating the view - the ViewEngineResult contains the actual logic for showing it.

Magellan comes with two View Engines that share a common base class:

The two Magellan view engines

Although there are three view results (PageResult, WindowResult and DialogResult), we only have two View Engines. This is because the logic of finding the view and the constraints are the same - the only difference is the call to Show() vs. ShowDialog(), which is rendering logic, not finding logic.

View Location

As shown in the diagram above, Magellan's View Engines derive from a ReflectionBasedViewEngine.

When your controller specifies something like this:

public ActionResult Home()
{
    return Page("Index");
}

The ReflectionBasedViewEngine will find all types in the assembly (you can use different assemblies by passing them in the view engine's constructor). It then passes them to the derived types to be filtered - for example, WindowViewEngine will restrict the list to only include types derived from the WPF Window class, and PageViewEngine will filter by the Page base class.

Once the ReflectionBasedViewEngine has a set of candidate types, it uses some rules to find the best view. The rules are based firstly on whether the names match, and secondly by namespace proximity, taking some conventions into account. The conventions are encapsulated in an IViewNamespaceProvider interface, which is also passed into the View Engine's constructor.

In the code above, we specified the "Index" view. Assuming our controllers are in a namespace called MyCompany.MyProduct.Controllers, and the controller is named HomeController, Magellan will look for these combinations of view names, via the DefaultViewNamespaceProvider, working through the list in order:

MyProject.Controllers.Views.Home.Index
MyProject.Controllers.Views.Home.IndexPage
MyProject.Controllers.Views.Home.IndexView
MyProject.Controllers.Views.Index
MyProject.Controllers.Views.IndexPage
MyProject.Controllers.Views.IndexView
MyProject.Controllers.Home.Index
MyProject.Controllers.Home.IndexPage
MyProject.Controllers.Home.IndexView
MyProject.Controllers.Index
MyProject.Controllers.IndexPage
MyProject.Controllers.IndexView
MyProject.Views.Home.Index
MyProject.Views.Home.IndexPage
MyProject.Views.Home.IndexView
MyProject.Views.Index
MyProject.Views.IndexPage
MyProject.Views.IndexView
MyProject.Home.Index
MyProject.Home.IndexPage
MyProject.Home.IndexView
MyProject.Index
MyProject.IndexPage
MyProject.IndexView
Index
IndexPage
IndexView

Hopefully the list above makes sense. Effectively, we're looking for the first view we can find as close to the controller as possible. Some conventions like a sub folder called "Views" are also taken into account.

If a view cannot be found, Magellan will provide a friendly exception. The screenshot below show what would happen if my controller mis-spelled the name of a view:

An exception thrown when Magellan cannot find a view. It also lists the set of searched locations.

To change the conventions, simply create a class implementing IViewNamespaceProvider, and pass it to the view engine:

ViewEngines.Engines.Add(new PageViewEngine(new DefaultViewActivator(), new CustomNamespaceProvider()));

See also:

Back to: Magellan Home

Back to: Magellan Home

Action Results are a concept from ASP.NET MVC and other MVC frameworks, and they are also supported by Magellan. They provide a powerful mechanism for decoupling the controller from UI specific concerns. Before discussing how they work, we should discuss why they exist.

Why Action Results?

Suppose you weren't using Magellan and you had an action like this:

public void Login(string username, string password) 
{
    var window = IsValidLogin(username, password)
        ? new WelcomeWindow();
        : new LoginWindow();
    window.Show();
}

This code is nice and small, but unfortunately it would be difficult to test. Your test would have to set up a WPF environment, call the controller method, and check to see which Window was opened.

To make the code more testable, you might inject a service that supports showing Windows:

public void Login(string username, string password) 
{
    if (IsValidLogin(username, password))
        _windowService.Show<WelcomeWindow>();
    else _windowService.Show<LoginWindow>();
}

This is a little more testable - your test can use a mocking library to provide a test version of the IWindowService, and you can test to see if it was called without necessarily creating any Windows at test time. Unfortunately, it forces you to do interaction based testing instead of state based testing. You can't just call the controller and verify the result; you have to set up a record/replay mock to see what the controller did. Mocks are great, but if you can avoid mocks, that's even better in my books.

To make controller actions more testable, Magellan uses ActionResults that allow controllers to declare their intentions, rather than actually doing anything. ActionResults are an example of the Command Pattern. At runtime Magellan takes care of executing them, but from a testing point of view, you can just interrogate them to see what the controller specified.

Here's a Magellan version of the action above:

public ActionResult Login(string username, string password) 
{
    return IsValidLogin(username, password)
        ? Window("Welcome")
        : Window("Login");
}

The tests would be as simple as:

[Test]
public void InvalidLoginShouldShowLoginWindow() 
{
    var controller = new MyController();
    var result = controller.Login("fred", "invalidPassword") as WindowViewResult;
    Assert.IsNotNull(result);
    Assert.AreEqual("Login", result.ViewName);
}

[Test]
public void ValidLoginShouldShowWelcomeWindow() 
{
    var controller = new MyController();
    var result = controller.Login("fred", "validPassword") as WindowViewResult;
    Assert.IsNotNull(result);
    Assert.AreEqual("Welcome", result.ViewName);
}

Could they be any simpler?

Types of Action Results

The ActionResult class is actually an abstract base class, with many derived types. The class is actually super simple:

public abstract class ActionResult
{
    public void Execute(ControllerContext controllerContext)
    {
        ExecuteInternal(controllerContext);
    }
    protected abstract void ExecuteInternal(ControllerContext controllerContext);
}

Derived from the ActionResult base class are:

  • ViewResult, which has some derived types of its own:
    • PageResult, for navigating to a WPF Page
    • WindowResult, for showing a WPF Window in non-modal state
    • DialogResult, for showing a WPF Window in a dialog state
  • BackResult, for sending a GoBack() request to the current navigation service
  • RedirectResult, for invoking a different action on a potentially different controller
  • StartProcessResult, which launches a process via Process.Start()
  • CancelResult, which does nothing

Controller Helpers

To use these derived types, you could simply write:

public ActionResult MyAction() 
{
    return new PageResult("MyPage");
}

However, the Magellan Controller base class defines a number of helpful methods for creating these results. They make the controller actions a little shorter and more declarative, for example:

public ActionResult MyAction() 
{
    return Page("MyPage");
}

Here are some examples of the methods in action. It should be easy to guess which type of ActionResult is in play:

return Page("MyPage");                  // Navigates to the page
return Page("MyPage", true);            // Navigates to the page, and removes all 'back' entries from the journal
return Window("MyWindow");              // Creates the Window and calls Window.Show()
return Dialog("MyWindow");              // Creates the Window and calls Window.ShowDialog();
return StartProcess("calc.exe");        // Launches calc.exe
return StartProcess("calc.exe", true);  // Launches calc.exe and waits for the user to close it
return Back();                          // Navigates back in the navigation service
return Back(true);                      // Navigates back and removes the page from the journal
return Redirect("MyAction");            // Executes MyAction and returns that instead
return Cancel();                        // Does nothing

The StartProcessResult is another good example to consider. If your controller actually called Process.Start(), how would you test it? With the result, you can just cast it to StartProcessResult and verify the properties to see what process the controller wanted to start.

The ViewResult derived classes share some logic in common for dealing with View Engines, which get their own section.

Rolling your Own

Because the ActionResult base class is so simple, rolling your own is easy. Let's take a MessageBox for example. Here's what our controller might look like:

public class MyController : CustomController
{
    public ActionResult SayHello()
    {
        return MessageBox("Hello world!");
    }
}

A test for such a controller can look like this:

[Test]
public void ShouldShowMessageBox()
{
    var controller = new MyController();
    var result = controller.SayHello() as MessageBoxResult;
    Assert.IsNotNull(result);
    Assert.AreEqual("Hello world!", result.Message);
}

We probably want to create a base class for our controllers with the useful helpers:

public class CustomController : Controller
{
    public MessageBoxResult MessageBox(string message)
    {
        return new MessageBoxResult("My Application", message, MessageBoxButtons.OK);
    }

    public MessageBoxResult MessageBox(string title, string message) 
    {
        return new MessageBoxResult(title, message, MessageBoxButtons.OK);
    }
}

And finally, the star of the show, the MessageBoxResult:

public class MessageBoxResult : ActionResult
{
    private readonly string _title;
    private readonly string _message;
    private readonly MessageBoxButtons _buttons;

    public MessageBoxResult(string title, string message, MessageBoxButtons buttons)
    {
        _title = title;
        _message = message;
        _buttons = buttons;
    }

    public string Title
    {
        get { return _title; }
    }

    public string Message
    {
        get { return _title; }
    }    

    public MessageBoxButtons Buttons
    {
        get { return _buttons; }
    }    

    protected override void ExecuteInternal(ControllerContext controllerContext)
    {
        MessageBox.Show(_title, _message, _buttons);
    }
}

I hope you find action results as interesting as I do.

PS: if there's enough interest, I'll consider extending the MessageBoxResult and rolling it into Magellan.dll.

See also:

Back to: Magellan Home

Back to: Magellan Home

Action Filters are typically attributes that you can apply to a Magellan controller or controller action in order to intercept the call and provide an alternative way of handling the request. They provide a poor man's Aspect Oriented Programming mechanism for controllers.

Action Filters

The sample below shows how an Action Filter might be used. The Log attribute can be applied to either methods or classes:

public class MyController : Controller
{
    [Log]
    public ActionResult Show(int customerId) 
    {
        ...
    }
}

The Log action filter attribute might be implemented as follows:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LogAttribute : Attribute, IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("Entering: {0}({1})",
            context.Request.Action,
            string.Join(", ", context.Request.ActionParameters.Select(x => x.Key + "=" + x.Value).ToArray())
            );
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("Exiting: {0}({1})",
            context.Request.Action,
            string.Join(", ", context.Request.ActionParameters.Select(x => x.Key + "=" + x.Value).ToArray())
            );
    }
}

As you can see above, action filters have two hooks - before the action is invoked, and after the action is invoked. Before the action, you can:

  • See information about the request
  • Rewrite the parameters in the request
  • Short-circuit or 'cancel' the action by setting the Result property

After the action, you can:

  • Get access to the result of the action
  • Change the result of the action
  • See the exception that was thrown
  • Replace or suppress the exception that was thrown

Result Filters

Magellan also supports Result Filters via the IResultFilter interface. These are invoked after the action has been executed, and before the result is handled. These give you the ability to handle exceptions caused when rendering the view (rather than just when processing the action).

The logging example above could be extended to support Result Filters:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LogAttribute : Attribute, IActionFilter, IResultFilter
{
    // Other code here

    public void OnResultExecuting(ResultExecutingContext context)
    {
        Console.WriteLine("Rendering: {0}({1})",
            context.Request.Action,
            string.Join(", ", context.Request.ActionParameters.Select(x => x.Key + "=" + x.Value).ToArray())
            );
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("Rendered: {0}({1})",
            context.Request.Action,
            string.Join(", ", context.Request.ActionParameters.Select(x => x.Key + "=" + x.Value).ToArray())
            );
    }
}

Examples

Here are some useful ways to use Action and View Filters:

  • Logging of all controller requests
  • Enforcing exception handling policies
  • Caching of request results (bypassing the action the second time around)
  • Performing permission checks and redirecting to another view if the user doesn't have permissions
  • Reusing views

Back to: Magellan Home

Back to: Magellan Home

As an MVC framework, controllers are the most prominent object in Magellan. At their simplest, controllers are implemented as classes, and actions are implemented as methods on the class. Actions on a controller must be public methods, and must return ActionResult objects. Here is an example:

public class CustomerController : Controller
{
    public ActionResult Index()
    {
        Model = Customers.GetAll();
        return Page();
    }

    public ActionResult Show(int customerId) 
    {
        Model = Customers.Get(customerId);
        return Page();
    }
}

This page describes how controllers are executed.

Controller Factories

Navigation begins at the Navigator, either by using a command, behavior or calling it explicitly. The Navigator processes the request by resolving a controller from the controller factory, executing the request, and releasing the controller back to the factory:

public void Navigate(NavigationRequest request)
{
    Guard.ArgumentNotNull(request, "request");

    var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
    var controllerResult = controllerFactory.CreateController(request, request.Controller);
    var controller = controllerResult.Controller;
    var context = new ControllerContext(controller, request, controllerResult.Release);
    controller.Execute(context);
}

The controller factory implements the IControllerFactory interface, and provides a method to resolve the controller:

public interface IControllerFactory
{
    ControllerFactoryResult CreateController(NavigationRequest request, string controllerName);
}

The controller factory is set via the ControllerBuilder.Current.SetControllerFactory method. See the topic on using an IOC container for an example on creating your own controller factory.

Controllers

The controllers returned by the controller factory must implement the IController interface, which is quite simple:

public interface IController
{
    void Execute(NavigationRequest request);
}

The NavigationRequest passed to the controller contains information about the controller name, action name, and any parameters for the request. It is up to the controller to decide how to handle the request given this information. If the class/method convention used by Magellan isn't enough, your controller can use different mechanisms to prosecute the request. Magellan's navigator, behaviors, and commands can still be used.

Controllers will usually derive from the Controller base class. When this controller executes a request, it will work with an action invoker to resolve the action, call any action filters, execute the action, and evaluate the result.

The Execute method on the Controller base class is quite simple:

public void Execute(NavigationRequest request)
{
    Request = request;
    ActionInvoker.ExecuteAction(ControllerContext, request.Action, ModelBinders);
}

To make writing controllers easy, a number of helper methods exist for creating the action results, such as View(), Cancel() and Redirect(). You can read more about them in the action results section.

Action Invoker

The action invoker is used by the controller to handle the details of executing the action. This allows you to continue to derive from the Controller class while executing actions in a very different way. By default, the controller's ActionInvoker property is set to the DefaultActionInvoker.

The default action invoker uses reflection to map the requests Action property to a method on the controller. It then performs the following steps:

  1. Executes all pre-action filters
  2. Executes the action
  3. Executes all post-action filters
  4. Executes the action result

Action filters provide a way for you to implement cross-cutting concerns on controllers, such as authorization, logging, state management, and other examples.

When the action has been executed, it will return an Action Result. This is an important point - controllers do not show views directly, they simply return an object which is responsible for resolving and showing the view. This allows you handle the navigation request in a very different way without altering controllers.

Model Binders

One of the tasks the action invoker takes care of is mapping parameters from the current NavigationRequest to arguments passed to the action method on the controller. Rather than putting this logic in the action invoker, it instead calls through to objects known as Model Binders.

The ModelBinders.Binders static property is a registry of active model binders, associated with a type. By default there is only one binder - a DefaultModelBinder which uses type descriptors and casting to perform the conversion. If you wish to customize how request parameters are mapped to action method parameters, you can do so through implementing the IModelBinder interface and registering it with the ModelBinders.Binders collection.

See also:

Back to: Magellan Home

Back to: Magellan Home

The MVVM Light Toolkit is an MVVM framework by WPF MVP Laurent Bugnion, the author of Silverlight 2 Unleashed. It works well alongside Magellan and makes it easy to put behaviors behind views. The integration model with Magellan is quite similar to using the Microsoft MVVM toolkit.

One difference is that the MVVM Light Toolkit typically uses resources to refer to the ViewModel, limiting the amount of code behind that is required. Since Magellan controllers typically create the model, this does need to change.

As with using the Microsoft MVVM toolkit, we use the controller to create the ViewModel:

public class HomeController : Controller
{
    public ActionResult Main()
    {
        Model = new MainViewModel();
        return Page();
    }
}

MVVM Light views typically use a locator for creating the view model - this is a problem because our controller above will be creating the VM, and Magellan is taking care of assigning it.

<Window x:Class="MvvmLight1.MvvmView1"
    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"
    DataContext="{Binding Main, Source={StaticResource Locator}}"
    >

To keep Blend support, change the XAML to use d:DataContext instead of DataContext.

<Window x:Class="MvvmLight1.MvvmView1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ViewModel="clr-namespace:MvvmLight1.ViewModel" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d"
    d:DataContext="{x:Type ViewModel:MainViewModel}"
    >

Back to: Magellan Home

Back to: Magellan Home

Magellan's MVC framework is designed to handle navigation between views. However, the views themselves, and how they are implemented, is outside of Magellan's concern. Views can be simple XAML pages, or they can be driven by an MVVM or MVP pattern, or any other way you might like.

Microsoft provide a Visual Studio project template known as the MVVM Toolkit, which makes it easy to get started using the MVVM pattern.

To use Magellan with the MVVM toolkit, you can set up a new MVVM project using the MVVM project template. Then configure it the same way as described in the Magellan quickstart.

By default with the MVVM toolkit, you typically create the ViewModel and View and assign them yourself, as shown in the project template:

Views.MainView view = new Views.MainView();
view.DataContext = new ViewModels.MainViewModel();
view.Show();

Instead, with Magellan, you can assign the controller's Model property to you ViewModel:

public class HomeController : Controller
{
    public ActionResult Main()
    {
        Model = new MainViewModel();
        return Page();
    }
}

The view engines will create the page or Window, and set the DataContext to the Model for you. It will then navigate to the page or show the Window.

Your ViewModels can make use of commands, event managers, data binding, and all the other common MVVM patterns. MVVM would typically handle:

  • Local interaction with the view
  • Validation
  • Control state, such as whether a button should be enabled based on data

While Magellan would be used for:

  • Navigating to another page or window

A good way to think about this is to think in terms of the web. On the web, JavaScript is typically the "view model" - it handles the logic for a particular page. The server processes requests for many pages and navigation between pages - that's the job of Magellan.

There is also a NavigateCommand that can be used in ViewModels, instead of the need to use a DelegateCommand/RelayCommand for common navigation events.

Back to: Magellan Home

Back to: Magellan Home

Magellan was designed to work with Composite WPF from day one. Composite WPF provides support for multiple modules, loosely coupled pub/sub eventing, and regions for sub-dividing zones in the UI. However, Composite WPF does not enforce any particular UI pattern - MVVM, MVP and MVC could all work.

Magellan and Composite WPF can work well together to create a composite navigation-oriented application using the MVC pattern. Here are some examples:

  • Composite WPF modules could contain views and controllers
  • Composite WPF events could be used for navigation - i.e., a Navigate event that could be raised by different modules and services
  • Instead of pages, Magellan view results could return UserControls that are added to regions

Region Support

For region support, Magellan.Composite.dll contains some extensions that can be used. A controller may look like this:

public class ShellController : CompositeController 
{
    public ActionResult Explorer()
    {
        return CompositeView("Explorer").InRegion("LeftRegion");
    }
} 

On application startup, the view can be navigated to via:

Navigator.Primary.Navigate("Shell", "Explorer");

Lastly, an additional Region View Engine needs to be registered. As discussed in the IOC topic, you can also use a custom view activator to control how views are instantiated, if you want to use IOC. In this case we'll use the Microsoft common ServiceLocator:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new PageViewEngine(new ServiceLocatorViewActivator()));
ViewEngines.Engines.Add(new WindowViewEngine(new ServiceLocatorViewActivator()));
ViewEngines.Engines.Add(new CompositeViewEngine(new ServiceLocatorViewActivator()));

When the InRegion extension method is used, and the view class derives from UIElement, the region view engine will use the service locator to resolve the default RegionManager, and then add the view to the region.

Controller Factory

There is also a new controller factory that can be used with the Common Service Locator. It will automatically back onto ServiceLocator.Current to resolve controllers, so you just have to register them in the container:

ControllerBuilder.Current.SetControllerFactory(new ServiceLocatorControllerFactory());

See also:

Back to: Magellan Home

Back to: Magellan Home

Magellan was designed to work without an Inversion of Control or Dependency Injection container, to keep it simple and accessible. However, as applications become more complicated, modern WPF applications can benefit immensely from IOC containers.

Magellan's extensibility points make using a container easy. Magellan resolves and instantiates two main types of objects - controllers and views. The out of the box implementation uses conventions, but they can be overridden.

To take control of resolving controllers, implement the IControllerFactory interface. Here is an example using Ninject:

public class NinjectControllerFactory : IControllerFactory
{
    private readonly IKernel _container;

    public NinjectControllerFactory(IKernel container)
    {
        _container = container;
    }

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

The ControllerFactoryResult has a second overload that also allows you to pass a callback that is invoked when the request has been processed - this can be used for disposing the controller if required.

Then assign it as the default controller factory:

var container = new StandardKernel();
container.Bind<IController>().To<HomeController>().Named("Home");
container.Bind<IController>().To<SettingsController>().Named("Settings");
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory(container));

When a navigation request is processed, the Navigator will consult the ControllerBuilder.Current for the controller factory. It will use this to ask the factory for a controller, then execute the controller. The ReleaseController method will be called as soon as the navigation request has completed.

Typically, each navigation will create a new controller - if you want to use the same controller, you could have your controller factory recycle the objects, or make them singletons.

The second object that Magellan creates is views. This is done through the IViewActivator implementation. The default implementation looks for a public, parameterless constructor.

Again, let's use Ninject to control view instantiation.

public class NinjectViewActivator : IViewActivator
{
    private readonly IKernel _container;

    public NinjectViewActivator(IKernel container)
    {
        _container = container;
    }

    public object Instantiate(Type viewType)
    {
        return _container.Get(viewType);        
    }
}

We then need to tell the view engines that it is available. Since the view engines are automatically registered, we need to manually re-register them:

var activator = new NinjectViewActivator(container);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new PageViewEngine(activator));
ViewEngines.Engines.Add(new WindowViewEngine(activator));

If you are using Magellan with Composite WPF, you will also need to register the region view engine:

ViewEngines.Engines.Add(new CompositeViewEngine(activator));

Back to: Magellan Home

Back to: Magellan Home

This guide will walk you through getting started with Magellan. You will need a copy of Visual Studio 2008 with Service Pack 1.

Project Setup

  1. Download the Magellan library. Unzip it to a known location.
  2. Create a new WPF Application project using Visual Studio 2008.
  3. Add references to Magellan.dll and System.Windows.Interactivity.dll from the ZIP file that you downloaded.
  4. Create the following folder structure:

A new VS project, ready for Magellan

Create a model

In the Views/Home folder, add a class named AddModel. This will represent the view model of an addition action:

public class AddModel
{
    public int A { get; set; }
    public int B { get; set; }
    public int Result { get; set; }
}

Create a controller

Create a new class in the Controllers folder named HomeController. It will contain two actions - Index, which will be the input page, and Add, which will show the results:

using Magellan.Framework;

namespace MagellanHelloWorld.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return Page();
        }

        public ActionResult Add(int a, int b)
        {
            Model = new AddModel
            {
                A = a,
                B = b,
                Result = a + b
            };
            return Page();
        }
    }
}

Now wire up the controller in App.xaml.cs:

using System.Windows;
using Magellan.Framework;
using MagellanHelloWorld.Controllers;

namespace MagellanHelloWorld
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            var controllerFactory = new ControllerFactory();
            controllerFactory.Register("Home", () => new HomeController());
            ControllerBuilder.Current.SetControllerFactory(controllerFactory);

            base.OnStartup(e);
        }
    }
}

Set up the shell

In Window1.xaml, add a Frame element to the content:

<Window 
    x:Class="MagellanHelloWorld.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    >
    <Grid>
        <Frame Name="mainFrame" />
    </Grid>
</Window>

In the Window1 constructor, navigate the mainFrame to the Index action on your controller.

using System.Windows;
using Magellan;

namespace MagellanHelloWorld
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            Navigator.For(mainFrame).Navigate("Home", "Index");
        }
    }
}

Create the views

In the Views/Home folder, create two new Page files: Index.xaml and Add.xaml.

Index.xaml will be used to capture the inputs, and send them to the Add action on the controller. Notice how the Parameters are defined as part of the Navigation behavior:

<Page 
    x:Class="MagellanHelloWorld.Views.Home.Index"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
    xmlns:magellan="http://xamlforge.com/magellan" 
    Title="Index"
    >
    <StackPanel>
        <TextBox Name="parameterA" />
        <TextBlock Text="+" />
        <TextBox Name="parameterB" />

        <Button Content="Calculate">
            <i:Interaction.Behaviors>
                <NavigateBehavior Controller="Home" Action="Add">
                    <NavigateBehavior.Parameters>
                        <Parameter ParameterName="a" Value="{Binding ElementName=parameterA, Path=Text}" />
                        <Parameter ParameterName="b" Value="{Binding ElementName=parameterB, Path=Text}" />
                    </NavigateBehavior.Parameters>
                </NavigateBehavior>
            </i:Interaction.Behaviors>
        </Button>
    </StackPanel>
</Page>

The Add.xaml will show the output of the calculation. Note how it assumes the use of DataContext in the bindings.

<Page 
    x:Class="MagellanHelloWorld.Views.Home.Add"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
    Title="Add"
    >
    <StackPanel>
        <WrapPanel>
            <TextBlock Text="{Binding Path=A}" />
            <TextBlock Text="+" />
            <TextBlock Text="{Binding Path=B}" />
            <TextBlock Text="=" />
            <TextBlock Text="{Binding Path=Result}" />
        </WrapPanel>

        <Button Content="Try Again">
            <i:Interaction.Behaviors>
                <NavigateBehavior Controller="Home" Action="Index" />
            </i:Interaction.Behaviors>
        </Button>
    </StackPanel>
</Page>

The Magellan view engine will take care of setting the Page's DataContext to the Model from the controller.

Done

That's all there is to it - just hit F5 and admire the results of your handywork! At this point you should be able to enter the values, and they will appear on the next page. Wasn't that easy?

Index.xaml Add.xaml

Magellan took care of:

  • Locating the controller
  • Invoking the actions
  • Mapping the strings from the text boxes to integer parameters on the action (using Model Binders)
  • Locating the view
  • Connecting the model with the view

Tests

Of course, it wouldn't be complete without a test. Here's what the unit test for our Add action might look like:

[Test]
public void AddTest()
{
    var controller = new HomeController();
    controller.Add(1, 4);
    var model = (AddModel)controller.Model;

    Assert.AreEqual(1, model.A);
    Assert.AreEqual(4, model.B);
    Assert.AreEqual(5, model.Result);
}

What next?

Back to: Magellan Home

Back to: Magellan Home

Magellan is a lightweight framework that makes it easy to build WPF navigation applications. It is inspired by the ASP.NET MVC framework. The main features are:

  • Model-View-Controller support
  • Action filters for cross-cutting concerns such as authorization and redirection
  • Blend behaviors to make navigation easy
  • Transitions between pages

Magellan was drawn from a number of samples I had put together early this year and some work done on a client project.

The source download includes an "iPhone" application for demonstrating the features.

The sample iPhone application

We start with a simple project structure:

A VS2008 project with a number of folders for controllers, models and views

A controller implementation typically looks like this:

public class PhoneController : Controller
{
    public ActionResult Group(Group group)
    {
        var contacts = _contactRepository.GetContacts(group);

        Model = new GroupViewModel(group.Name, contacts);
        return Page();
    }

Views are XAML Page objects, and can optionally have a model. Here's an example:

View models

The idea is that upon navigation, a controller is created, the action is executed, and the view and view model are created. The view then becomes the focus of the frame. Put simply, the view and viewmodel are stateful, and the controller is stateless.

Navigation between views (with nice transitions) can be done either programatically:

Navigator.For(Frame).NavigateWithTransition("Home", "Main", "ZoomOut");

Or through Blend behaviors:

Blend Navigate behavior

The framework supports the ASP.NET MVC concepts of Action Filters, Model Binders, View Engines and more - I'll cover them in a later post.

Back to: Magellan Home