asp.net-mvc MVC 3 模型绑定子类型(抽象类或接口)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9417888/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 01:46:21  来源:igfitidea点击:

MVC 3 Model Binding a Sub Type (Abstract Class or Interface)

asp.net-mvcasp.net-mvc-3model-binding

提问by B Z

Say I have a Product model, the Product model has a property of ProductSubType (abstract) and we have two concrete implementations Shirt and Pants.

假设我有一个 Product 模型,Product 模型有一个 ProductSubType(抽象)属性,我们有两个具体的实现 Shirt 和 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).

在我的 UI 中,用户有一个下拉菜单,他们可以选择产品类型并根据正确的产品类型显示输入元素。我已经弄清楚了所有这些(使用 ajax get 进行下拉更改,返回部分/编辑器模板并相应地重新设置 jquery 验证)。

Next I created a custom model binder for ProductTypeBase.

接下来,我为 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:

这将正确绑定值并在大多数情况下工作,除非我丢失了服务器端验证。因此,我预感我做错了,我进行了更多搜索,并发现了 Darin Dimitrov 的这个答案:

ASP.NET MVC 2 - Binding To Abstract Model

ASP.NET MVC 2 - 绑定到抽象模型

So I switched the model binder to only override CreateModel, but now it doesn't bind the values.

所以我将模型绑定器切换为仅覆盖 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;
    }

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.

通过 MVC 3 src,它似乎在 BindProperties 中,GetFilteredModelProperties 返回一个空结果,我认为是因为 bindingcontext 模型设置为没有任何属性的 ProductTypeBase。

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...

谁能发现我做错了什么?这似乎不应该如此困难。我确定我遗漏了一些简单的东西......我有另一种选择,而不是在 Product 模型中拥有一个 SubProduct 属性,而只为衬衫和裤子拥有单独的属性。这些只是视图/表单模型,所以我认为这是可行的,但如果有任何了解正在发生的事情,我希望让当前的方法起作用......

Thanks for any help!

谢谢你的帮助!

Update:

更新:

I didn't make it clear, but the custom model binder I added, inherits from the DefaultModelBinder

我没有说清楚,但是我添加的自定义模型绑定器,继承自 DefaultModelBinder

Answer

回答

Setting ModelMetadata and Model was the missing piece. Thanks Manas!

设置 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);

        }

回答by Manas

This can be achieved through overriding CreateModel(...). I will demonstrate that with an example.

这可以通过覆盖 CreateModel(...) 来实现。我将用一个例子来证明这一点。

1. Lets create a model and some base and child classes.

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. Now create a modelbinder and override CreateModel

2. 现在创建一个模型绑定器并覆盖 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.

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. Now Set MyModelBinder as Default ModelBinder in global.asaxThis is done to set a default model binder for all actions, for a single action we can use ModelBinder attribute in action parameters)

4. 现在在 global.asax 中将 MyModelBinder 设置为默认 ModelBinder这样做是为了为所有动作设置默认模型绑定器,对于单个动作我们可以在动作参数中使用 ModelBinder 属性)

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

5. 现在我们可以创建 MyModel 类型的视图和 MyDerievedClass 类型的部分视图

Index.cshtml

索引.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

派生视图.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.

现在它将按预期工作,Controller 将收到一个“MyDerievedClass”类型的对象。验证将按预期进行。

enter image description here

在此处输入图片说明

回答by Marcel de Castilho

I had the same problem, I ended up using MvcContribas sugested here.

我遇到了同样的问题,我最终使用了MvcContrib作为sugested here

The documentationis outdated but if you look at the samples it's pretty easy.

文件是过时的,但如果你看看样品它很容易。

You'll have to register your types in the Global.asax:

您必须在 Global.asax 中注册您的类型:

protected void Application_Start(object sender, EventArgs e) {
    // (...)
    DerivedTypeModelBinderCache.RegisterDerivedTypes(typeof(ProductTypeBase), new[] { typeof(Shirt), typeof(Pants) });
}

Add two lines to your partial views:

在局部视图中添加两行:

@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>

Finally, in the main view (using EditorTemplates):

最后,在主视图中(使用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>
}

回答by bunny1985

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:

好吧,我遇到了同样的问题,我认为我已经以更一般的方式解决了。在我的情况下,我通过 Json 从后端到客户端以及从客户端到后端发送对象:

First of all In abstract class I have field that i set in constructor:

首先,在抽象类中,我在构造函数中设置了字段:

ClassDescriptor = this.GetType().AssemblyQualifiedName;

So In Json I Have ClassDescriptor field

所以在 Json 我有 ClassDescriptor 字段

Next thing was to write custom binder:

接下来是编写自定义活页夹:

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);
        }       
}

And now all I have to do is to decorate class with attribute. For example:

现在我要做的就是用属性装饰类。例如:

[ModelBinder(typeof(SmartClassBinder))] public class ConfigurationItemDescription

[ModelBinder(typeof(SmartClassBinder))] 公共类 ConfigurationItemDescription

That's it.

就是这样。