Here is an implemenatation that handles collections/lists, arrays and dictionaries that have a .index, otherwise it falls through to the RC code. NOTE: this will not fall back to a blank prefix for method parameter binding. Use the BindAttribute to specify a blank. I did not want to modify the original context if not necessary.
/* Usage documentation at the bottom of this file */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;
namespace System.Web.Mvc
{
public class IndexModelBinder : DefaultModelBinder
{
#region handle Beta style collections with .index
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//handle collections with an .index specified
Type modelType = bindingContext.ModelType;
object model = bindingContext.Model;
Type enumerableType = GetInterface(modelType, typeof(IEnumerable<>));
Type dictionaryType = GetInterface(modelType, typeof(IDictionary<,>));
if ((model == null && modelType.IsArray) ||
dictionaryType != null ||
enumerableType != null)
{
ValueProviderResult vpr;
if (bindingContext.ValueProvider.TryGetValue(DefaultModelBinder.CreateSubPropertyName(bindingContext.ModelName, "index"), out vpr))
{
string[] indexes = (string[])vpr.RawValue;
if (model == null && modelType.IsArray)
{
Type elementType = modelType.GetElementType();
Type listType = typeof(IList<>).MakeGenericType(elementType);
object collection = CreateModel(controllerContext, bindingContext, listType);
ModelBindingContext arrayContext = new ModelBindingContext()
{
Model = collection,
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
ModelType = listType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
IList list = (IList)UpdateCollectionByIndex(controllerContext, arrayContext, elementType, indexes);
if (list == null) return null;
Array array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
if (model == null) model = CreateModel(controllerContext, bindingContext, modelType);
if (dictionaryType != null)
{
Type[] types = dictionaryType.GetGenericArguments();
ModelBindingContext dictionaryContext = new ModelBindingContext()
{
Model = model,
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
ModelType = modelType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return UpdateDictionaryByIndex(controllerContext, dictionaryContext, types[0], types[1], indexes);
}
if (enumerableType != null)
{
Type elementType = enumerableType.GetGenericArguments()[0];
if (typeof(ICollection<>).MakeGenericType(new Type[] { elementType }).IsInstanceOfType(model))
{
ModelBindingContext collectionContext = new ModelBindingContext()
{
Model = model,
ModelName = bindingContext.ModelName,
ModelState = bindingContext.ModelState,
ModelType = modelType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
return UpdateCollectionByIndex(controllerContext, collectionContext, elementType, indexes);
}
}
}
}
return base.BindModel(controllerContext, bindingContext);
}
private object UpdateDictionaryByIndex(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType, string[] indexes)
{
IModelBinder keybinder = Binders.GetBinder(keyType);
IModelBinder valuebinder = Binders.GetBinder(valueType);
List<object, object>> items = new List<object, object>>();
foreach (string index in indexes)
{
string prefix = string.Format("{0}[{1}]", bindingContext.ModelName, index);
string keyname = CreateSubPropertyName(prefix, "key");
string valuename = CreateSubPropertyName(prefix, "value");
if (!bindingContext.ValueProvider.Keys.Any(s => s.StartsWith(keyname))) continue;
ModelBindingContext keyContext = new ModelBindingContext()
{
ModelName = keyname,
ModelState = bindingContext.ModelState,
ModelType = keyType,
ValueProvider = bindingContext.ValueProvider
};
object key = keybinder.BindModel(controllerContext, keyContext);
if (key == null) continue;
ModelBindingContext valueContext = new ModelBindingContext()
{
ModelName = valuename,
ModelState = bindingContext.ModelState,
ModelType = valueType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
object value = valuebinder.BindModel(controllerContext, valueContext);
Verify(bindingContext.ModelState, valuename, valueType, value);
items.Add(new KeyValuePair<object, object>(key, value));
}
if (items.Count == 0) return null;
object dictionary = bindingContext.Model;
ClearAndFillDictionary(keyType, valueType, dictionary, items);
return dictionary;
}
protected object UpdateCollectionByIndex(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType, string[] indexes)
{
IModelBinder binder = Binders.GetBinder(elementType);
List<object> items = new List<object>();
foreach (string index in indexes)
{
string prefix = string.Format("{0}[{1}]", bindingContext.ModelName, index);
if (!bindingContext.ValueProvider.Keys.Any(s => s.StartsWith(prefix))) continue;
ModelBindingContext objContext = new ModelBindingContext()
{
ModelName = prefix,
ModelState = bindingContext.ModelState,
ModelType = elementType,
PropertyFilter = bindingContext.PropertyFilter,
ValueProvider = bindingContext.ValueProvider
};
object obj = binder.BindModel(controllerContext, objContext);
Verify(bindingContext.ModelState, prefix, elementType, obj);
items.Add(obj);
}
if (items.Count == 0) return null;
object collection = bindingContext.Model;
ClearAndFillCollection(elementType, collection, items);
return collection;
}
protected static readonly MethodInfo _clearAndFillCollectionMethod = typeof(IndexModelBinder).GetMethod("ClearAndFillCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
protected static void ClearAndFillCollection(Type collectionType, object collection, IEnumerable contents)
{
_clearAndFillCollectionMethod.MakeGenericMethod(collectionType).Invoke(null, new object[] { collection, contents });
}
protected static void ClearAndFillCollectionImpl(ICollection collection, IEnumerable contents)
{
collection.Clear();
foreach (object o in contents)
collection.Add((o is T) ? (T)o : default(T));
}
protected static readonly MethodInfo _clearAndFillDictionaryMethod = typeof(IndexModelBinder).GetMethod("ClearAndFillDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
protected static void ClearAndFillDictionary(Type keyType, Type valueType, object dictionary, IEnumerable<object, object>> contents)
{
_clearAndFillDictionaryMethod.MakeGenericMethod(keyType, valueType).Invoke(null, new object[] { dictionary, contents });
}
protected static void ClearAndFillDictionaryImpl(IDictionary dictionary, IEnumerable<object, object>> contents)
{
dictionary.Clear();
foreach (var o in contents)
dictionary[(TKey)o.Key] = (o.Value is TValue) ? (TValue)o.Value : default(TValue);
}
protected static readonly MethodInfo _verifyMethod = typeof(DefaultModelBinder).GetMethod("VerifyValueUsability", BindingFlags.Static | BindingFlags.NonPublic);
protected static void Verify(ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)
{
_verifyMethod.Invoke(null, new object[] { modelState, modelStateKey, elementType, value });
}
protected static readonly MethodInfo _extractInterface = typeof(DefaultModelBinder).GetMethod("ExtractGenericInterface", BindingFlags.Static | BindingFlags.NonPublic);
protected static Type GetInterface(Type queryType, Type interfaceType)
{
return _extractInterface.Invoke(null, new object[] { queryType, interfaceType }) as Type;
}
#endregion
}
}
/*
To Use, set the default binder in the Global.asax.cs Application_Start()
ModelBinders.Binders.DefaultBinder = new IndexModelBinder();
I have implemented the beta behaviour if an index field exists, otherwise it passes through to the RC1 method of sequential.
NOTE: This will not automatically fall back to a blank model name for top level parameters, use a BindAttribute to specify a blank name
RC1 behaviour: Name your fields company.Contacts[0].FieldName, counting up from 0
<% for (int i=0;i<Model.Contacts.Count;i++)
ContactEntity contact = Model.Contacts[i]; {%>
<tr>
<td>Contacts.ContactId</td>
<td><%= Html.TextBox("company.Contacts[" + i + "].ContactId", contact.ContactId)%></td>
</tr>
<tr>
<td>Contacts.FirstName</td>
<td><%= Html.TextBox("company.Contacts[" + i + "].FirstName", contact.FirstName)%></td>
</tr>
<tr>
<td>Contacts.LastName</td>
<td><%= Html.TextBox("company.Contacts[" + i + "].LastName", contact.LastName)%></td>
</tr>
<% } %>
To add, you need to add a new set of fields numbered one higher.
Beta behaviour: looks for a field (.index) that holds the index values to iterate:
<% foreach (ContactEntity contact in Model.Contacts) {%>
<tr>
<td>Contacts.index</td>
<td><%= Html.TextBox("company.Contacts.index", contact.ContactId)%></td>
</tr>
<tr>
<td>Contacts.ContactId</td>
<td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].ContactId", contact.ContactId)%></td>
</tr>
<tr>
<td>Contacts.FirstName</td>
<td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].FirstName", contact.FirstName)%></td>
</tr>
<tr>
<td>Contacts.LastName</td>
<td><%= Html.TextBox("company.Contacts[" + contact.ContactId + "].LastName", contact.LastName)%></td>
</tr>
<% } %>
if you want to add a new object, create any unique index:
<%= Html.TextBox("company.Contacts.index", -1)%>
<%= Html.TextBox("company.Contacts[-1].ContactId", 0)%>
<%= Html.TextBox("company.Contacts[-1].FirstName")%>
*/