Привязка модели MVC: почему я не могу привязать к свойству итератора?

Когда моя модель имеет IEnumerable<T> свойство, которое реализовано как итератор (т.е. yield return), MVC DefaultModelBinder не может привязаться к этому свойству, когда входящие значения используют синтаксис в квадратных скобках (например, "Foo[0]").

Пример модели:

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); }
        }
    }
}

Неудачный пример 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'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");
        }

    }
}

Вышеуказанный модульный тест обновит Bar свойство моей модели быть ["bar"] без проблем (с квадратными скобками в ключах набора или без них), но ничего не сможет связать с Foo имущество.

Кто-нибудь знает (на низком уровне), почему реализация IEnumerable свойство как итератор приведет к сбою привязки модели?

Меня не очень интересуют обходные пути 2, а скорее некоторый анализ, так как я исчерпал свои знания в области фреймворка;)


1: модульное тестирование было самым простым способом изолировать проблему для SO, а не проходить через целый пример приложения MVC.

2: например, я знаю, что если я удалю квадратные скобки из ввода и повторно использую то же самое "Foo" ключ для всех значений, будет работать привязка модели. Однако для реального случая неудачи требуются квадратные скобки, поскольку каждый элемент в коллекции является сложным типом со своими собственными под-свойствами. Или другой обходной путь: добавьте не итератор IEnumerable<T> параметр для действия и назначить его свойству непосредственно внутри действия. Тьфу.

1 ответ

Довольно просто, правда. DefaultModelBinder не перезаписать ваш IEnumerable<> экземпляр, если он не нулевой. Если это ноль, это создаст новый List<T> и заполните его.

Если он не нулевой, у него есть определенные типы списков, с которыми он знает, как обращаться. Если ваш список реализует ICollection<>, тогда это заполнит это. Но ваш экземпляр (с yield) вообще не может быть обновлено!

Если вам удобно перезаписывать foobacking тогда вы можете обойти это, написав пользовательское связующее для моделей.

Другие вопросы по тегам