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 and View Reuse
2 min read

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

Enjoying these posts? Subscribe for more

Subscribe now
Already have an account? Sign in
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

You've successfully subscribed to Paul Stovell's Blog.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.