When my model has an IEnumerable<T>
property that is implemented as an iterator (i.e. yield return
), MVC's DefaultModelBinder
cannot bind to that property when incoming values use square bracket syntax (eg "Foo[0]"
).
Example Model:
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); }
}
}
}
Failing example1:
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's 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's first element should have been set");
}
}
}
The above unit test will update the Bar
property of my model to be ["bar"]
no problem (with or without square brackets in the collection keys), but will fail to bind anything to the Foo
property.
Does anyone know (at a low level) why implementing an IEnumerable
property as an iterator would cause model binding to fail?
I'm not really interested in workarounds2, but rather some analysis, as I've exhausted my knowledge of the framework getting this far ;)
1: A unit test was the easiest way to isolate the problem for SO, rather than going through a whole MVC application example.
2: For example, I know that if I remove square brackets from the input and reuse the same "Foo"
key for all the values, the model binding will work. However the real failing case requires square brackets as each item in the collection is a complex type with it's own sub-properties. Or another workaround: add a non-iterator IEnumerable<T>
parameter to the action, and assign that to the property directly inside the action. Ugh.
Pretty simple, really. The DefaultModelBinder
won't overwrite your IEnumerable<>
instance if it's non-null. If it's null, it will create a new List<T>
and fill it.
If it's non-null, it has certain types of lists it knows how to deal with. If your list implements ICollection<>
, then it will fill it. But your instance (with yield
) can't be updated at all!
If you're comfortable with overwriting foobacking
then you can work around this by writing a custom model binder.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With