MVC Model Binding: why can't I bind an iterator property?

When my model has an IEnumerable<T> property implemented as an iterator (ie yield return ), MVC DefaultModelBinder cannot bind to this property when input values ​​use square bracket syntax (for example, "Foo[0]" ).

Model Example:

 namespace ModelBinderTest { using System.Collections.Generic; public class MyModel { private List<string> fooBacking = new List<string>(); public IEnumerable<string> Foo { get { foreach (var o in fooBacking) { yield return o; // <-- ITERATOR BREAKS MODEL BINDING } } set { fooBacking = new List<string>(value); } } private List<string> barBacking = new List<string>(); public IEnumerable<string> Bar { get { // Returning any non-iterator IEnumerable works here. Eg: return new List<string>(barBacking); } set { barBacking = new List<string>(value); } } } } 

Bad Example 1 :

 namespace ModelBinderTest { using System; using System.Linq; using System.Web.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] [CLSCompliant(false)] public class DefaultModelBinderTestIterator { [TestMethod] public void BindsIterator() { // Arrange var model = new MyModel(); ModelBindingContext bindingContext = new ModelBindingContext() { FallbackToEmptyPrefix = true, ModelMetadata = ModelMetadataProviders .Current .GetMetadataForType(null, model.GetType()), ModelName = "", ValueProvider = new NameValueCollectionValueProvider( new System.Collections.Specialized.NameValueCollection() { { "Foo[0]", "foo" }, { "Bar[0]", "bar" }, }, System.Globalization.CultureInfo.InvariantCulture ) }; DefaultModelBinder binder = new DefaultModelBinder(); // Act MyModel updatedModel = (MyModel)binder.BindModel( new ControllerContext(), bindingContext); // Assert Assert.AreEqual(1, updatedModel.Bar.Count(), "Bar property should have been updated"); Assert.AreEqual("bar", updatedModel.Bar.ElementAtOrDefault(0), "Bar first element should have been set"); Assert.AreEqual(1, updatedModel.Foo.Count(), "Foo property should have been updated"); Assert.AreEqual("foo", updatedModel.Foo.ElementAtOrDefault(0), "Foo first element should have been set"); } } } 

The unit test above will update the Bar property of my model as ["bar"] without problems (with or without square brackets in the collection keys), but it will not be able to bind anything to the Foo property.

Does anyone know (at a low level) why introducing the IEnumerable property as an iterator will cause model binding to fail?

I do not really like workarounds 2 but rather some analysis, since I have exhausted my knowledge that the structure has come to this;)


1: A unit test was the easiest way to isolate the problem for SO, and not through the entire MVC sample application.

2: For example, I know that if I remove the square brackets from the input and reuse the same "Foo" key for all values, model binding will work. However, a real bad case requires square brackets, because each element in the collection is a complex type with its own sub-properties. Or another workaround: add the non-iterator IEnumerable<T> parameter to the action and assign this property directly inside the action. Ugh.Sub>

+6
iterator asp.net-mvc ienumerable model-binding
source share
1 answer

Pretty simple, actually. DefaultModelBinder does not overwrite your instance of IEnumerable<> if it is not equal to zero. If it is zero, it will create a new List<T> and populate it.

If it is not null, it has certain types of lists in which it knows how to fight. If your list implements ICollection<> , it will populate it. But your instance (with yield ) cannot be updated at all!

If you enjoy rewriting foobacking , then you can work around this by writing a custom foobacking .

+3
source share

All Articles