Magellan and View Reuse

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.

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? :)