Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 335
Roatin Marth Avatar asked Feb 08 '11 20:02

Roatin Marth


1 Answers

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.

like image 65
Craig Stuntz Avatar answered Nov 07 '22 08:11

Craig Stuntz