Wednesday, April 25, 2012

ASP.NET MVC: use integer array as parameter or as property of a model

How many time, in your ASP.NET MVC application do you need to use an array of integer or other type as parameter of an action, or likely, as property in your model class?
A lot, I know..

By default is not possible, at least in ASP.MVC 2.

One example of possible use is a list of checkboxs with each one, as value, an identifier from some table on the database.
Iin this case, when you post your data, with a web debugger (like firebug) or a sniffer, you will see values like these:
... &myFieldName=101&myFieldName=322&myFieldName=112&myFieldName=316&myFieldName=109& ...

If you try to get the value declaring myFieldName as string , you will see this value:
"101,322,112,316,109"

So you can image what you need to do..

But we can solve it in a elegant clean and reusable way, changing the default behaviour: we have to write a model binder, like in this class below.


public class IntegerArrayModelBinder : IModelBinder
{
   /// <summary>
   /// Char used for splitting
   /// </summary>
   private const char CHAR_TO_SPLIT = ',';

   private static readonly ILog _log
                  = LogManager.GetLogger(typeof(IntegerArrayModelBinder));

   #region Implementation of IModelBinder

   /// <summary>
   /// Binds the model to a value by using the specified controller
   /// context and binding context.
   /// </summary>
   /// <returns>
   /// The bound value.
   /// </returns>
   /// <param name="controllerContext">The controller context.</param>
   /// <param name="bindingContext">The binding context.</param>
   public object BindModel(ControllerContext controllerContext,
                           ModelBindingContext bindingContext)
   {
      if (bindingContext == null)
      {
         throw new ArgumentNullException("bindingContext");
      }
      ValueProviderResult valueProviderResult
           = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

      if (_log.IsDebugEnabled)
      {
         _log.DebugFormat(
            "ModelName = {0}, valueProviderResult = {1}" +
            ", AttemptedValue = {2}, RawValue = {3}"
            , bindingContext.ModelName
            , valueProviderResult
            , valueProviderResult == null
                    ? null : valueProviderResult.AttemptedValue
            , valueProviderResult == null
                    ? null : valueProviderResult.RawValue
            );
      }

      if (valueProviderResult == null)
      {
         return new int[0];
      }
      string attemptedValue = valueProviderResult.AttemptedValue;
      if (string.IsNullOrEmpty(attemptedValue))
      {
         return new int[0];
      }

      var integers = attemptedValue.Split(new char[] { CHAR_TO_SPLIT }
                                  , StringSplitOptions.RemoveEmptyEntries);
      var list = new List<int>(integers.Length);

      foreach (string integer in integers)
      {
         int tmp;
         //this will use the NumberFormatInfo.CurrentInfo
         if (int.TryParse(integer, out tmp))
         {
            list.Add(tmp);
         }
         else
         {
            if (_log.IsWarnEnabled)
            {
               _log.WarnFormat("Unable to convert {0} to an integer"
                               , integer);
            }
         }
      }

      if (_log.IsDebugEnabled)
      {
         _log.DebugFormat("Result = {0}", list.Join("@"));
      }

      return list.ToArray();
   }

   #endregion
}

Warning: this class is using log4net logging library for debug logging: feel free to remove all the reference to it.
The char used as splitter is the ',' but you can change it.

Finally you have to register it in your Global.asax.cs .

   protected void Application_Start()
   {
      /* other code */

      ModelBinders.Binders.Add(typeof(int[]), new IntegerArrayModelBinder());
   }

Happy binding!