【问题标题】:Polymorphic model binding多态模型绑定
【发布时间】:2011-08-28 17:18:20
【问题描述】:

这个问题在早期版本的 MVC 中是asked before。还有this blog entry 关于解决问题的方法。我想知道 MVC3 是否引入了任何可能有帮助的东西,或者是否还有其他选择。

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

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

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

  1. 按照 Darin 在我链接到的第一篇文章中的建议创建自定义模型绑定器。
  2. 按照我链接到的第二篇文章的建议创建一个鉴别器属性。
  3. 根据类型发布到不同的操作方法
  4. ???

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

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

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

还有其他建议吗?

编辑:

我决定采用达林的方法,但稍作改变。我将此添加到我的抽象模型中:

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

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

【问题讨论】:

    标签: asp.net-mvc asp.net-mvc-3 polymorphism model-binding


    【解决方案1】:

    由于我显然选择了选项 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 隐藏字段的值推断出来的。它不是硬编码的,这意味着您可以稍后添加其他子类型,而无需接触此模型绑定器。

    同样的技术可以easily be applied 用于基本视图模型的集合。

    【讨论】:

    • 嗯.. 这肯定会提高可维护性。它可以在您的具体模型与属性签名没有区别的情况下工作(Attribute 方法中的一个缺陷)。不过,我不确定我是否喜欢用这些歧视者来污染我的观点。
    • 我选择了您的解决方案,但做了一些小改动。查看我的编辑。
    • 我也想出了一个不同的解决方案,(见我的回答)结果证明它更简单,而且更安全,因为它不涉及向客户端公开他的数据类型,并希望它没有被黑客搞砸。
    • Darin 的方法对我来说效果很好,但是我无法访问与 bindingContext 不在同一命名空间中的类型。它给了我一个错误,指出它在程序集中找不到我的类型。关于该案例的解决方法有什么想法吗?
    • 我添加了一项检查以确保传入的类型继承自预期的基类。如果没有这个,攻击者可能会导致您实例化任意类,这可能很危险。
    【解决方案2】:

    我刚刚想到了一个有趣的解决方案来解决这个问题。而不是像这样使用 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 来找出正确的 Map 来调用自动映射器。

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

    【讨论】:

    • 也许我做错了什么,但如果我只做TryUpdateModel(model) 它只会更新MyModel 的属性等,如果我想让它完全更新模型我必须做@987654328 @,但无论哪种方式,这都是一个很棒的技巧。
    【解决方案3】:

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

    在我的例子中,我有一个用于许多不同视图模型类型的抽象基类型。所以在主视图模型中,我有一个抽象基类型的属性:

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

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

    我的问题是,模型绑定器不查看附加到 View.ItemView 的对象类型,而是只查看声明的属性类型,即 AbstractBaseItemView - 并决定仅绑定 em> 抽象类型中定义的属性,忽略碰巧正在使用的 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);
        }
        
        // ...
    }
    

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

    它也仅在声明的属性类型是 abstract 类型时才有效,例如抽象类或接口。

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

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

    【讨论】:

    • 这对我很有效。我工作了几天才找到解决方案。正如您所描述的那样,我能够创建新对象,但是具体类型的属性永远不会被活页夹填充。现在他们是。
    【解决方案4】:

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

    然后,在 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 附加到操作名称中,从而允许为每个具体模型定义唯一的操作。

    【讨论】:

    • 这些解决方案都依赖于隐藏字段。但这有两个问题。 1) 开发人员必须记住添加隐藏字段,以及 2) 有人可能会创建具有相同名称的模型参数并意外引发奇怪的路由问题。通过使这个全局化,它既方便又方便,但很容易打破和/或绊倒。属性方法的优点是只影响那个动作方法。
    猜你喜欢
    • 2021-02-05
    • 1970-01-01
    • 2013-06-21
    • 1970-01-01
    • 2021-05-11
    • 2015-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多