此问题在早期版本的MVC中已asked before。还有this blog entry关于解决问题的方法。我想知道MVC3是否引入了任何可能有用的东西,或者是否还有其他选择。
简而言之。这是情况。我有一个抽象的基本模型和2个具体的子类。我有一个强类型视图,用EditorForModel()
呈现模型。然后我有自定义模板来呈现每种具体类型。
问题来自发布时间。如果我做了后操作方法以基类作为参数,然后MVC不能创建它的虚拟版本(我不想反正,我想它来创建实际的具体类型)。如果我创建多个仅通过参数签名变化的post动作方法,那么MVC会抱怨它不明确。
据我所知,我对如何解决这个问题有几点选择。我出于各种原因不喜欢它们,但我会在这里列出它们:
我不喜欢1,因为它基本上是隐藏的配置。其他一些开发代码的开发人员可能不知道它并浪费了大量时间来弄清楚为什么在更改内容时会出现问题。
我不喜欢2,因为它看起来有点像hacky。但是,我倾向于采用这种方法。
我不喜欢3,因为这意味着违反DRY。
还有其他建议吗?
编辑:
我决定采用达林的方法,但稍作改动。我把它添加到我的抽象模型中:
[HiddenInput(DisplayValue = false)]
public string ConcreteModelType { get { return this.GetType().ToString(); }}
然后在我的DisplayForModel()
中自动生成隐藏。您唯一需要记住的是,如果您不使用DisplayForModel()
,则必须自行添加。
答案 0 :(得分:59)
由于我显然选择了选项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。
答案 1 :(得分:14)
我刚刚想到了解决这个问题的有效方法。而不是像这样使用参数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到使用AutoMapper调用。
我想我们这些从第1天起就没有使用过MVC的人会忘记UpdateModel
和TryUpdateModel
,但它仍有其用途。
答案 2 :(得分:7)
我花了一个美好的一天来回答一个密切相关的问题 - 尽管我不确定这是一个相同的问题,但我在这里发布它以防其他人正在寻找解决方案确切的问题。
在我的例子中,我有一个抽象的基类型,用于许多不同的视图模型类型。所以在主视图模型中,我有一个抽象基类型的属性:
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);
}
// ...
}
虽然这种变化感觉很糟糕并且非常“系统性”,但它似乎有效 - 而且,据我所知,并没有造成相当大的安全风险,因为它确实不领带进入CreateModel(),因此不允许你发布任何内容并欺骗模型绑定器来创建任何对象。
仅当声明的属性类型是抽象类型时才有效,例如抽象类或接口。
在一个相关的说明中,我发现我在这里看到的其他实现覆盖CreateModel()可能只会 在你发布全新对象时工作 - 并且会受到影响当声明的属性类型是抽象类型时,我遇到了同样的问题。因此,您很可能无法在现有模型对象上编辑具体属性,但只能创建新的。
换句话说,您可能需要将此解决方法集成到您的活页夹中,以便能够正确编辑在绑定之前添加到视图模型的对象...个人而言,我觉得这样更安全方法,因为我控制添加的具体类型 - 所以控制器/动作可以通过简单地用空实例填充属性来间接指定可能绑定的具体类型。
我希望这对其他人有帮助......
答案 3 :(得分:4)
使用Darin的方法通过视图中的隐藏字段区分您的模型类型,我建议您使用自定义RouteHandler
来区分模型类型,并将每个类型指向控制器上唯一命名的操作。例如,如果您的控制器中的Create
操作有两个具体模型Foo和Bar,请执行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附加到操作名称,从而允许为每个具体模型定义唯一操作。