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!

Friday, April 20, 2012

ASP.NET WebForm System.InvalidOperationException on postback: Operation is not valid due to the current state of the object.

If on a post-back in a WebForm of an ASP.NET application you get this error:
System.InvalidOperationException: Operation is not valid due to the current state of the object.
And the stack trace is:
in System.Web.HttpRequest.FillInFormCollection()
in System.Web.HttpRequest.get_Form()
in System.Web.HttpRequest.get_HasForm()
in System.Web.UI.Page.GetCollectionBasedOnMethod(Boolean dontReturnNull)
in System.Web.UI.Page.DeterminePostBackMode()
in System.Web.UI.Page.ProcessRequestMain(
                           Boolean includeStagesBeforeAsyncPoint,
                           Boolean includeStagesAfterAsyncPoint)

You are trying to send a lot of values (TextBox, CheckBox, etc..) to the server.

The solution are two:
  1. refactor your page (not always easily possible);
  2. change the security settings;
Why security settings? Because the exception is caused by an internal security check done by the framework on the number of the posted values.
So you have to increase that limit on your web.config file, adding this key:

<appSettings>
    <add key="aspnet:MaxHttpCollectionKeys" value="2000" />
</appSettings>

Of course change this values at your needs.

 If your Exception occur when your send JSON with JavaScript, you have to add this other key to the web.config file:

<appSettings>
    <add key="aspnet:MaxHttpCollectionKeys" value="2000" />
</appSettings>

Again, change this value at your needs.

Happy exception handling! ;-)

Thursday, April 12, 2012

Asp.NET data binding: Object does not match target type.

Every day is a new adventure .. Today I face this "funny" behaviour of the ASP.NET "data binding".

I'm working on ASP.NET webform application, and a piece of this application must automatically generate some documents, nearly all PDF.
The documents are all heterogeneous, and in an object oriented way, I decided to implement a interface on each class able to generate a document.

The interface IDocumentoSemplice is pretty trivial and have only few properties.



What I like to do, is gather all possible documents for a user, and show them in a GridView.
The documents are loaded in a service class and then bounded to the GridView for the "DataBind".
The columns in the page are nearly all BoundField.

   <asp:boundfield datafield="DataCompilazione" dataformatstring="{0:d}"  
                   headertext="Data Compilazione">
   </asp:boundfield>

All seems to be great, but when I start the page, this exception pop up: "Object does not match target type."

I check and recheck the code, but cannot find anything wrong.
Finally I googled a bit for this problem, and what I found is pretty funny: the "DataBind" (at least the GridView one) need all the data-bounded objects are to be of the same type.

The solution was pretty simple: convert all the bounded field in template one:

   <asp:TemplateField>
      <ItemTemplate>
         <%# Server.HtmlEncode(((IDocumentoSemplice)Container.DataItem)
                               .DataCompilazione.ToShortDateString())%>
      </ItemTemplate>
   </asp:TemplateField>



The problem seem to be caused by some internal classes used for the reflection, needed to get the properties value at runtime.


Happy "DataBind" ...