Asbestos Supply

2010-01-08 ASP.NET MVC & State Pattern: Choose your view

Problem

I have a model that uses the State pattern.  Some States allow read/write access to the Model's properties and some are read-only.  I wanted a good way to separate the logic for this from my Views; the View shouldn't have to decide whether properties are displayed as TextBoxes or inside Divs.

First Thought

At first I was going to create a custom ViewEngine.  That ViewEngine would be responsible for choosing the right View – that is, if the View was called “Edit” but the model was in a readonly state, it would change the View to “ReadOnly”.  However, that just didn't sound right – why was that responsibility falling on the ViewEngine? Plus, it's kind of inflexible – what if some editable Views are called “Edit” but some are called “Details”.

Solution

I ended up with a mixture of a custom ActionFilter and a base Controller class.  Before I show you the code, let me show you the usage:

[AcceptVerbs(HttpVerbs.Get)]
[StateItemEditView("ReadOnly")]
public ActionResult Edit(int id)
{
    var module = _Repository.GetByID(id);
    return View(module);
}

Notice Line 2 – this specifies that:

  • The current action has editable content
  • The ReadOnly View name is called "ReadOnly"

The code for the controller, which I use as a base class for my controllers that act upon StateItems:

public class StateItemController : Controller
{
    protected override ViewResult View(string viewName, string masterName, object model)
    {
        object readOnlyViewName;
        if (ControllerContext.Controller.TempData
            .TryGetValue(StateItemEditViewAttribute.key_ReadOnlyView, out readOnlyViewName))
        {
            var stateItem = model as IStateItem;
            if (workflowItem != null && stateItem.CurrentState.IsReadOnly)
                viewName = readOnlyViewName as string;
        }
        return base.View(viewName, masterName, model);
    }
}

Before returning the View, which has a ViewName string, we want to make sure the ViewName is correct.  We check the Controller's TempData for a predefined key.  If that key exists, the value will be the name of the View to display for ReadOnly states.  We then check if the Model exists and is a StateItem, and if it's ReadOnly we change the ViewName before returning the View.

The StateItemEditViewAttribute is just responsible for setting the TempData key:

public class StateItemEditViewAttribute : ActionFilterAttribute
{
    public StateItemEditViewAttribute(string readOnlyView)
    {
        _ReadOnlyView = readOnlyView;
    }
    private string _ReadOnlyView;
    public const string key_ReadOnlyView = "ReadOnlyView";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Controller.TempData.Add(key_ReadOnlyView, _ReadOnlyView);
    }
}

I'm using this for a binary decision – Edit/ReadOnly – but this could be used for a lot of other purposes too.