多态模型绑定
在之前的MVC版本中,这个问题已经被问到了。 这个博客也有关于解决这个问题的方法。 我想知道是否MVC3引入了任何可能的帮助,或者如果有任何其他的select。
简而言之。 这是情况。 我有一个抽象的基础模型,和2个具体的子类。 我有一个用EditorForModel()
渲染模型的强types视图。 然后我有自定义模板来呈现每个具体types。
问题出现在后期。 如果我做了后置动作方法以基类作为参数,那么MVC不能创build它的抽象版本(我不想要反正,我想它创build实际的具体types)。 如果我创build了多个仅通过参数签名变化的后操作方法,那么MVC抱怨它是不明确的。
所以据我所知,我有几个关于如何解决这个问题的select。 我不喜欢其中的任何一种,但是我会在这里列出来:
- 创build一个自定义的模型绑定器,如Darin在我链接的第一篇文章中所build议的。
- 创build一个鉴别属性作为我build议链接的第二篇文章。
- 根据types发布到不同的操作方法
- ???
我不喜欢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的人忘记了UpdateModel
和TryUpdateModel
,但它仍然有其用处。
我花了一个美好的一天,想出了一个密切相关的问题的答案 – 虽然我不确定这是一个问题,但我会在这里发表,以防其他人正在寻找同样的问题的解决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附加到操作名称,从而允许为每个具体模型定义唯一的操作。