MVC 3 模型绑定子类型(抽象类或接口)
假设我有一个 Product 模型,该 Product 模型具有 ProductSubType (抽象)属性,并且我们有两个具体实现 Shirt 和 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;
}
}
这可以正确绑定值并且在大多数情况下都有效,除了我丢失了服务器端验证。因此,我预感我这样做不正确,我做了更多搜索,并发现了 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));
if (productType == 1)
{
subType = new Shirt();
}
else if (productType == 2)
{
subType = new Pants();
}
return subType;
}
单步执行 MVC 3 src,似乎在 BindProperties 中,GetFilteredModelProperties 返回一个空结果,我认为这是因为 BindingContext 模型设置为 ProductTypeBase ,它没有任何属性。
谁能发现我做错了什么吗?这似乎不应该这么困难。我确信我错过了一些简单的东西...我有另一种选择,而不是在产品模型中使用 SubProduct 属性,而只为衬衫和裤子设置单独的属性。这些只是视图/表单模型,所以我认为这会起作用,但希望当前的方法能够工作,如果有什么可以理解正在发生的事情......
感谢您的帮助!
更新:
我没有说清楚,但我添加的自定义模型绑定器继承自 DefaultModelBinder
答案
设置 ModelMetadata 和 Model 是缺失的部分。谢谢玛纳斯!
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);
}
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);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
这可以通过重写 CreateModel(...) 来实现。我将用一个例子来证明这一点。
1.让我们创建一个模型以及一些基类和子类。
2.现在创建一个模型绑定器并覆盖 CreateModel
3。现在在控制器中创建 get 和 post 操作。
4.现在将 MyModelBinder 设置为 global.asax 中的默认 ModelBinder 这样做是为了为所有操作设置默认模型绑定器,对于单个操作,我们可以在操作参数中使用 ModelBinder 属性)
5。现在我们可以创建 MyModel 类型的视图和 MyDerievedClass 类型的部分视图
Index.cshtml
DerievedView.cshtml
现在它将按预期工作,控制器将收到类型为“MyDerievedClass”的对象。
验证将按预期进行。
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.
2. Now create a modelbinder and override CreateModel
3. Now in the controller create get and post action.
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)
5. Now we can create view of type MyModel and a partial view of type MyDerievedClass
Index.cshtml
DerievedView.cshtml
Now it will work as expected, Controller will receive an Object of type "MyDerievedClass".
Validations will happen as expected.
我遇到了同样的问题,我最终使用了 MvcContrib 作为建议 此处。
文档已过时,但如果您查看示例,就会发现非常简单。
您必须在 Global.asax 中注册您的类型:
将两行添加到您的部分视图中:
最后,在主视图中(使用 EditorTemplates):
I had the same problem, I ended up using MvcContrib as sugested here.
The documentation is outdated but if you look at the samples it's pretty easy.
You'll have to register your types in the Global.asax:
Add two lines to your partial views:
Finally, in the main view (using EditorTemplates):
出色地
我遇到了同样的问题,并且我认为我已经以更通用的方式解决了。
在我的例子中,我通过 Json 从后端到客户端以及从客户端到后端发送对象:
首先,在抽象类中,我有在构造函数中设置的字段:
所以在 Json 中,我有 ClassDescriptor 字段
接下来是编写自定义绑定器:
现在我所要做的就是用属性来装饰类。例如:
[ModelBinder(typeof(SmartClassBinder))]
公共类 ConfigurationItemDescription
就是这样。
well
I had this same problem and I have solved in a more general way I think.
In My case I'm sending object thru Json from backend to client and from client to backend:
First of all In abstract class I have field that i set in constructor:
So In Json I Have ClassDescriptor field
Next thing was to write custom binder:
And now all I have to do is to decorate class with attribute. For example:
[ModelBinder(typeof(SmartClassBinder))]
public class ConfigurationItemDescription
That's it.