Magellan and View Reuse

I just added a page on Magellan Action Filters. One of the reasons I'm excited about this is that action filters and view filters will allow me to handle a common and complicated issue - re-activating an existing view.

Take a controller action like this:

public ActionResult Show(int customerId)
{
    var customer = Customers.Find(customerId);
    Model = new CustomerDetailsModel(customer);
    return Window("CustomerDetails");    
}

If you clicked a pair of customers in a list, you would end up with a couple of Windows open, one for each customer as expected. However, if you clicked the same customer twice, you would also end up with two Windows - this is contrary to what most people would expect. Instead, most people would expect the first Window to be re-activated. This typically isn't an issue on the web, but it's a common issue for smart clients.

Action filters could combine with view filters to re-use the view automatically. For example, suppose we added this filter:

[ReuseView(CacheByParameters = new[] { "customerId" })]
public ActionResult Show(int customerId)

When executed, this action filter would track whether the action has already been executed. If it had, it would return a result that allowed the previous Window to be re-activated instead of executing it again. This provides a very cross-cutting way of solving a common problem, by combining two simple pieces of architecture.

The ReuseView action and result filter could be implemented as follows:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ReuseViewAttribute : Attribute, IActionFilter, IResultFilter
{
    private static readonly Dictionary<string, WeakReference> _windowReferences = new Dictionary<string, WeakReference>();

    public ReuseViewAttribute()
    {
    }

    public string[] CacheByParameters { get; set; }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var cacheKey = CalculateCacheKey(context.ControllerContext);
        var window = GetFromCache(cacheKey);
        if (window != null)
        {
            window.Activate();
            context.OverrideResult = new CancelResult();
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {

    }

    public void OnResultExecuting(ResultExecutingContext context)
    {

    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        if (context.Result is ViewResult == false)
            return;

        var cacheKey = CalculateCacheKey(context.ControllerContext);
        AddToCache(cacheKey, context.Result);
    }

    private string CalculateCacheKey(ControllerContext controllerContext)
    {
        return string.Format("Controller={0};Action={1};Params=[{2}]",
            controllerContext.Request.Controller,
            controllerContext.Request.Action,
            string.Join(",",
                controllerContext.Request.ActionParameters
                .Where(x => CacheByParameters.Contains(x.Key))
                .Select(x => x.Key + "=" + x.Value.ToString())
                .ToArray()
                )
            );
    }

    private static Window GetFromCache(string cacheKey)
    {
        if (!_windowReferences.ContainsKey(cacheKey))
            return null;

        var window = _windowReferences[cacheKey].Target as Window;
        if (window == null || !window.IsVisible)
            return null;

        return window;
    }

    private static void AddToCache(string cacheKey, ActionResult result)
    {
        var viewResult = result as ViewResult;
        if (viewResult == null)
            return;

        var windowViewEngineResult = viewResult.ViewEngineResult as WindowViewEngineResult;
        if (windowViewEngineResult == null)
            return;

        var window = windowViewEngineResult.RenderedInstance;
        _windowReferences[cacheKey] = new WeakReference(window);
    }
}

Download the Sample

I am tempted to pull this into Magellan, but I think it's a scenario that needs more thought and would probably need to be customized per application, so a fully generic solution might be hard to achieve. What do you think? Perhaps it's time for a Magellan Contrib? :)

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.

Carl Scarlett
Carl Scarlett
23 Dec 2009

I think it's best to leave this out. For developers using Magellan it would be handy to have this written already, but what if we need to extend it?

It's probably more of a Magellan coding pattern, so a good sample project or extension library containing it would suffice.