Привязка модели 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
тогда вы можете обойти это, написав пользовательское связующее для моделей.