MVC 3 模型绑定子类型(抽象类或接口) [英] MVC 3 Model Binding a Sub Type (Abstract Class or Interface)
问题描述
假设我有一个 Product 模型,Product 模型有一个 ProductSubType(抽象)属性,我们有两个具体的实现 Shirt 和 Pants.
这是来源:
公共类产品{公共 int Id { 获取;放;}[必需的]公共字符串名称 { 获取;放;}[必需的]公共小数点?价格{得到;放;}[必需的]公众号?产品类型{获取;放;}公共 ProductTypeBase 子产品 { 获取;放;}}公共抽象类 ProductTypeBase { }公共类衬衫:ProductTypeBase{[必需的]公共字符串颜色 { 获取;放;}public bool HasSleeves { get;放;}}公共类裤子:ProductTypeBase{[必需的]公共字符串颜色 { 获取;放;}[必需的]公共字符串大小 { 获取;放;}}
在我的 UI 中,用户有一个下拉菜单,他们可以选择产品类型,然后根据正确的产品类型显示输入元素.我已经弄清楚了所有这些(使用 ajax get 进行下拉更改,返回部分/编辑器模板并相应地重新设置 jquery 验证).
接下来,我为 ProductTypeBase 创建了一个自定义模型绑定器.
公共覆盖对象 BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext){ProductTypeBase subType = null;var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));如果(产品类型 == 1){var 衬衫 = 新衬衫();shirt.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));shirt.HasSleeves = (bool)bindingContext.ValueProvider.GetValue("SubProduct.HasSleeves").ConvertTo(typeof(bool));子类型 = 衬衫;}否则如果(产品类型 == 2){var 裤子 = 新裤子();pants.Size = (string)bindingContext.ValueProvider.GetValue("SubProduct.Size").ConvertTo(typeof(string));pants.Color = (string)bindingContext.ValueProvider.GetValue("SubProduct.Color").ConvertTo(typeof(string));子类型 = 裤子;}返回子类型;}}
这将正确绑定值并且在大多数情况下都有效,除非我丢失了服务器端验证.所以我预感我做错了,我做了更多的搜索,发现了 Darin Dimitrov 的这个答案:
所以我将模型绑定器切换为仅覆盖 CreateModel,但现在它不绑定值.
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType){ProductTypeBase subType = null;var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));如果(产品类型 == 1){subType = 新衬衫();}否则如果(产品类型 == 2){subType = 新裤子();}返回子类型;}
通过 MVC 3 src,似乎在 BindProperties 中,GetFilteredModelProperties 返回一个空结果,我认为是因为 bindingcontext 模型设置为没有任何属性的 ProductTypeBase.
谁能发现我做错了什么?这似乎不应该这么困难.我确定我遗漏了一些简单的东西......我有另一种选择,而不是在 Product 模型中拥有一个 SubProduct 属性,而只是为衬衫和裤子拥有单独的属性.这些只是视图/表单模型,所以我认为这是可行的,但如果有任何了解正在发生的事情,我希望让当前的方法起作用......
感谢您的帮助!
更新:
我没说清楚,但是我添加的自定义模型绑定器,继承自 DefaultModelBinder
答案
设置 ModelMetadata 和 Model 是缺失的部分.谢谢玛纳斯!
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType){如果(modelType.Equals(typeof(ProductTypeBase))){类型实例化类型 = null;var productType = (int)bindingContext.ValueProvider.GetValue("ProductType").ConvertTo(typeof(int));如果(产品类型 == 1){实例化类型 = 类型(衬衫);}否则如果(产品类型 == 2){实例化类型 = 类型(裤子);}var obj = Activator.CreateInstance(instantiationType);bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);bindingContext.ModelMetadata.Model = obj;返回对象;}返回 base.CreateModel(controllerContext, bindingContext, modelType);}
这可以通过重写 CreateModel(...) 来实现.我会用一个例子来证明这一点.
1.让我们创建一个模型和一些基类和子类.
公共类 MyModel{公共 MyBaseClass BaseClass { 获取;放;}}公共抽象类 MyBaseClass{公共虚拟字符串 MyName{得到{返回MyBaseClass";}}}公共类 MyDerievedClass : MyBaseClass{公共 int MyProperty { 获取;放;}公共覆盖字符串 MyName{得到{返回MyDerievedClass";}}}
2.现在创建一个模型绑定器并覆盖 CreateModel
公共类 MyModelBinder : DefaultModelBinder{受保护的覆盖对象 CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType){///MyBaseClass 和 MyDerievedClass 是硬编码的.///我们可以使用反射来读取程序集并获取任何基类型的具体类型if (modelType.Equals(typeof(MyBaseClass))){类型实例化类型 = typeof(MyDerievedClass);var obj=Activator.CreateInstance(instantiationType);bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);bindingContext.ModelMetadata.Model = obj;返回对象;}返回 base.CreateModel(controllerContext, bindingContext, modelType);}}
3.现在在控制器中创建获取和发布操作.
[HttpGet]公共 ActionResult 索引(){ViewBag.Message = "欢迎使用 ASP.NET MVC!";MyModel 模型 = new MyModel();model.BaseClass = new MyDerievedClass();返回视图(模型);}[HttpPost]公共 ActionResult 索引(MyModel 模型){返回视图(模型);}
4.现在在 global.asax 中将 MyModelBinder 设置为默认 ModelBinder 这样做是为了为所有动作设置默认模型绑定器,对于单个动作,我们可以在动作参数中使用 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 = "索引";布局 = "~/Views/Shared/_Layout.cshtml";}<h2>索引</h2>@using (Html.BeginForm()) {@Html.ValidationSummary(true)<字段集><legend>MyModel</legend>@Html.EditorFor(m=>m.BaseClass,"DerievedView")<p><输入类型=提交"值=创建"/></p></fieldset>}
DerievedView.cshtml
@model MvcApplication2.Models.MyDerievedClass@Html.ValidationSummary(true)<字段集><legend>MyDerievedClass</legend><div class="editor-label">@Html.LabelFor(model => model.MyProperty)
<div class="editor-field">@Html.EditorFor(model => model.MyProperty)@Html.ValidationMessageFor(model => model.MyProperty)
</fieldset>
现在它将按预期工作,Controller 将收到一个MyDerievedClass"类型的对象.验证将按预期进行.
Say I have a Product model, the Product model has a property of ProductSubType (abstract) and we have two concrete implementations Shirt and Pants.
Here is the source:
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; }
}
In my UI, user has a dropdown, they can select the product type and the input elements are displayed according to the right product type. I have all of this figured out (using an ajax get on dropdown change, return a partial/editor template and re-setup the jquery validation accordingly).
Next I created a custom model binder for 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;
}
}
This binds the values correctly and works for the most part, except I lose the server side validation. So on a hunch that I am doing this incorrectly I did some more searching and came across this answer by Darin Dimitrov:
ASP.NET MVC 2 - Binding To Abstract Model
So I switched the model binder to only override CreateModel, but now it doesn't bind the values.
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;
}
Stepping though the MVC 3 src, it seems like in BindProperties, the GetFilteredModelProperties returns an empty result, and I think is because bindingcontext model is set to ProductTypeBase which doesn't have any properties.
Can anyone spot what I am doing wrong? This doesn't seem like it should be this difficult. I am sure I am missing something simple...I have another alternative in mind of instead of having a SubProduct property in the Product model to just have separate properties for Shirt and Pants. These are just View/Form models so I think that would work, but would like to get the current approach working if anything to understand what is going on...
Thanks for any help!
Update:
I didn't make it clear, but the custom model binder I added, inherits from the DefaultModelBinder
Answer
Setting ModelMetadata and Model was the missing piece. Thanks Manas!
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);
}
This can be achieved through overriding CreateModel(...). I will demonstrate that with an example.
1. Lets create a model and some base and child classes.
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. Now create a modelbinder and override 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. Now in the controller create get and post action.
[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. Now Set MyModelBinder as Default ModelBinder in global.asax This is done to set a default model binder for all actions, for a single action we can use ModelBinder attribute in action parameters)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.DefaultBinder = new MyModelBinder();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
5. Now we can create view of type MyModel and a partial view of type 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>
Now it will work as expected, Controller will receive an Object of type "MyDerievedClass". Validations will happen as expected.
这篇关于MVC 3 模型绑定子类型(抽象类或接口)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!