Quantcast
Channel: asp.net - Steve Hobbs
Viewing all articles
Browse latest Browse all 18

Fun with action filters

$
0
0

I was fortunate enough to be able to attend the brilliant DevWeek developer’s conference in London this year, and even more lucky to attend a lecture by Dino Esposito (please check out his brilliant Architecting Applications for the Enterprise book) on Asp.Net Action Filters. The purpose of his session was to demonstrate the importance of Action Filters and how we should all be using them much more than we normally do. I will be the first to admin that action filters are not the first possible solution that comes to mind when trying to solve a particular architectural problem in my Asp.Net MVC application.

I must say though, I was inspired. Yes, I realise I had previously posted about one particular useful action filter, but I really haven’t done too much with them until now. Dino put forth some uses for them, which I believe are discussed in more detail in his MVC book:

  • Using an action filter to figure out which button was pressed (on a multi-button form) and compartmentalise the resulting code path
  • An action filter which automatically populates generic data on a view model (imagine a list of countries or some other static data required by your view)

We’ve all had some grief with the first scenario at some stage or another. It’s just not quite as easy to do as one would expect. The second allows you to abstract away some data population code and generally keep things a bit tidier than they otherwise would.

Today I created another action filter which takes a CSV list of data, parses it, and gives you a strongly-typed list of values. It looks like this:

[SplitString(Parameter="contentItemKeys", Delimiter=",")]
public virtual ActionResult GetItemInfo(IEnumerable<Guid> contentItemKeys)
{
}

So imagine that I have POSTed a comma-delimited list of GUIDs to this action. Normally, if there is one GUID then Asp.Net MVC should be able to resolve that properly and give you a list with one thing in it. However, if you have more than one GUID in that comma-delimited list, then you will have an empty list given to you. Why? Because the framework doesn’t know how to parse that list properly.

You could use a custom model binder to achieve the desired effect, but creating an action filter to do the same thing is much neater and much more flexible.

I’ve created an action filter called SplitString and it works like this:

  • The filter accepts a couple of arguments: the parameter you want to act on, and the delimiter to use.
  • It overrides the OnActionExecuting method and looks for the specified parameter, first in the routing data, then in request data.
  • It then finds the type that each item should be, using a little reflection.
  • It then parses the list, converts each item in the parsed string to the desired type, and spits out the full list.

First, the class definition:

[AttributeUsage(AttributeTargets.Method)]
public class SplitStringAttribute : ActionFilterAttribute {
  public string Parameter { get; set; }
  public string Delimiter { get; set; }

  public SplitStringAttribute() {
    Delimiter = ",";
  }
}

Inside OnActionExecuting lets find the value we need to work with:

string value = null;
HttpRequestBase request = filterContext.RequestContext.HttpContext.Request;

if (filterContext.RouteData.Values.ContainsKey(this.Parameter) && filterContext.RouteData.Values[this.Parameter] is string) {
 value = (string) filterContext.RouteData.Values[this.Parameter];
} else if (request[this.Parameter] is string) {
 value = request[this.Parameter] as string;
}

Next we need to find the type to convert to. Specifically, we need to find the type of the generic argument which forms the type of the parameter we’re interested in. So if our parameter is IEnumerable<T>, I want to know what type T is. I’ve wrapped this up in a method:

Type listArgType = GetParameterEnumerableType(filterContext);

private Type GetParameterEnumerableType(ActionExecutingContext filterContext) {
 var param = filterContext.ActionParameters[this.Parameter];

 Type paramType = param.GetType();
 Type interfaceType = paramType.GetInterface(typeof(IEnumerable < > ).FullName);
 Type listArgType = null;

 if (interfaceType != null) {
  var genericParams = interfaceType.GetGenericArguments();
  if (genericParams.Length == 1) {
   listArgType = genericParams[0];
  }
 }

 return listArgType;
}

Here we simply:

  • Find the type of the parameter using the filterContext
  • Check to see if the type is IEnumerable<>. We do this simply by getting the interface and checking if it is not null
  • Finally we get any generic arguments, check that there is exactly one, and then return that type. This is the type that we will convert each item in our CSV list to.

Next, we process our CSV string and create our container list:

string[] values = value.Split(Delimiter.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

Type listType = typeof(List < > ).MakeGenericType(listArgType);

dynamic list = Activator.CreateInstance(listType);

We just split the string according to the delimiter that we need to use, then create a new generic list with the type we procured earlier. I’ve used a dynamic type here to make it much easier and more efficient to work with.

Next, we run through each value in our CSV list and add it to this new generic list:

foreach(var item in values) {
 try {
  dynamic convertedValue = TypeDescriptor.GetConverter(listArgType).ConvertFromInvariantString(item);
  list.Add(convertedValue);
 } catch (Exception ex) {
  throw new ApplicationException(string.Format("Could not convert split string value to '{0}'", listArgType.FullName), ex);
 }
}

The real magic here is the type converter. We can simply pass it a type and an item to convert and it will just do it for you, in a nice generic way. This means you don’t have to manually support a known list of types – let the framework handle that for you!

Finally, and the real cherry on top, is that to make all this work, we simply substitute the original action parameter value with this new list that we’ve just created:

filterContext.ActionParameters[this.Parameter] = list;

The result is, your action parameter will now be populated correct with the parsed list of values, in a strongly typed fashion.

Here's the full code:


Viewing all articles
Browse latest Browse all 18

Trending Articles