2009-03-19 19 views
7

Ist es möglich, eine Fremdschlüsselbeziehung in meinem Modell an eine Formulareingabe zu binden?Asp.net-MVC-Modell-Bindungs-Fremdschlüssel-Beziehung

Angenommen, ich habe eine Eins-zu-viele-Beziehung zwischen Car und Manufacturer. Ich möchte ein Formular für die Aktualisierung Car haben, das einen Auswahleingang für die Einstellung Manufacturer enthält. Ich hatte gehofft, dass ich das mit der eingebauten Modellbindung machen kann, aber ich fange an zu denken, dass ich es selbst machen muss.

Meine Aktion Methode Signatur sieht wie folgt aus:

public JsonResult Save(int id, [Bind(Include="Name, Description, Manufacturer")]Car car) 

Die Form bucht die Werte Name, Beschreibung und Hersteller, wo Hersteller int ein Primärschlüssel-Typ ist. Name und Beschreibung werden richtig gesetzt, aber nicht Hersteller, was sinnvoll ist, da der Modellbinder keine Ahnung hat, was das PK-Feld ist. Heißt das, ich müsste eine benutzerdefinierte schreiben, dass es davon bewusst ist? Ich bin nicht sicher, wie das funktionieren würde, da meine Datenzugriffsrepositorys durch einen IoC-Container auf jedem Controller-Konstruktor geladen werden.

Antwort

6

Hier ist mein Take - das ist ein benutzerdefiniertes Modellbinder, der auf GetPropertyValue überprüft, ob die Eigenschaft ein Objekt aus meiner Modellassembly ist und ein IRepository <> in meinem NInject IKernel registriert ist. Wenn es das IRepository von Ninject abrufen kann, verwendet es das, um das Fremdschlüsselobjekt abzurufen.

public class ForeignKeyModelBinder : System.Web.Mvc.DefaultModelBinder 
{ 
    private IKernel serviceLocator; 

    public ForeignKeyModelBinder(IKernel serviceLocator) 
    { 
     Check.Require(serviceLocator, "IKernel is required"); 
     this.serviceLocator = serviceLocator; 
    } 

    /// <summary> 
    /// if the property type being asked for has a IRepository registered in the service locator, 
    /// use that to retrieve the instance. if not, use the default behavior. 
    /// </summary> 
    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, 
     PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) 
    { 
     var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); 
     if (submittedValue == null) 
     { 
      string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, "Id"); 
      submittedValue = bindingContext.ValueProvider.GetValue(fullPropertyKey); 
     } 

     if (submittedValue != null) 
     { 
      var value = TryGetFromRepository(submittedValue.AttemptedValue, propertyDescriptor.PropertyType); 

      if (value != null) 
       return value; 
     } 

     return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); 
    } 

    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) 
    { 
     string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, "Id"); 
     var submittedValue = bindingContext.ValueProvider.GetValue(fullPropertyKey); 
     if (submittedValue != null) 
     { 
      var value = TryGetFromRepository(submittedValue.AttemptedValue, modelType); 

      if (value != null) 
       return value; 
     } 

     return base.CreateModel(controllerContext, bindingContext, modelType); 
    } 

    private object TryGetFromRepository(string key, Type propertyType) 
    { 
     if (CheckRepository(propertyType) && !string.IsNullOrEmpty(key)) 
     { 
      Type genericRepositoryType = typeof(IRepository<>); 
      Type specificRepositoryType = genericRepositoryType.MakeGenericType(propertyType); 

      var repository = serviceLocator.TryGet(specificRepositoryType); 
      int id = 0; 
#if DEBUG 
      Check.Require(repository, "{0} is not available for use in binding".FormatWith(specificRepositoryType.FullName)); 
#endif 
      if (repository != null && Int32.TryParse(key, out id)) 
      { 
       return repository.InvokeMethod("GetById", id); 
      } 
     } 

     return null; 
    } 

    /// <summary> 
    /// perform simple check to see if we should even bother looking for a repository 
    /// </summary> 
    private bool CheckRepository(Type propertyType) 
    { 
     return propertyType.HasInterface<IModelObject>(); 
    } 

} 

Sie könnten offensichtlich Ninject für Ihren DI-Container und Ihren eigenen Repository-Typ ersetzen.

+2

Sehr hilfreiches Beispiel! Eine Idee, die ich vorschlagen kann, ist jedoch die Verwendung der IModelBinderProvider-Schnittstelle, um dieses Modellbinder für Ihre Modelltypen anzusprechen, anstatt innerhalb des Binders zu überprüfen. Brad Wilson schrieb darüber [http://bradwilson.typepad.com/blog/2010/10/service-location-pt9-model-binders.html]. –

+0

Ja, das wäre großartig. Ich habe jedoch noch nicht auf MVC3 aktualisiert. –

3

Sicherlich hat jedes Auto nur einen Hersteller. Wenn dies der Fall ist, sollten Sie ein ManufacturerID-Feld haben, an das Sie den Wert des Select binden können. Das heißt, Ihre Auswahl sollte den Herstellernamen als Text und die ID als Wert haben. In Ihrem Sicherungswert binden Sie ManufacturerID und nicht Manufacturer.

<%= Html.DropDownList("ManufacturerID", 
     (IEnumerable<SelectListItem>)ViewData["Manufacturers"]) %> 

Mit

ViewData["Manufacturers"] = db.Manufacturers 
           .Select(m => new SelectListItem 
              { 
               Text = m.Name, 
               Value = m.ManufacturerID 
              }) 
           .ToList(); 

Und

public JsonResult Save(int id, 
         [Bind(Include="Name, Description, ManufacturerID")]Car car) 
+1

Wenn das Modell mit POCOs erstellt wird, scheint mir eine Eigenschaft 'ManufacturerID' auf dem' Car' nicht richtig zu sein.Ist das wirklich der bevorzugte Weg, um diese Art von Modellbindung zu lösen? –

+0

Ich bin mir nicht sicher, was du meinst. Normalerweise würde ich ein Fremdschlüsselfeld haben, um die Automobil- und Herstellereinheiten in Beziehung zu setzen. Es ist ziemlich normal, dass dies ein "ID" -Feld ist. Ich nehme an, dass Sie dieses Feld in Ihrem Modell möglicherweise nicht verfügbar machen, aber Sie könnten es sicherlich tun. Normalerweise verwende ich LINQtoSQL, und ich kann Ihnen versichern, dass es auf der Car-Entität sowohl eine ManufacturerID-Eigenschaft als auch eine zugehörige Manufacturer-Entität geben würde. – tvanfosson

2

Vielleicht ist es ein spät, aber Sie können ein benutzerdefiniertes Modell Bindemittel verwenden, um dies zu erreichen. Normalerweise würde ich es wie @tvanofosson tun, aber ich hatte einen Fall, in dem ich den AspNetMembershipProvider-Tabellen UserDetails hinzufügte. Da ich auch nur POCO verwende (und es von EntityFramework abbilde), wollte ich keine ID verwenden, da dies aus betriebswirtschaftlicher Sicht nicht gerechtfertigt war. Deshalb habe ich ein Modell nur zum Hinzufügen/Registrieren von Benutzern erstellt. Dieses Modell hatte alle Eigenschaften für den Benutzer und eine Role-Eigenschaft. Ich wollte einen Textnamen der Rolle an die RoleModel-Repräsentation binden. Das ist im Grunde, was ich tat:

public class RoleModelBinder : DefaultModelBinder 
{ 
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 
    { 
     string roleName = controllerContext.HttpContext.Request["Role"]; 

     var model = new RoleModel 
          { 
           RoleName = roleName 
          }; 

     return model; 
    } 
} 

Dann hatte ich folgende zum Global.asax hinzuzufügen:

ModelBinders.Binders.Add(typeof(RoleModel), new RoleModelBinder()); 

und die Verwendung in der Ansicht:

<%= Html.DropDownListFor(model => model.Role, new SelectList(Model.Roles, "RoleName", "RoleName", Model.Role))%> 

Ich hoffe, das hilft dir.

+0

Ich habe dies als eine Frage http://stackoverflow.com/questions/3642870/custom-model-binder-for-dropdownlist-not-selecting-correct-value reposted. – nfplee