MVC 3 模型绑定子类型(抽象类或接口)

发布于 2025-01-08 12:11:05 字数 4158 浏览 0 评论 0原文

假设我有一个 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 的答案:

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 属性,而只为衬衫和裤子设置单独的属性。这些只是视图/表单模型,所以我认为这会起作用,但希望当前的方法能够工作,如果有什么可以理解正在发生的事情......

感谢您的帮助!

更新:

我没有说清楚,但我添加的自定义模型绑定器继承自 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

温柔戏命师 2025-01-15 12:11:05

这可以通过重写 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.现在创建一个模型绑定器并覆盖 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 设置为 global.asax 中的默认 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 = "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”的对象。
验证将按预期进行。

在此处输入图像描述

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.

enter image description here

日记撕了你也走了 2025-01-15 12:11:05

我遇到了同样的问题,我最终使用了 MvcContrib 作为建议 此处

文档已过时,但如果您查看示例,就会发现非常简单。

您必须在 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>
}

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:

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):

@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>
}
油饼 2025-01-15 12:11:05

出色地
我遇到了同样的问题,并且我认为我已经以更通用的方式解决了。
在我的例子中,我通过 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

就是这样。

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:

ClassDescriptor = this.GetType().AssemblyQualifiedName;

So In Json I Have ClassDescriptor field

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

That's it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文