asp.net-mvc 多态模型绑定

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/7222533/
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:26:11  来源:igfitidea点击:

Polymorphic model binding

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

提问by Erik Funkenbusch

This question has been asked beforein earlier versions of MVC. There is also this blog entryabout a way to work around the problem. I'm wondering if MVC3 has introduced anything that might help, or if there are any other options.

这个问题在早期版本的 MVC 中已经被问过。还有一个关于解决该问题的方法的博客条目。我想知道 MVC3 是否引入了任何可能有帮助的东西,或者是否还有其他选择。

In a nutshell. Here's the situation. I have an abstract base model, and 2 concrete subclasses. I have a strongly typed view that renders the models with EditorForModel(). Then I have custom templates to render each concrete type.

简而言之。这是情况。我有一个抽象的基本模型和 2 个具体的子类。我有一个强类型视图,它使用EditorForModel(). 然后我有自定义模板来呈现每个具体类型。

The problem comes at post time. If I make the post action method take the base class as the parameter, then MVC can't create an abstract version of it (which i would not want anyways, i'd want it to create the actual concrete type). If I create multiple post action methods that vary only by parameter signature, then MVC complains that it's ambiguous.

问题出现在发布时间。如果我让 post action 方法将基类作为参数,那么 MVC 就不能创建它的抽象版本(无论如何我都不想要它,我希望它创建实际的具体类型)。如果我创建多个仅因参数签名而异的 post action 方法,则 MVC 会抱怨它不明确。

So as far as I can tell, I have a few choices on how to solve this proble. I don't like any of them for various reasons, but i will list them here:

因此,据我所知,关于如何解决这个问题,我有几个选择。由于各种原因,我不喜欢它们中的任何一个,但我将在此处列出它们:

  1. Create a custom model binder as Darin suggests in the first post I linked to.
  2. Create a discriminator attribute as the second post I linked to suggests.
  3. Post to different action methods based on type
  4. ???
  1. 按照 Darin 在我链接的第一篇文章中建议的那样创建自定义模型绑定器。
  2. 创建一个鉴别器属性,作为我链接到的第二篇文章的建议。
  3. 根据类型发布到不同的操作方法
  4. ???

I don't like 1, because it is basically configuration that is hidden. Some other developer working on the code may not know about it and waste a lot of time trying to figure out why things break when changes things.

我不喜欢 1,因为它基本上是隐藏的配置。其他一些在代码上工作的开发人员可能不知道它,并浪费了大量时间试图弄清楚为什么在更改内容时会中断。

I don't like 2, because it seems kind of hacky. But, i'm leaning towards this approach.

我不喜欢 2,因为它看起来有点 hacky。但是,我倾向于这种方法。

I don't like 3, because that means violating DRY.

我不喜欢 3,因为这意味着违反 DRY。

Any other suggestions?

还有其他建议吗?

Edit:

编辑:

I decided to go with Darin's method, but made a slight change. I added this to my abstract model:

我决定采用 Darin 的方法,但做了一些细微的改变。我将此添加到我的抽象模型中:

[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}

Then a hidden automatically gets generated in my DisplayForModel(). The only thing you have to remember is that if you're not using DisplayForModel(), you'll have to add it yourself.

然后在我的DisplayForModel(). 您唯一需要记住的是,如果您不使用DisplayForModel(),则必须自己添加。

回答by Darin Dimitrov

Since I obviously opt for option 1 (:-)) let me try to elaborate it a little more so that it is less breakableand avoid hardcoding concrete instances into the model binder. The idea is to pass the concrete type into a hidden field and use reflection to instantiate the concrete type.

由于我显然选择了选项 1 (:-)),让我尝试对其进行详细说明,以使其不易损坏并避免将具体实例硬编码到模型绑定器中。这个想法是将具体类型传递到隐藏字段中,并使用反射来实例化具体类型。

Suppose that you have the following view models:

假设您有以下视图模型:

public abstract class BaseViewModel
{
    public int Id { get; set; }
}

public class FooViewModel : BaseViewModel
{
    public string Foo { get; set; }
}

the following controller:

以下控制器:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new FooViewModel { Id = 1, Foo = "foo" };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(BaseViewModel model)
    {
        return View(model);
    }
}

the corresponding Indexview:

相应的Index观点:

@model BaseViewModel
@using (Html.BeginForm())
{
    @Html.Hidden("ModelType", Model.GetType())    
    @Html.EditorForModel()
    <input type="submit" value="OK" />
}

and the ~/Views/Home/EditorTemplates/FooViewModel.cshtmleditor template:

~/Views/Home/EditorTemplates/FooViewModel.cshtml编辑器模板:

@model FooViewModel
@Html.EditorFor(x => x.Id)
@Html.EditorFor(x => x.Foo)

Now we could have the following custom model binder:

现在我们可以拥有以下自定义模型绑定器:

public class BaseViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
        var type = Type.GetType(
            (string)typeValue.ConvertTo(typeof(string)),
            true
        );
        if (!typeof(BaseViewModel).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("Bad Type");
        }
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
        return model;
    }
}

The actual type is inferred from the value of the ModelTypehidden field. It is not hardcoded, meaning that you could add other child types later without having to ever touch this model binder.

实际类型是从ModelType隐藏字段的值推断出来的。它不是硬编码的,这意味着您可以稍后添加其他子类型而无需接触此模型绑定器。

This same technique could be easily be appliedto collections of base view models.

同样的技术可以很容易地应用于基本视图模型的集合。

回答by Erik Funkenbusch

I have just thought of an intersting solution to this problem. Instead of using Parameter bsed model binding like this:

我刚刚想到了这个问题的一个有趣的解决方案。而不是像这样使用 Parameter bsed 模型绑定:

[HttpPost]
public ActionResult Index(MyModel model) {...}

I can instead use TryUpdateModel() to allow me to determine what kind of model to bind to in code. For example I do something like this:

我可以改为使用 TryUpdateModel() 来确定要在代码中绑定的模型类型。例如我做这样的事情:

[HttpPost]
public ActionResult Index() {...}
{
    MyModel model;
    if (ViewData.SomeData == Something) {
        model = new MyDerivedModel();
    } else {
        model = new MyOtherDerivedModel();
    }

    TryUpdateModel(model);

    if (Model.IsValid) {...}

    return View(model);
}

This actually works a lot better anyways, because if i'm doing any processing, then I would have to cast the model to whatever it actually is anyways, or use isto to figure out the correct Map to call with AutoMapper.

无论如何,这实际上效果要好得多,因为如果我正在做任何处理,那么无论如何我都必须将模型转换为它实际是的任何东西,或者is用来找出正确的 Map 以使用 AutoMapper 调用。

I guess those of us who haven't been using MVC since day 1 forget about UpdateModeland TryUpdateModel, but it still has its uses.

我想我们这些从第一天起就没有使用 MVC 的人忘记了UpdateModeland TryUpdateModel,但它仍然有它的用途。

回答by mindplay.dk

It took me a good day to come up with an answer to a closely related problem - although I'm not sure it's precisely the same issue, I'm posting it here in case others are looking for a solution to the same exact problem.

我花了一天的时间才想出一个密切相关问题的答案 - 尽管我不确定这是否完全相同,但我将其发布在这里,以防其他人正在寻找相同问题的解决方案。

In my case, I have an abstract base-type for a number of different view-model types. So in the main view-model, I have a property of an abstract base-type:

就我而言,我有许多不同视图模型类型的抽象基类型。所以在主视图模型中,我有一个抽象基类型的属性:

class View
{
    public AbstractBaseItemView ItemView { get; set; }
}

I have a number of sub-types of AbstractBaseItemView, many of which define their own exclusive properties.

我有许多 AbstractBaseItemView 的子类型,其中许多定义了自己的专有属性。

My problem is, the model-binder does not look at the type of object attached to View.ItemView, but instead looks only at the declared property-type, which is AbstractBaseItemView - and decides to bind onlythe properties defined in the abstract type, ignoring properties specific to the concrete type of AbstractBaseItemView that happens to be in use.

我的问题是,模型绑定器不查看附加到 View.ItemView 的对象类型,而是只查看声明的属性类型,即 AbstractBaseItemView - 并决定绑定抽象类型中定义的属性,忽略特定于正在使用的 AbstractBaseItemView 的具体类型的属性。

The work-around for this isn't pretty:

解决这个问题的方法并不漂亮:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

// ...

public class ModelBinder : DefaultModelBinder
{
    // ...

    override protected ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsAbstract && bindingContext.Model != null)
        {
            var concreteType = bindingContext.Model.GetType();

            if (Nullable.GetUnderlyingType(concreteType) == null)
            {
                return new AssociatedMetadataTypeTypeDescriptionProvider(concreteType).GetTypeDescriptor(concreteType);
            }
        }

        return base.GetTypeDescriptor(controllerContext, bindingContext);
    }

    // ...
}

Although this change feels hacky and is very "systemic", it seems to work - and does not, as far as I can figure, pose a considerable security-risk, since it does nottie into CreateModel() and thus does notallow you to post whatever and trick the model-binder into creating just any object.

虽然这种变化感觉哈克,是非常“系统性”,似乎工作-并且没有,就我自己看着办,造成相当大的安全风险,因为它并没有扎入CreateModel(),因此它不会让你发布任何内容并欺骗模型绑定器创建任何对象。

It also works only when the declared property-type is an abstracttype, e.g. an abstract class or an interface.

它也仅在声明的属性类型是抽象类型时才起作用,例如抽象类或接口。

On a related note, it occurs to me that other implementations I've seen here that override CreateModel() probably will onlywork when you're posting entirely new objects - and will suffer from the same problem I ran into, when the declared property-type is of an abstract type. So you most likely won't be able to editspecific properties of concrete types on existingmodel objects, but only create new ones.

在相关说明中,我发现我在这里看到的其他实现覆盖 CreateModel() 可能在您发布全新对象时才有效 - 并且会遇到我遇到的相同问题,当声明的属性-type 是抽象类型。因此,您很可能无法在现有模型对象上编辑具体类型的特定属性,而只能创建新的属性。

So in other words, you will probably need to integrate this work-around into your binder to also be able to properly edit objects that were added to the view-model prior to binding... Personally, I feel that's a safer approach, since I control what concrete type gets added - so the controller/action can, indirectly, specify the concrete type that may be bound, by simply populating the property with an empty instance.

因此,换句话说,您可能需要将此解决方法集成到您的活页夹中,以便还能够正确编辑在绑定之前添加到视图模型中的对象......就个人而言,我认为这是一种更安全的方法,因为我控制添加的具体类型 - 因此控制器/操作可以通过简单地用空实例填充属性来间接指定可能绑定的具体类型。

I hope this is helpful to others...

我希望这对其他人有帮助...

回答by counsellorben

Using Darin's method to discriminate your model types via a hidden field in your view, I would recommend that you use a custom RouteHandlerto distinguish your model types, and direct each one to a uniquely named action on your controller. For example, if you have two concrete models, Foo and Bar, for your Createaction in your controller, make a CreateFoo(Foo model)action and a CreateBar(Bar model)action. Then, make a custom RouteHandler, as follows:

使用 Darin 的方法通过视图中的隐藏字段区分模型类型,我建议您使用自定义RouteHandler来区分模型类型,并将每个模型类型定向到控制器上唯一命名的操作。例如,如果您有两个具体模型 Foo 和 Bar,用于您Create在控制器中的操作,请执行一个CreateFoo(Foo model)操作和一个CreateBar(Bar model)操作。然后,制作一个自定义的RouteHandler,如下:

public class MyRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        var httpContext = requestContext.HttpContext;
        var modelType = httpContext.Request.Form["ModelType"]; 
        var routeData = requestContext.RouteData;
        if (!String.IsNullOrEmpty(modelType))
        {
            var action = routeData.Values["action"];
            routeData.Values["action"] = action + modelType;
        }
        var handler = new MvcHandler(requestContext);
        return handler; 
    }
}

Then, in Global.asax.cs, change RegisterRoutes()as follows:

然后,在 Global.asax.cs 中,更改RegisterRoutes()如下:

public static void RegisterRoutes(RouteCollection routes) 
{ 
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 

    AreaRegistration.RegisterAllAreas(); 

    routes.Add("Default", new Route("{controller}/{action}/{id}", 
        new RouteValueDictionary( 
            new { controller = "Home",  
                  action = "Index",  
                  id = UrlParameter.Optional }), 
        new MyRouteHandler())); 
} 

Then, when a Create request comes in, if a ModelType is defined in the returned form, the RouteHandler will append the ModelType to the action name, allowing a unique action to be defined for each concrete model.

然后,当 Create 请求进来时,如果在返回的表单中定义了 ModelType,RouteHandler 会将 ModelType 附加到操作名称,从而允许为每个具体模型定义唯一的操作。