2009-03-19 6 views
7

Est-il possible de lier une relation de clé étrangère sur mon modèle à une entrée de formulaire? Dire que j'ai une relation un-à-plusieurs entre Car et Manufacturer. Je veux avoir un formulaire pour mettre à jour Car qui comprend une entrée de sélection pour le réglage Manufacturer. J'espérais pouvoir le faire en utilisant le modèle intégré, mais je commence à penser que je vais devoir le faire moi-même.Relation de clé étrangère de liaison de modèle ASP.NET MVC

Ma signature de la méthode d'action ressemble à ceci:

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

Le formulaire messages les valeurs Nom, Description et fabricant, lorsque le fabricant est une clé primaire de type int. Le nom et la description sont définis correctement, mais pas par le fabricant, ce qui est logique puisque le classeur modèle n'a aucune idée de ce qu'est le champ PK. Cela signifie-t-il que je devrais écrire un IModelBinder personnalisé pour qu'il en soit conscient? Je ne sais pas comment cela fonctionnerait puisque mes dépôts d'accès aux données sont chargés via un conteneur IoC sur chaque constructeur Controller.

Répondre

6

Voici ma prise - c'est un classeur de modèle personnalisé qui, lorsqu'il est demandé à GetPropertyValue, cherche à voir si la propriété est un objet de mon assembly model, et possède un IRepository <> enregistré dans mon NInject IKernel. S'il peut obtenir l'IRepository à partir de Ninject, il l'utilise pour récupérer l'objet clé étrangère.

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

} 

Vous pouvez évidemment substituer Ninject pour votre conteneur DI et votre propre type de référentiel.

+2

Exemple très utile! Une idée que je peux suggérer, cependant, utilise l'interface 'IModelBinderProvider' pour cibler ce classeur modèle pour vos types de modèles au lieu de vérifier à l'intérieur du classeur. Brad Wilson a écrit à ce sujet [ici] (http://bradwilson.typepad.com/blog/2010/10/service-location-pt9-model-binders.html). –

+0

oui, ce serait génial. Je n'ai pas encore mis à jour pour MVC3, cependant. –

3

Bien sûr, chaque voiture n'a qu'un seul fabricant. Si c'est le cas, vous devez avoir un champ ManufacturerID que vous pouvez lier à la valeur de select. C'est-à-dire que votre sélection doit avoir le nom du fabricant comme texte et l'identifiant comme valeur. Dans votre valeur d'enregistrement, associez ManufacturerID au fabricant.

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

Avec

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

Et

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

Si le modèle est construit en utilisant POCOs, avoir une propriété 'ManufacturerID' sur le' Car' ne me semble pas juste.Est-ce vraiment la façon préférée de résoudre ce genre de liaison de modèle? –

+0

Je ne suis pas sûr de ce que vous voulez dire. Normalement, j'aurais un champ de clé étrangère pour relier la voiture et les entités du fabricant. C'est plutôt standard d'avoir un champ "ID". Je suppose que vous pourriez ne pas choisir d'exposer ce champ dans votre modèle, mais vous pourriez certainement le faire. J'utilise généralement LINQtoSQL et je peux vous assurer qu'il y aura à la fois une propriété ManufacturerID et une entité Manufacturer associée sur l'entité Car. – tvanfosson

2

Peut-être est une fin, mais vous pouvez utiliser un liant modèle personnalisé pour y parvenir. Normalement, je le ferais de la même manière que @tvanofosson mais j'avais un cas où j'ajoutais UserDetails aux tables AspNetMembershipProvider. Comme je n'utilise que POCO (et le mappez à partir de EntityFramework), je n'ai pas voulu utiliser un identifiant car il n'était pas justifié du point de vue commercial, donc j'ai créé un modèle uniquement pour ajouter/enregistrer des utilisateurs. Ce modèle possédait toutes les propriétés de l'utilisateur et une propriété Role également. Je voulais lier un nom de texte du rôle à sa représentation RoleModel. C'est essentiellement ce que je faisais:

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

Alors je dû ajouter ce qui suit à la Global.asax:

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

Et l'utilisation de la vue:

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

J'espère que cette t'aide.

+0

J'ai renvoyé ceci comme question http://stackoverflow.com/questions/3642870/custom-model-binder-for-dropdownlist-not-selecting-correct-value. – nfplee

Questions connexes