MVC 3 Model Binding a Sub Type (абстрактный класс или интерфейс)
Скажем, у меня есть модель продукта, модель продукта имеет свойство ProductSubType (abstract), и у нас есть две конкретные реализации Shirt and Pants.
Вот источник:
public class Product
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public decimal? Price { get; set; }
[Required]
public int? ProductType { get; set; }
public ProductTypeBase SubProduct { get; set; }
}
public abstract class ProductTypeBase { }
public class Shirt : ProductTypeBase
{
[Required]
public string Color { get; set; }
public bool HasSleeves { get; set; }
}
public class Pants : ProductTypeBase
{
[Required]
public string Color { get; set; }
[Required]
public string Size { get; set; }
}
В моем пользовательском интерфейсе пользователь имеет выпадающий список, они могут выбирать тип продукта, а элементы ввода отображаются в соответствии с правильным типом продукта. Я все это понял (используя ajax, получив изменение выпадающего списка, верните шаблон частичного/редактора и соответствующим образом переустановите проверку jquery).
Затем я создал настраиваемое связующее устройство для ProductTypeBase.
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ProductTypeBase subType = null;
var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));
if (productType == 1)
{
var shirt = new Shirt();
shirt.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));
shirt.HasSleeves = (bool)bindingContext.ValueProvider.GetValue("SubProduct.HasSleeves").ConvertTo(typeof(bool));
subType = shirt;
}
else if (productType == 2)
{
var pants = new Pants();
pants.Size = (string)bindingContext.ValueProvider.GetValue("SubProduct.Size").ConvertTo(typeof(string));
pants.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));
subType = pants;
}
return subType;
}
}
Это правильно привязывает значения и работает по большей части, за исключением того, что я теряю проверку на стороне сервера. Поэтому, подозревая, что я делаю это неправильно, я сделал еще несколько поисков и наткнулся на этот ответ Дарина Димитрова:
ASP.NET MVC 2 - привязка к абстрактной модели
Итак, я переключил смену модели только для переопределения CreateModel, но теперь она не связывает значения.
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
ProductTypeBase subType = null;
var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));
if (productType == 1)
{
subType = new Shirt();
}
else if (productType == 2)
{
subType = new Pants();
}
return subType;
}
Выбрав MVC 3 src, похоже, что в BindProperties, GetFilteredModelProperties возвращает пустой результат, и я думаю, это потому, что для модели bindingcontext задана ProductTypeBase, которая не имеет никаких свойств.
Может ли кто-нибудь определить, что я делаю неправильно? Это не похоже, что это должно быть так сложно. Я уверен, что мне не хватает чего-то простого... У меня есть другая альтернатива, вместо того, чтобы иметь свойство SubProduct в модели Product, чтобы иметь только отдельные свойства для Shirt and Pants. Это всего лишь модели View/Form, поэтому я думаю, что это сработает, но хотелось бы, чтобы текущий подход работал, если что-то понять, что происходит...
Спасибо за любую помощь!
Update:
Я не дал понять, но добавленное пользовательское связующее устройство наследует от DefaultModelBinder
Ответ
Установка ModelMetadata и модели была отсутствующей частью. Спасибо Манас!
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
if (modelType.Equals(typeof(ProductTypeBase))) {
Type instantiationType = null;
var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));
if (productType == 1) {
instantiationType = typeof(Shirt);
}
else if (productType == 2) {
instantiationType = typeof(Pants);
}
var obj = Activator.CreateInstance(instantiationType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
bindingContext.ModelMetadata.Model = obj;
return obj;
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
Ответы
Ответ 1
Это может быть достигнуто путем переопределения CreateModel (...). Я продемонстрирую это с помощью примера.
1. Давайте создадим модель и некоторые базовые и дочерние классы.
public class MyModel
{
public MyBaseClass BaseClass { get; set; }
}
public abstract class MyBaseClass
{
public virtual string MyName
{
get
{
return "MyBaseClass";
}
}
}
public class MyDerievedClass : MyBaseClass
{
public int MyProperty { get; set; }
public override string MyName
{
get
{
return "MyDerievedClass";
}
}
}
2. Теперь создайте modelbinder и переопределите CreateModel
public class MyModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
/// MyBaseClass and MyDerievedClass are hardcoded.
/// We can use reflection to read the assembly and get concrete types of any base type
if (modelType.Equals(typeof(MyBaseClass)))
{
Type instantiationType = typeof(MyDerievedClass);
var obj=Activator.CreateInstance(instantiationType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
bindingContext.ModelMetadata.Model = obj;
return obj;
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
3. Теперь в контроллере создайте действие get и post.
[HttpGet]
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
MyModel model = new MyModel();
model.BaseClass = new MyDerievedClass();
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel model)
{
return View(model);
}
4. Теперь установите MyModelBinder как ModelBinder по умолчанию в global.asax. Это делается для установки стандартного связующего объекта для всех действий, для одного действия мы можем использовать атрибут ModelBinder в параметрах действия)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.DefaultBinder = new MyModelBinder();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
5. Теперь мы можем создать представление типа MyModel и частичный вид типа MyDerievedClass
Index.cshtml
@model MvcApplication2.Models.MyModel
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>MyModel</legend>
@Html.EditorFor(m=>m.BaseClass,"DerievedView")
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
DerievedView.cshtml
@model MvcApplication2.Models.MyDerievedClass
@Html.ValidationSummary(true)
<fieldset>
<legend>MyDerievedClass</legend>
<div class="editor-label">
@Html.LabelFor(model => model.MyProperty)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.MyProperty)
@Html.ValidationMessageFor(model => model.MyProperty)
</div>
</fieldset>
Теперь он будет работать, как ожидалось, контроллер получит объект типа "MyDerievedClass".
Проверки будут выполняться, как ожидалось.
![enter image description here]()
Ответ 2
У меня была та же проблема, я закончил использование MvcContrib как sugested здесь.
documentation устарел, но если вы посмотрите на образцы, это довольно легко.
Вам необходимо зарегистрировать свои типы в Global.asax:
protected void Application_Start(object sender, EventArgs e) {
// (...)
DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) });
}
Добавьте к своим частичным представлениям две строки:
@model MvcApplication.Models.Shirt
@using MvcContrib.UI.DerivedTypeModelBinder
@Html.TypeStamp()
<div>
@Html.LabelFor(m => m.Color)
</div>
<div>
@Html.EditorFor(m => m.Color)
@Html.ValidationMessageFor(m => m.Color)
</div>
Наконец, в главном представлении (с помощью EditorTemplates):
@model MvcApplication.Models.Product
@{
ViewBag.Title = "Products";
}
<h2>
@ViewBag.Title</h2>
@using (Html.BeginForm()) {
<div>
@Html.LabelFor(m => m.Name)
</div>
<div>
@Html.EditorFor(m => m.Name)
@Html.ValidationMessageFor(m => m.Name)
</div>
<div>
@Html.EditorFor(m => m.SubProduct)
</div>
<p>
<input type="submit" value="create" />
</p>
}
Ответ 3
хорошо
У меня была такая же проблема, и я решил более общим образом, я думаю.
В моем случае я отправляю объект через Json из бэкэнд в клиент и от клиента к серверу:
Прежде всего, в абстрактном классе у меня есть поле, которое я устанавливаю в конструкторе:
ClassDescriptor = this.GetType().AssemblyQualifiedName;
Итак, в Json у меня есть поле ClassDescriptor
Следующим шагом было написать пользовательское связующее:
public class SmartClassBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
string field = String.Join(".", new String[]{bindingContext.ModelName , "ClassDescriptor"} );
var values = (ValueProviderCollection) bindingContext.ValueProvider;
var classDescription = (string) values.GetValue(field).ConvertTo(typeof (string));
modelType = Type.GetType(classDescription);
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
И теперь мне нужно только украсить класс атрибутом. Например:
[ModelBinder (TypeOf (SmartClassBinder))]
открытый класс ConfigurationItemDescription
Что это.