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