2011-03-28 43 views
17

首先,对于大帖子(我试着先做一些研究)以及对同一问题的技术组合(ASP.NET MVC 3,Ninject和MvcContrib)。ASP.NET MVC 3:带继承性/多态性的DefaultModelBinder

我正在开发一个ASP.NET MVC 3项目来处理一些客户端命令。

总之:我有一些继承自对象的抽象类和抽象类Order,我需要在向我的控制器发出POST请求时解析它们。我如何解决正确的类型?我是否需要重写DefaultModelBinder类,或者有其他方法可以做到这一点?有人可以提供我一些代码或其他链接关于如何做到这一点?任何帮助将是伟大的! 如果帖子令人困惑,我可以做任何更改以清楚说明!

所以,我对我的订单需要处理以下继承树:

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 */ } 

这班都是由实体框架生成的,所以我不会改变他们,因为我将需要更新模型(我知道我可以扩展它们)。此外,还会有更多订单,但全部来自Order

我有一个通用视图(Create.aspx)为了创建一个订单,并且这个视图为每个继承的订单(在这种情况下为OrderBottlingOrderFinishing)调用强类型的局部视图。我为OrderController类定义了一个用于GET请求的Create()方法和用于POST请求的其他方法。第二是这样的:

public class OrderController : Controller 
{ 
    /* rest of the implementation ommited */ 

    [HttpPost] 
    public ActionResult Create(Order order) { /* implementation ommited */ } 
} 

现在的问题:当我接收与从形式的数据POST请求,MVC的默认粘合剂尝试实例化Order对象,这是因为该方法的类型确定就是它。但是因为Order是抽象的,所以它不能实例化,这是应该做的。

问题:怎样才能发现哪个具体的Order类型是由视图发送的?

我已经在这里搜索了堆栈溢出并搜索了很多关于这个(我正在处理这个问题大约3天了!),并找到了一些方法来解决一些类似的问题,但我找不到像我真正的问题。为解决此有两个选项:

  • 覆盖ASP.NET MVC DefaultModelBinder,并使用缸内直喷发现哪个类型是Order;
  • 为每个订单创建一个方法(不漂亮,并且会有问题维护)。

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

private class OrdersService : NinjectModule 
{ 
    public override void Load() 
    { 
     Bind<Order>().To<OrderBottling>(); 
     Bind<Order>().To<OrderFinishing>(); 
    } 
} 

我试图让各类throught Ninject的Get<>()方法之一,但它告诉我,该是解决其种类较多,其中一个方法。所以,我明白这个模块并没有很好的实现。我也试图对这两种类型执行:Bind<Order>().To<OrderBottling>().WithPropertyInject("OrderTypeId", 2);,但它有同样的问题...什么才是实现这个模块的正确方法?

我也尝试使用MvcContrib模型绑定。我已经做到了这一点:

[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()); 
} 

但是,这将引发异常:system.missingMethodException而:无法创建抽象类。所以,我认为活页夹不是或不能解析为正确的类型。

许多许多在此先感谢!

编辑:首先,感谢Martin和Jason的回答,并为延误感到抱歉!我尝试了两种方法,并且都奏效了!我将马丁的答案标记为正确,因为它更灵活,并满足我项目的一些需求。具体地,对于每个请求的ID被存储在数据库中,如果我只在一个地方(数据库或在类)更改ID把它们的类可以打破软件。马丁的方法在这一点上非常灵活。

@马丁:我的代码我改了行

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

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

,因为我所在的班的另一个项目(等,在不同的组件)。我分享这个,因为它看起来像比只获取不能解析外部程序集类型的执行程序集更灵活。在我的情况下,所有的订单类都在同一个程序集上。这不是最好,也不是一个神奇的公式,但我觉得有趣的是,分享这个;)

+0

完整的堆栈跟踪总是可以更容易地诊断问题。 – 2011-03-28 14:20:50

+0

@jmpcm - 只要确定,如果从'Order'中删除'abstract'修饰符,它是否工作? – 2011-03-28 14:32:33

+0

@Sergi:不,它也不起作用。我之前创建模型的时候,我没有把Order作为抽象的,结果是一样的,但有一个不同的错误(不记得它是什么)。 – jmpcm 2011-03-28 14:35:27

回答

16

我已经尝试过做类似的事情,我得出的结论是没有什么建将处理这个问题。

我去的选择是创建自己的模型绑定(尽管从默认继承所以它没有太多的代码)。它使用称为xxxConcreteType的类型的名称查找回发值,其中xxx是它绑定到的另一个类型。这意味着必须使用您要绑定的类型的值回传一个字段;在这种情况下OrderConcreteType的值为OrderBottling或OrderFinishing。

你的另一种方法是使用的UpdateModel或TryUpdateModel和你的方法ommit参数。您将需要确定哪一种你叫这(通过一个参数或其他方式)之前要更新模型,并事先实例化类,那么你可以使用这两种方法这样就把它

编辑:

这里是代码..

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") 
+0

嗨马丁!感谢您的想法!你所描述的是我在我做的DefaultModelBinder研究中看到的选项之一。我没有尝试过这种方式,但如果它是最好的,我会试试看!如果你可以在这里放一些代码,我会非常感激! :) – jmpcm 2011-03-28 14:44:17

+0

有没有人能够在更复杂的情况下得到这个工作?我已经成功地利用它来改变“var concreteType = Assembly.GetExecutingAssembly()。GetType(concreteTypeValue.AttemptedValue); ”来搜索所有已加载的程序集,但我需要能够有一个父代<==>子viewmodel在哪里这个孩子是一个视图模型,它的属性被一个抽象类型所改变......似乎并不喜欢这个......任何提示? – 2012-01-18 21:06:09

+0

查看我的下面的附加答案,查看所有加载的程序集的版本。 – 2012-02-21 03:17:41

7

您可以创建一个运行时的是定制模型绑定器你的动作接受某种类型,并且它可以创建一个你想要返回的任何类型的对象。 CreateModel()方法需要一个ControllerContext和ModelBindingContext,使您可以访问路由传递的参数,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()时,你有你的行动订单参数一起使用:

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

您也可以建立一个通用的模型绑定器是适用于所有抽象模型。我的解决方案要求您向名为'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); 
    } 
} 
0

更改行:

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

这是一个天真的实现,它检查每个程序集的类型。我确信有更聪明的方法来做到这一点,但这种方式运作得很好。

+0

Hi @Corey!感谢您的提示,但正如您在编辑时所看到的,我已经完成了这个问题。这是一种更简洁的写作方式。具有此代码的软件以及我为此提出的这个问题的软件近一年的工作完美无缺:)感谢您的关注! – jmpcm 2012-02-22 11:04:40

2

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

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

正如你看到的,你必须添加字段包含了什么从抽象类继承具体类应创建信息(姓名的)。例如类:类抽象内容,类TextContent,内容应该有类型设置为“TextContent”。 记得切换默认模型绑定在Global.asax中:

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

对于以下link更多信息和示例项目检查。