I'm new to Action Filters in general and have always wanted to know how it worked. This looks like a good opportunity to learn...
We're replacing the UI for a Castle Monorail app and I'd like to keep the functionality of or implement something similar to the [Resource] attribute it provides as a custom action filter that writes the strings to the viewbag so that we can write them to a JSON object in global-ish scope and use them in JavaScript.
Currently, we decorate controller classes like this (of course I'd be decorating the controller methods, instead):
[Resource("common", "namespace.Resources.Common")]
[Resource("events", "namespace.Resources.Events")]
[Resource("people", "namespace.Resources.People")]
public class someController : BaseController
My question is this: "How do I write an action filter that builds a list of resources from the individual uses and deposits them into a model or viewbag variable?" ... or possibly ... "How do I write an action filter that can be called multiple times without overwriting data from previous calls?"
I've read the tutorials and articles I could find, but any info I've found about multiple calls to an action filter on a single controller method is almost certainly about controlling order of execution rather than how it is actually implemented.
I welcome any code/pseudo code to demystify this for me.
Thanks in Advance
First, you have to divorce Microsoft's idea of "ActionFilterAttribute". This puts all of the logic in one place, but it is confusing because Attributes and Filters are two entirely different things.
You need to separate them in this case so the filter can detect multiple attributes and do what you want with the data (which is a bit unclear but I imagine you will put some kind of list into the ViewData).
First, we have the attribute definition. It needs to be marked specially with an AttributeUsage attribute so it can be used multiple times and so it can be used on both controllers and action methods (I am assuming you will want to aggregate all of the attributes on the current controller and action, but you don't have to do it that way).
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class ResourceAttribute : Attribute
{
public ResourceAttribute(string name, string value)
{
this.Name = name;
this.Value = value;
}
public string Name { get; private set; }
public string Value { get; private set; }
}
Then it is simply a matter of using Reflection to get instances of the attributes, the same way you would get instances of attributes anywhere. There is a handy ActionDescriptor
method that gives you direct access to the action and controller type.
The context also gives you direct access to the ViewData
.
public class ResourceFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var attributes = this.GetAllAttributes(filterContext.ActionDescriptor);
foreach (ResourceAttribute attribute in attributes)
{
var name = attribute.Name;
var value = attribute.Value;
// Do something with the meta-data from the attribute...
}
if (attributes.Any())
{
// Set the ViewData only when there are attributes
filterContext.Controller.ViewData["SomeKey"] = "SomeValue";
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
// Do nothing
}
public IEnumerable<ResourceAttribute> GetAllAttributes(ActionDescriptor actionDescriptor)
{
var result = new List<ResourceAttribute>();
// Check if the attribute exists on the action method
result.AddRange(
actionDescriptor
.GetCustomAttributes(typeof(ResourceAttribute), false) as ResourceAttribute[]
);
// Check if the attribute exists on the controller
result.AddRange(
actionDescriptor
.ControllerDescriptor
.GetCustomAttributes(typeof(ResourceAttribute), false) as ResourceAttribute[]
);
return result;
}
}
Now, you just need to put the pieces into place. First, register the action filter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
// Register the filter globally
filters.Add(new ResourceFilter());
filters.Add(new HandleErrorAttribute());
}
}
Then decorate your controllers and actions accordingly. The filter will run every time, so you need to make sure there is a condition in it that makes it only output the ViewData when there are attributes to process.