Привязка модели MVC: почему я не могу привязать свойство итератора? - PullRequest
6 голосов
/ 08 февраля 2011

Когда моя модель имеет свойство 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 Ответ

3 голосов
/ 09 февраля 2011

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

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

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

...