ASP.NET MVC 3:具有inheritance/多态的DefaultModelBinder

首先,对于这个大post(我先尝试做一些研究)和同一问题的混合技术(ASP.NET MVC 3,Ninject和MvcContrib)感到抱歉。

我正在使用ASP.NET MVC 3开发一个项目来处理一些客户端订单。

简而言之:我有一些inheritance自的对象和抽象类Order ,当我向控制器发出POST请求时,我需要解析它们。 我该如何解决正确的类型? 我是否需要覆盖DefaultModelBinder类,还是有其他方法可以执行此操作? 有人可以提供一些代码或其他链接,如何做到这一点? 任何帮助都会很棒! 如果post令人困惑,我可以做任何改变,以明确!

所以,对于我需要处理的订单,我有以下inheritance树:

 public abstract partial class Order { public Int32 OrderTypeId {get; set; } /* rest of the implementation ommited */ } public class OrderBottling : Order { /* implementation ommited */ } public class OrderFinishing : Order { /* implementation ommited */ } 

这些类都是由Entity Framework生成的,所以我不会修改它们,因为我需要更新模型(我知道我可以扩展它们)。 此外,还会有更多订单,但都来自Order

我有一个通用视图( Create.aspx )来创建一个订单,这个视图调用每个inheritance订单的强类型部分视图(在本例中为OrderBottlingOrderFinishing )。 我在OrderController类上为GET请求和其他POST请求定义了一个Create()方法。 第二个如下:

 public class OrderController : Controller { /* rest of the implementation ommited */ [HttpPost] public ActionResult Create(Order order) { /* implementation ommited */ } } 

现在问题是:当我收到带有表单数据的POST请求时,MVC的默认绑定器尝试实例化一个Order对象,这是正常的,因为方法的类型就是这样。 但是因为Order是抽象的,所以它无法实例化,这应该是应该做的。

问题:如何查看视图发送的具体Order类型?

我已经在这里搜索了Stack Overflow并搜索了很多关于这个问题(我现在正在研究这个问题大约3天了!)并找到了一些方法来解决一些类似的问题,但我找不到像我这样的问题问题。 解决这个问题的两个选择:

  • 覆盖ASP.NET MVC DefaultModelBinder并使用Direct Injection来发现Order是哪种类型;
  • 为每个订单创建一个方法(不是很漂亮,维护起来会有问题)。

我没有尝试过第二个选项,因为我认为这不是解决问题的正确方法。 对于第一个选项,我尝试了Ninject来解析订单的类型并实例化它。 我的Ninject模块如下所示:

 private class OrdersService : NinjectModule { public override void Load() { Bind().To(); Bind().To(); } } 

我已经尝试通过Ninject的Get()方法获得其中一种类型,但它告诉我这是解决类型的一种方法。 所以,我理解该模块没有很好地实现。 我也尝试过这两种类型的实现: Bind().To().WithPropertyInject("OrderTypeId", 2); ,但它有同样的问题……实现这个模块的正确方法是什么?

我也尝试过使用MvcContrib Model Binder。 我做到了这个:

 [DerivedTypeBinderAware(typeof(OrderBottling))] [DerivedTypeBinderAware(typeof(OrderFinishing))] public abstract partial class Order { } 

Global.asax.cs我做到了:

 protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(Order), new DerivedTypeModelBinder()); } 

但是这会引发exception: System.MissingMethodException:无法创建抽象类 。 所以,我认为活页夹没有或无法解析为正确的类型。

非常感谢提前!

编辑:首先,谢谢Martin和Jason的回答,对不起延迟! 我尝试了两种方法,两者都有效! 我认为Martin的答案是正确的,因为它更灵活,满足了我项目的一些需求。 具体来说,每个请求的ID都存储在数据库中,如果我只在一个地方(数据库或类)更改ID,则将它们放在类上会破坏软件。 马丁的方法在这一点上非常灵活。

@Martin:在我的代码上我更改了这一行

 var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); 

 var concreteType = Assembly.GetAssembly(typeof(Order)).GetType(concreteTypeValue.AttemptedValue); 

因为我的课程在另一个项目上(在不同的程序集上)。 我正在分享这个,因为它似乎比只获得无法解析外部程序集类型的执行程序集更灵活。 在我的例子中,所有订单类都在同一个程序集中。 这不是更好,也不是一个神奇的公式,但我觉得分享这个很有意思;)

我之前尝试过类似的事情,我得出的结论是,没有任何内置可以解决这个问题。

我选择的选项是创建我自己的模型绑定器(虽然从默认inheritance,所以它没有太多的代码)。 它查找了一个post back值,其名称为xxxConcreteType,其中xxx是它绑定的另一种类型。 这意味着必须使用您尝试绑定的类型的值回发字段; 在这种情况下,OrderConcreteType的值为OrderBottling或OrderFinishing。

您的另一种选择是使用UpdateModel或TryUpdateModel并从您的方法中省略参数。 您需要在调用之前确定要更新的模型类型(通过参数或其他方式)并事先实例化该类,然后您可以使用任一方法来填充它

编辑:

这是代码..

 public class AbstractBindAttribute : CustomModelBinderAttribute { public string ConcreteTypeParameter { get; set; } public override IModelBinder GetBinder() { return new AbstractModelBinder(ConcreteTypeParameter); } private class AbstractModelBinder : DefaultModelBinder { private readonly string concreteTypeParameterName; public AbstractModelBinder(string concreteTypeParameterName) { this.concreteTypeParameterName = concreteTypeParameterName; } protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { var concreteTypeValue = bindingContext.ValueProvider.GetValue(concreteTypeParameterName); if (concreteTypeValue == null) throw new Exception("Concrete type value not specified for abstract class binding"); var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); if (concreteType == null) throw new Exception("Cannot create abstract model"); if (!concreteType.IsSubclassOf(modelType)) throw new Exception("Incorrect model type specified"); var concreteInstance = Activator.CreateInstance(concreteType); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, concreteType); return concreteInstance; } } } 

将您的操作方法更改为如下所示:

 public ActionResult Create([AbstractBind(ConcreteTypeParameter = "orderType")] Order order) { /* implementation ommited */ } 

您需要在视图中添加以下内容:

 @Html.Hidden("orderType, "Namespace.xxx.OrderBottling") 

您可以创建一个在您的操作接受某种类型时操作的CustomerBinder,它可以创建您想要返回的任何类型的对象。 CreateModel()方法采用ControllerContext和ModelBindingContext,使您可以访问route,url querystring和post传递的参数,您可以使用这些参数用值填充对象。 默认模型绑定器实现转换同名属性的值,以将它们放在对象的字段中。

我在这里做的只是检查其中一个值以确定要创建的类型,然后调用DefaultModelBinder.CreateModel()方法将其创建的类型切换为适当的类型。

 public class OrderModelBinder : DefaultModelBinder { protected override object CreateModel( ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { // get the parameter OrderTypeId ValueProviderResult result; result = bindingContext.ValueProvider.GetValue("OrderTypeId"); if (result == null) return null; // OrderTypeId must be specified // I'm assuming 1 for Bottling, 2 for Finishing if (result.AttemptedValue.Equals("1")) return base.CreateModel(controllerContext, bindingContext, typeof(OrderBottling)); else if (result.AttemptedValue.Equals("2")) return base.CreateModel(controllerContext, bindingContext, typeof(OrderFinishing)); return null; // unknown OrderTypeId } } 

通过将其添加到Global.asax.cs中的Application_Start(),将其设置为在操作上具有Order参数时使用:

 ModelBinders.Binders.Add(typeof(Order), new OrderModelBinder()); 

您还可以构建适用于所有抽象模型的通用ModelBinder。 我的解决方案要求您在视图中添加一个名为“ModelTypeName”的隐藏字段,并将值设置为您想要的具体类型的名称。 但是,应该可以使这个更聪明,并通过将类型属性与视图中的字段匹配来选择具体类型。

在Global.asax.cs Application_Start()中:

 ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); 

CustomModelBinder:

 public class CustomModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { if (modelType.IsAbstract) { var modelTypeValue = controllerContext.Controller.ValueProvider.GetValue("ModelTypeName"); if (modelTypeValue == null) throw new Exception("View does not contain ModelTypeName"); var modelTypeName = modelTypeValue.AttemptedValue; var type = modelType.Assembly.GetTypes().SingleOrDefault(x => x.IsSubclassOf(modelType) && x.Name == modelTypeName); if(type == null) throw new Exception("Invalid ModelTypeName"); var concreteInstance = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => concreteInstance, type); return concreteInstance; } return base.CreateModel(controllerContext, bindingContext, modelType); } } 

我对该问题的解决方案支持可以包含其他抽象类,多inheritance,集合或generics类的复杂模型。

 public class EnhancedModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { Type type = modelType; if (modelType.IsGenericType) { Type genericTypeDefinition = modelType.GetGenericTypeDefinition(); if (genericTypeDefinition == typeof(IDictionary<,>)) { type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments()); } else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) || (genericTypeDefinition == typeof(IList<>))) { type = typeof(List<>).MakeGenericType(modelType.GetGenericArguments()); } return Activator.CreateInstance(type); } else if(modelType.IsAbstract) { string concreteTypeName = bindingContext.ModelName + ".Type"; var concreteTypeResult = bindingContext.ValueProvider.GetValue(concreteTypeName); if (concreteTypeResult == null) throw new Exception("Concrete type for abstract class not specified"); type = Assembly.GetExecutingAssembly().GetTypes().SingleOrDefault(t => t.IsSubclassOf(modelType) && t.Name == concreteTypeResult.AttemptedValue); if (type == null) throw new Exception(String.Format("Concrete model type {0} not found", concreteTypeResult.AttemptedValue)); var instance = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, type); return instance; } else { return Activator.CreateInstance(modelType); } } } 

如您所见,您必须添加字段(名称为Type ),其中包含应创建从抽象类inheritance的具体类的信息。 例如类: 类抽象内容类TextContent ,内容应将Type设置为“TextContent”。 请记住在global.asax中切换默认模型绑定器:

 protected void Application_Start() { ModelBinders.Binders.DefaultBinder = new EnhancedModelBinder(); [...] 

有关更多信息和示例项目检查以下链接 。

换行:

 var concreteType = Assembly.GetExecutingAssembly().GetType(concreteTypeValue.AttemptedValue); 

对此:

  Type concreteType = null; var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in loadedAssemblies) { concreteType = assembly.GetType(concreteTypeValue.AttemptedValue); if (null != concreteType) { break; } } 

这是一个天真的实现,它检查每个类型的程序集。 我确信有更聪明的方法可以做到,但这种方法效果很好。