Welcome to Manicprogrammer Sign in | Join | Help

Asp.Net MVC Model Binding

Introduction

I´m doing a JQuery based component for dynamic rows using Asp.Net MVC.

What that means is that the following HTML would get generated:

...
<tr>
  <td>
    <input type="text" name="model.Items[0].Name" />
  </td>
  <td>
    <input type="text" name="model.Items[0].Description" />
  </td>
</tr>
<tr>
  <td>
    <input type="text" name="model.Items[1].Name" />
  </td>
  <td>
    <input type="text" name="model.Items[1].Description" />
  </td>
</tr>
...

What that means is that I need that code to be translated to a model object with two items featuring name and description in my action. Something in the lines of:

public class Model
{
  public IList<SubModel> Items { get; set; }
}
 
public class SubModel
{
  public string Name {get;set;}
  public string Description {get;set;}
}

Binding to Models in ASP.Net MVC – Flawed?

My first guess was to use DefaultModelBinder like this:

public ActionResult Save([ModelBinder(typeof(DefaultModelBinder))]Model model)
{
  //do something with model.
}

You can imagine my surprise when I found out that it does not work. Coming from MonoRail I might’ve been spoiled, but I really expected it to work seamlessly. That’s when I learned that if you want advanced binding in ASP.Net MVC you have to do it yourself.

“What? Implement one binder for each model? Then get the values from the Request MYSELF??? You gotta be kidding me?” – exactly my thoughts dear reader. Thus I implemented a binder that does exactly what the above says. The only issue with it is that it only uses Request.Form values for the List Items values. That can be changed if need be. I didn’t, so I didn’t bother. The code for the advanced binder is as follows (as usual it is “as-is” code and please don’t give me annoying criticism on quality of code since this is a proof-of-concept):

    public class AdvancedModelBinder : IModelBinder
    {
        public ModelBinderResult BindModel(ModelBindingContext bindingContext)
        {
            var result = Activator.CreateInstance(bindingContext.ModelType);
            BindPropertiesToResult(bindingContext.ModelType.Name, result, bindingContext);
            return new ModelBinderResult(result);
        }
 
        private void BindPropertiesToResult(string path, object result, ModelBindingContext bindingContext)
        {
            if (DepthCountFor(path) > 10) return;
            var properties = result.GetType().GetProperties();
            foreach (var property in properties)
            {
                var propertyValue = ReflectionUtils.IsList(property) ? GetListValuesFor(path, property, bindingContext) : GetValueFor(path, property, bindingContext);
                if (propertyValue != null) property.SetValue(result, propertyValue, null);
            }
        }
 
        private object GetListValuesFor(string path, PropertyInfo property, ModelBindingContext context)
        {
            var newList = ReflectionUtils.CreateListFor(property.PropertyType);
            var propertyPath = path + "." + property.Name + "[";
            if (!PathExists(propertyPath, context)) return null;
 
            var maxIndex = -1;
 
            foreach (string key in context.HttpContext.Request.Form.Keys)
            {
                if (!key.ToLowerInvariant().StartsWith(propertyPath.ToLowerInvariant())) continue;
                int index;
                var numericProperty = key.ToLowerInvariant().Replace(propertyPath.ToLowerInvariant(), string.Empty);
                var indexOf = numericProperty.IndexOf("]");
                if (indexOf == -1) continue;
                numericProperty = numericProperty.Substring(0, indexOf);
                var isNumeric = Int32.TryParse(numericProperty.Replace("]", string.Empty), out index);
                if (isNumeric && index > maxIndex)
                {
                    maxIndex = index;
                }
            }
 
            AddItemsToList(path + "." + property.Name, ReflectionUtils.GetListType(property.PropertyType), maxIndex, newList, context);
 
            return newList;
        }
 
        private object GetValueFor(string path, PropertyInfo property, ModelBindingContext context)
        {
            var propertyPath = path + "." + property.Name;
            if (!PathExists(propertyPath, context)) return null;
 
            if (!property.PropertyType.IsClass || property.PropertyType.Equals(typeof(string)))
            {
                var value = context.ValueProvider.GetValue(propertyPath);
                return value!=null ? value.AttemptedValue : null;
            }
 
            var newObject = Activator.CreateInstance(property.PropertyType);
            BindPropertiesToResult(propertyPath, newObject, context);
            return newObject;
        }
 
        private void AddItemsToList(string path, Type itemType, int index, object newList, ModelBindingContext context)
        {
            for (var i = 0; i <= index; i++)
            {
                var obj = Activator.CreateInstance(itemType);
                BindPropertiesToResult(string.Format("{0}[{1}]", path, i), obj, context);
                ReflectionUtils.InvokeMethodOn(newList, "Add", obj);
            }
        }
 
        private bool PathExists(string path, ModelBindingContext context)
        {
            foreach (string key in context.HttpContext.Request.Form.Keys)
            {
                if (key.ToLowerInvariant().StartsWith(path.ToLowerInvariant()))
                    return true;
            }
            return false;
        }
 
        private int DepthCountFor(string path)
        {
            return path.Count(c => c == '.');
        }
    }
 
    public static class ReflectionUtils
    {
        public static bool IsList(PropertyInfo property)
        {
            var propertyType = property.PropertyType;
            return propertyType.IsArray ||
                   propertyType.FullName.StartsWith(typeof(IList<>).FullName) ||
                   propertyType.IsSubclassOf(typeof(List<>)) ||
                   propertyType.GetInterfaces().ToList().Contains(typeof(IList<>));
        }
 
        public static object CreateListFor(Type type)
        {
            var listType = typeof(List<>);
            var genericType = listType.MakeGenericType(GetListType(type));
            return Activator.CreateInstance(genericType);
        }
 
        public static Type GetListType(Type listType)
        {
            return listType.GetGenericArguments()[0];
        }
 
        public static object InvokeMethodOn(object obj, string methodName, params object[] parameters)
        {
            var methodInfo = obj.GetType().GetMethod(methodName);
            return methodInfo.Invoke(obj, parameters);
        }
 
        public static object GetPropertyValue(object obj, string propertyName)
        {
            var property = obj.GetType().GetProperty(propertyName);
            return property.GetValue(obj, null);
        }
    }

Conclusion

This new model binder can be used like this:

public ActionResult Save([ModelBinder(typeof(AdvancedModelBinder))]TestModel testModel)

This will enable the scenario that I need, I´m just sharing with whomever might need it.

Hope it helps! Cheers,

Published Tuesday, January 06, 2009 9:49 PM by heynemann
Filed under ,

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# re: Asp.Net MVC Model Binding

Tuesday, January 06, 2009 8:39 PM by Greg Gigon

Hey Bernardo

This is not bad at all. Wonder how would you write the custom binder for more complex types (eg. Foo has properties that are not simple types).

I had this problem and ended up with Custom Binder for my model, unfortunately.

Looks like MVC still have a long way to go :)

Good job on the Collection binder.

# re: Asp.Net MVC Model Binding

Wednesday, January 07, 2009 12:00 AM by heynemann

I didn´t follow what you mean...

My binder cares for properties that are not simple types. This code is there for it:

if (!property.PropertyType.IsClass || property.PropertyType.Equals(typeof(string)))

You can say that you can include other special types like dates and timespans and others... I think so, but as I said I didn´t need them yet.

Cheers,

Bernardo Heynemann

# re: Asp.Net MVC Model Binding

Wednesday, January 07, 2009 6:01 AM by claudio.figueiredo

Or else you could stop being a noob and checking the mvc contrib binder. lol

# Forms, Repeatable Items, MVC, Validation, JSON – Phew, I got it!

Thursday, January 08, 2009 6:35 PM by while(availableTime>0) {

Introduction Following my last post , I want to introduce what I was really working in. A multi-item

# Shared Tutorials &raquo; Blog Archive &raquo; while(availableTime&gt;0) { : Asp.Net MVC Model Bindingwhile(availableTime&gt;0) { : Asp.Net MVC Model Binding

# Asp.Net MVC Model Binding | Planeta Globo.com

Thursday, April 16, 2009 1:12 AM by Asp.Net MVC Model Binding | Planeta Globo.com

Enter the text you see in the image:

Leave a Comment

(required) 
required 
(required)