Friday, January 7, 2011

Pass an Array of Integers to a Controller Action in ASP.NET MVC

Yesterday I blogged about a little widget that I had built to allow users to tag content.  In that example, the users build up lists of tags using an Ajax interface.  Behind the scenes, we need to keep track of whatever tags the user has selected and send them off to the server for saving when the user submits their page.

When I was developing this widget, I stored the values in a hidden field similar to this:

<input type=”hidden” name=”selectedTagIds” value=”1,2,3,4,5” />

And I created a controller action like so to receive the Id’s:

[HttpPost]
public ActionResult Index(IEnumerable<int> integers)
{
    // do something with integers here
    return View();
}

When I ran that example, the value of the parameter named integers was empty – even though I was populating my hidden field with values.  So I switched the Type of the parameter from IEnumerable<int> to string and then I saw that the values from my hidden field were passed successfully.

The reason that ASP.NET MVC doesn’t do the conversion for us has to do with the the behavior of the default model binders.  Model binders are the piece of magic that parse Request values and do their best to form them into strongly typed objects for us.

At that point I could easily split the string into an array and parse the array of strings into integers and read them into some model.  You can see an example of what the code for that would look like in the following blog article:

How to post values to the server that are not in an input element

The issue here is that, for every time that I want to do this, I need to repeat that code over and over again.  Thankfully, ASP.NET MVC comes with some extensibility points that allow us to solve this problem in an elegant yet simple manner.

The two methods that we have to choose from here are:

  1. Create a custom ModelBinder and do the custom parsing into a strongly-typed object model and then pass that model as the parameter of our controller action.
  2. Create a custom ActionFilter and do the custom parsing directly into the parameter of our controller action.

For the purposes of completeness, I’ll show both ways in this article.

Creating a Custom ModelBinder

Imagine that the data that we are passing will be part of a custom view model type and we want to map the array of integers into one of the properties of that object.  Picture that the custom view model looks similar to this (but with other properties):

public class IndexViewModel
{
    public List<int> Integers { get; set; }
}

And that we are passing our data to an Action method which looks like so:

[HttpPost]
public ActionResult Index(IndexViewModel model)
{
    // do something with model.Integers;
    return View(model);
}

To accomplish this feat, we will create a class which implements the IModelBinder interface and it’s within the BindModel method of that interface that we’ll place our logic.  The implementation of the custom model binder is relatively straightforward and looks like so:

public class IndexViewModelBinder : IModelBinder
{

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = new IndexViewModel();
        var integers = controllerContext.HttpContext.Request.Form["integers"]
            .Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

        foreach (var integer in integers)
        {
            int tmp;
            if (int.TryParse(integer, out tmp))
            {
                model.Integers.Add(tmp);
            }
        }
        return model;
    }
}

As you can see, we’ve basically taken the code that would previously have been inside our Action method and moved it out into this class.

To get this working there’s one final piece of implementation and that is that we need to register our custom ModelBinder with ASP.NET MVC so that, whenever it encounters an IndexViewModel parameter, it knows to hand off the responsibility for doing the binding to our implementation.  This is also straightforward and requires a single line of code to be placed in App_Start in our Global.cs file:

ModelBinders.Binders[typeof(IndexViewModel)] = new IndexViewModelBinder();

That’s it!  Now when the sample is run, our custom ModelBinder intercepts the request, creates an instance of the IndexViewModel, which is then handed to the ActionMethod, simple!

Creating a Custom ActionFilter

The next way to perform this feat is through a custom ActionFilter.  The concept is identical in that we are writing some code that will intercept the Request and convert the Request Form values into some typed object of our choosing. 

The benefit of this approach – over the custom ModelBinder – is that, if you can get away with it, you can create a more generic solution.  For example, we can create a custom ActionFilter which converts from string to IEnumerable<int> and we can then use that filter whenever we are passing a collection of integers to an Action method.  If we are always wanting to pass custom ViewModel ‘objects’, then custom ModelBinder’s are the way to go.

Creating the custom ActionFilter is almost identical to the ModelBinder approach however this time we are inheriting from the base ActionFilterAttribute class.  Other than that, the code is almost exactly the same.  Here is the implementation of a custom ActionFilter to convert Request parameters into IEnumerable<int>:

public class IntegerArrayFilterAttribute : ActionFilterAttribute
{
    public string ParameterName { get; set; }
       
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var list = new List<int>();
        var integers = filterContext.HttpContext.Request.Form["integers"]
            .Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

        foreach (var integer in integers)
        {
            int tmp;
            if (int.TryParse(integer, out tmp))
            {
                list.Add(tmp);
            }
        }
        filterContext.ActionParameters[ParameterName] = list;
    }
}

Other than using a different base class, the main difference is that we are accessing the HttpRequest through a different object and that we must directly assign the ActionParameter value at the end.

Finally, to consume our custom FilterAttribute we simply apply an attribute to our Action method like so:

[HttpPost, IntegerArrayFilter(ParameterName="integers")]
public ActionResult Index2(IEnumerable<int> integers)
{
    var count = integers.Count();
    return View();
}

Again, very simple stuff indeed.  As mentioned previously, the benefit of this approach is that we can now reuse our custom IntegerArrayFilter wherever we have this requirement and hence the possibility of code reuse is much greater.

Enjoy!

3 comments:

  1. Hi,
    thank you for this post, however, what should i do while having 2D array of non constant size ? in that case I dont know, how to split the array. Is there some other way except setting up other properties that will hold the sizes of the dimensions ?
    Thank you :)

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This was very helpful. I was able to make a slight modification in order to have this work for GET Requests:
    var integers = filterContext.HttpContext.Request.QueryString[ParameterName].Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

    ReplyDelete