多态模型绑定

发布于 2024-12-02 02:25:12 字数 1163 浏览 3 评论 0原文

在早期版本的 MVC 中之前已经问过这个问题。还有此博客条目介绍了一种方法解决这个问题。我想知道 MVC3 是否引入了任何可能有帮助的内容,或者是否还有其他选项。

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

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

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

  1. 创建一个自定义模型活页夹,正如达林在我链接到的第一篇文章中建议的那样。
  2. 按照我链接到的第二篇文章的建议创建一个鉴别器属性。
  3. 根据类型发布到不同的操作方法
  4. ???

我不喜欢1,因为它基本上是隐藏的配置。其他一些处理代码的开发人员可能不知道这一点,并且会浪费大量时间试图弄清楚为什么在更改内容时会出现问题。

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

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

还有其他建议吗?

编辑:

我决定采用达林的方法,但做了一些小小的改变。我将其添加到我的抽象模型中:

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

然后在我的 DisplayForModel() 中自动生成隐藏。您唯一需要记住的是,如果您没有使用 DisplayForModel(),则必须自己添加它。

This question has been asked before in earlier versions of MVC. There is also this blog entry about 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.

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.

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.

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

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.

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

I don't like 3, because that means violating 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:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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

评论(4

回忆凄美了谁 2024-12-09 02:25:12

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

假设您有以下视图模型:

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

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

以下控制器:

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

相应的 Index 视图:

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

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

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

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

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

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

同样的技术可以轻松应用于基础集合查看模型。

Since I obviously opt for option 1 (:-)) let me try to elaborate it a little more so that it is less breakable and 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.

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 Index view:

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

and the ~/Views/Home/EditorTemplates/FooViewModel.cshtml editor template:

@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 ModelType hidden field. It is not hardcoded, meaning that you could add other child types later without having to ever touch this model binder.

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

萤火眠眠 2024-12-09 02:25:12

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

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

我可以使用 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);
}

无论如何,这实际上效果更好,因为如果我正在进行任何处理,那么我必须将模型强制转换为它实际上是什么,或者使用 is找出要使用 AutoMapper 调用的正确 Map。

我想我们这些从第一天起就没有使用过 MVC 的人会忘记 UpdateModelTryUpdateModel,但它仍然有它的用处。

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

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

[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 is to to figure out the correct Map to call with AutoMapper.

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

梦回旧景 2024-12-09 02:25:12

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

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

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

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

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

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

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

虽然这种改变感觉很hacky并且非常“系统化”,但它似乎有效 - 并且据我所知,不会造成相当大的安全风险,因为它确实< em>不绑定到CreateModel(),因此不允许允许您发布任何内容并欺骗模型绑定器创建任何对象。

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

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

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

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.

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 only the properties defined in the abstract type, ignoring properties specific to the concrete type of AbstractBaseItemView that happens to be in use.

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 not tie into CreateModel() and thus does not allow you to post whatever and trick the model-binder into creating just any object.

It also works only when the declared property-type is an abstract type, 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 only work 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 edit specific properties of concrete types on existing model objects, but only create new ones.

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.

德意的啸 2024-12-09 02:25:12

使用 Darin 的方法通过视图中的隐藏字段来区分模型类型,我建议您使用自定义的 RouteHandler 来区分模型类型,并将每个模型类型定向到控制器上唯一命名的操作。例如,如果您的控制器中的 Create 操作有两个具体模型 Foo 和 Bar,请创建一个 CreateFoo(Foo model) 操作和一个 CreateBar (条形模型) 操作。然后,制作一个自定义的 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; 
    }
}

然后,在 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())); 
} 

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

Using Darin's method to discriminate your model types via a hidden field in your view, I would recommend that you use a custom RouteHandler to 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 Create action in your controller, make a CreateFoo(Foo model) action and a CreateBar(Bar model) action. Then, make a custom RouteHandler, as follows:

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:

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.

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