多态模型绑定

在之前的MVC版本中,这个问题已经被问到了。 这个博客也有关于解决这个问题的方法。 我想知道是否MVC3引入了任何可能的帮助,或者如果有任何其他的select。

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

问题出现在后期。 如果我做了后置动作方法以基类作为参数,那么MVC不能创build它的抽象版本(我不想要反正,我想它创build实际的具体types)。 如果我创build了多个仅通过参数签名变化的后操作方法,那么MVC抱怨它是不明确的。

所以据我所知,我有几个关于如何解决这个问题的select。 我不喜欢其中的任何一种,但是我会在这里列出来:

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

我不喜欢1,因为它基本上是隐藏的configuration。 其他一些开发代码的开发人员可能并不知道,并且浪费了大量的时间来弄清楚为什么事情会改变。

我不喜欢2,因为它似乎有点哈克。 但是,我倾向于这种方法。

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

还有其他build议吗?

编辑:

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

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

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

因为我显然select了选项1(:-)),所以让我试着详细说明它,使其不易破碎,并避免将具体实例硬编码到模型联编程序中。 这个想法是将具体types传递到一个隐藏的字段,并使用reflection来实例化具体的types。

假设您有以下视图模型:

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

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

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

我刚刚想到了一个解决这个问题的方法。 而不是像这样使用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 ,但它仍然有其用处。

我花了一个美好的一天,想出了一个密切相关的问题的答案 – 虽然我不确定这是一个问题,但我会在这里发表,以防其他人正在寻找同样的问题的解决scheme。

在我的情况下,我有一个抽象的基本types为许多不同的视图模型types。 所以在主视图模型中,我有一个抽象基types的属性:

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

我有许多AbstractBaseItemView的子types,其中许多定义了它们自己的专有属性。

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

解决这个问题的方法并不完美:

 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(),因此不允许你张贴任何东西,欺骗模型绑定器创build任何对象。

它也适用于声明的属性types是抽象types,例如抽象类或接口。

在一个相关的说明中,我发现我在这里看到的其他实现覆盖CreateModel()可能只会在你发布全新的对象的时候起作用 – 并且会遇到同样的问题,当我声明的属性types是一个抽象types。 因此,您很可能无法编辑 现有模型对象上具体types的特定属性,只能创build新的模型对象。

换句话说,您可能需要将这个解决scheme整合到您的活页夹中,以便也能够正确编辑在绑定之前添加到视图模型中的对象。就我个人而言,我觉得这是一个更安全的方法,因为我控制什么具体types被添加 – 所以控制器/动作可以间接地指定可能绑定的具体types,只需填充一个空的实例属性。

我希望这对别人有帮助

使用Darin的方法通过视图中的隐藏字段来区分模型types,我build议您使用自定义的RouteHandler来区分模型types,并在控制器上指定每个模型的唯一名称。 例如,如果您有两个具体模型,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附加到操作名称,从而允许为每个具体模型定义唯一的操作。