arrow-left arrow-right brightness-2 chevron-left chevron-right circle-half-full dots-horizontal facebook-box facebook loader magnify menu-down RSS star Twitter twitter GitHub white-balance-sunny window-close
Magellan with Windows Forms
3 min read

Magellan with Windows Forms

This is an old post and doesn't necessarily reflect my current thinking on a topic, and some links or images may not work. The text is preserved here for posterity.

Back to: Magellan Home

Magellan does not natively support Windows Forms, but adding support is as easy as writing your own View Engine.

As described in the documentation on View Engines, there are two classes we need to write. The first is an object that implements IViewEngine, which is responsible for finding the view. The second is an object inheriting from ViewEngineResult, which contains logic for rendering the form (i.e., calling Form.Show()).

There are also a couple of design considerations:

  • Magellan controllers like to create View Models, and it's the ViewEngineResult's job to make them available for binding on the view.
  • Windows Forms applications typically use naming conventions like frmCustomerDetails, CustomerDetailsForm or CustomerDetailsWindow. Our engine should support these.

Binding Models to Forms

To support the model, we're going to create an interface that allows the model to be given to the form. The form can then figure out how to display the model (either using a Windows Forms BindingContext, or manual code).

public interface IBindableForm
{
    void Bind(object model);
}

Our form implementation could look like this:

public partial class MainForm : Form, IBindableForm
{
    public MainForm()
    {
        InitializeComponent();
    }

    public void Bind(object model)
    {
        Text = model.ToString();
    }
}

The Controller

Here is what our Controller might look like:

public class HomeController : Controller
{
    public ActionResult Launch()
    {
        Model = "Hello world!";
        return Window("Main");
    }
}

Notice that there is no reference to Windows Forms here. The Window() return value merely indicates that we want to show a Window - it doesn't indicate what kind of Window should be shown. This should make it easier to convert to WPF in the future.

The View Engine

The view engine is also pretty short. Since we want to reuse the naming conventions and reflection code that the other view engines use, we can derive from ReflectionBasedViewEngine:

public class FormsViewEngine : ReflectionBasedViewEngine
{
    private readonly IViewActivator _viewActivator;

    public FormsViewEngine(IViewActivator viewActivator)
    {
        _viewActivator = viewActivator;
    }

    protected override IEnumerable<string> GetAlternativeNames(string viewName)
    {
        yield return viewName;
        yield return viewName + "Form";
        yield return viewName + "Window";
        yield return viewName + "Dialog";
        yield return "frm" + viewName;
    }

    protected override bool ShouldHandle(ControllerContext controllerContext, ParameterValueDictionary viewParameters, string viewName)
    {
        var viewType = viewParameters.GetOrDefault<string>("ViewType");
        return viewType == "Window" || viewType == "Dialog";
    }

    protected override IEnumerable<Type> FilterCandidateTypes(ControllerContext controllerContext, ParameterValueDictionary viewParameters, string viewName, IEnumerable<Type> candidates)
    {
        return candidates.Where(x => typeof (Form).IsAssignableFrom(x));
    }

    protected override ViewEngineResult CreateViewResult(ControllerContext controllerContext, ParameterValueDictionary viewParameters, Type type)
    {
        return new WindowsFormViewEngineResult(type, _viewActivator, viewParameters);
    }
}

Notice how we override GetAlternativeNames to support our conventions. We also override FilterCandidateTypes to restrict the view to objects derived from the Windows Forms Form class.

The View Engine Result

The View Engine above has taken charge of finding the view. When a view is found, it creates a WindowsFormViewEngineResult. The view engine result has the job of instantiating and rendering the view:

public class WindowsFormViewEngineResult : ViewEngineResult
{
    private readonly Type _formType;
    private readonly IViewActivator _viewActivator;
    private readonly ParameterValueDictionary _viewParameters;

    public WindowsFormViewEngineResult(Type formType, IViewActivator viewActivator, ParameterValueDictionary viewParameters) : base(true, new string[] {})
    {
        _formType = formType;
        _viewActivator = viewActivator;
        _viewParameters = viewParameters;
    }

    public override void Render()
    {
        var form = (Form)_viewActivator.Instantiate(_formType);
        var bindableForm = form as IBindableForm;
        if (bindableForm != null)
        {
            var model = _viewParameters.GetOrDefault<object>("Model");
            bindableForm.Bind(model);
        }

        var isDialog = _viewParameters.GetOrDefault<string>("ViewType") == "Dialog";
        if (isDialog) form.ShowDialog();
        else form.Show();
    }
}

We make use of two critical view parameters - the ViewType, which indicates whether to show it as a Window or a Dialog, and the Model, which is populated from the Model property that was set on the controller.

Wiring It Up

To support our view engine, we just have to add it to the list of registered view engines:

ViewEngines.Engines.Add(new FormsViewEngine(new DefaultViewActivator()));

The DefaultViewActivator implements the IViewActivator interface, which is used for actually constructing the form (it's passed to the view engine result). If you want, you can use an IOC container instead.

The final Windows Form

Summary

Hopefully this illustrates how easy it is to write a View Engine. The biggest benefit to the Magellan approach is that our controllers are consistent whether we are using WPF or Windows Forms. There's also the added benefit that if we upgrade a view to WPF, our controllers don't change. Likewise if we change our naming conventions for the forms, we don't need to change the controllers. Finally, our controllers and models remain testable, which is always important.

Back to: Magellan Home

Paul Stovell's Blog

Hello, I'm Paul Stovell

I'm a Brisbane-based software developer, and founder of Octopus Deploy, a DevOps automation software company. This is my personal blog where I write about my journey with Octopus and software development.

I write new blog posts about once a month. Subscribe and I'll send you an email when I publish something new.

Subscribe

Comments