2011-03-28 96 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更多信息和示例項目檢查。