2009-03-19 26 views
7

È possibile associare una relazione di chiave esterna sul modello a un input di modulo?ASP.NET Modello MVC vincolante rapporto chiave esterna

Dire che ho una relazione uno-a-molti tra Car e Manufacturer. Voglio avere un modulo per l'aggiornamento Car che include un input di selezione per l'impostazione Manufacturer. Speravo di essere in grado di farlo usando il modello integrato, ma sto iniziando a pensare che dovrò farlo da solo.

mia azione firma del metodo è simile al seguente:

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

La forma messaggi i valori nome, descrizione e produttore, in cui produttore è una chiave primaria di tipo int. Il nome e la descrizione vengono impostati correttamente, ma non il produttore, il che ha senso dal momento che il raccoglitore modello non ha idea di cosa sia il campo PK. Significa che dovrei scrivere una custom IModelBinder che ne sia a conoscenza? Non sono sicuro di come funzionerebbe dal momento che i miei archivi di accesso ai dati vengono caricati attraverso un contenitore IoC su ciascun costruttore Controller.

risposta

6

Ecco la mia opinione: questo è un raccoglitore modello personalizzato che quando viene richiesto a GetPropertyValue, controlla se la proprietà è un oggetto dal mio assieme modello e ha un IRepository <> registrato nel mio NInject IKernel. Se può ottenere l'IRepository da Ninject, lo utilizza per recuperare l'oggetto chiave esterna.

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

} 

potresti ovviamente sostituire Ninject per il tuo contenitore DI e il tuo tipo di repository.

+2

Esempio molto utile! Un'idea che posso suggerire, però, sta usando l'interfaccia 'IModelBinderProvider' per indirizzare questo modello di raccoglitore per i tipi di modello invece di controllare all'interno del raccoglitore. Brad Wilson ha scritto su questo [qui] (http://bradwilson.typepad.com/blog/2010/10/service-location-pt9-model-binders.html). –

+0

sì, sarebbe fantastico. Non ho ancora aggiornato su MVC3, comunque. –

3

Sicuramente ogni automobile ha un solo produttore. In tal caso, è necessario disporre di un campo ManufacturerID in cui è possibile associare il valore della selezione a. Cioè, la tua selezione dovrebbe avere il nome del produttore come testo e l'id come valore. Nel valore di salvataggio, associare ManufacturerID anziché Manufacturer.

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

Con

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

E

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

Se il modello viene creato utilizzando POCO, avere una proprietà 'ManufacturerID' su' Car' non mi sembra giusto.È davvero il modo migliore per risolvere questo tipo di rilegatura del modello? –

+0

Non sono sicuro di cosa intendi. Normalmente, avrei un campo chiave estraneo per mettere in relazione le entità dell'automobile e del produttore. È piuttosto normale che questo sia un campo "ID". Suppongo che potresti non scegliere di esporre questo campo nel tuo modello, ma certamente potresti. Io di solito uso LINQtoSQL e posso assicurarvi che ci sarebbe sia una proprietà ManufacturerID che un'entità Produttore associata sull'entità Car. – tvanfosson

2

forse è una tarda, ma è possibile utilizzare un modello personalizzato legante per raggiungere questo obiettivo. Normalmente lo farei allo stesso modo di @tvanofosson ma avevo un caso in cui aggiungevo UserDetails alle tabelle AspNetMembershipProvider. Dal momento che anch'io uso solo POCO (e mapparlo da EntityFramework) non volevo usare un id perché non era giustificato dal punto di vista del business quindi ho creato un modello solo per aggiungere/registrare utenti. Questo modello ha anche tutte le proprietà per l'utente e una proprietà Role. Volevo associare un nome di testo del ruolo alla sua rappresentazione RoleModel. Questo è fondamentalmente quello che ho fatto:

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

Poi ho dovuto aggiungere quanto segue alla Global.asax:

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

E l'uso della vista:

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

Spero che questo ti aiuta.

+0

L'ho ripubblicato come una domanda http://stackoverflow.com/questions/3642870/codice-modello-modello-per-prodotto-nella-selezione-corrente-valore-corrente. – nfplee

Problemi correlati