2009-03-19 16 views
7

¿Es posible enlazar una relación de clave externa en mi modelo a una entrada de formulario?Relación de clave externa de vinculación de modelo ASP.NET MVC

Supongamos que tengo una relación uno a muchos entre Car y Manufacturer. Deseo tener un formulario para actualizar Car que incluye una entrada de selección para configurar Manufacturer. Esperaba poder hacer esto usando el enlace de modelo incorporado, pero estoy empezando a pensar que tendré que hacerlo yo mismo.

Mi acción método de firma se ve así:

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

Las formulario vuelve a los valores de nombre, descripción y del fabricante, donde Fabricante es una clave principal de tipo int. El nombre y la descripción se configuran correctamente, pero no el fabricante, lo cual tiene sentido ya que el archivador modelo no tiene idea de cuál es el campo PK. ¿Eso significa que tendría que escribir un IModelBinder personalizado que lo tenga en cuenta? No estoy seguro de cómo funcionaría, ya que mis repositorios de acceso a datos se cargan a través de un contenedor IoC en cada constructor Controller.

Respuesta

6

Aquí está mi opinión: esta es una carpeta de modelo personalizado que cuando se solicita GetPropertyValue, mira si la propiedad es un objeto del ensamblaje de mi modelo y tiene un IRepository <> registrado en mi NInject IKernel. Si puede obtener el IRepository desde Ninject, lo utiliza para recuperar el objeto de clave externa.

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

} 

obviamente puede sustituir Ninject por su contenedor DI y su propio tipo de repositorio.

+2

¡Un ejemplo muy útil! Sin embargo, una idea que puedo sugerir es usar la interfaz 'IModelBinderProvider' para apuntar a este modelo de carpetas para los tipos de modelo en lugar de verificar dentro del cuaderno. Brad Wilson escribió sobre esto [aquí] (http://bradwilson.typepad.com/blog/2010/10/service-location-pt9-model-binders.html). –

+0

sí, sería genial. No he actualizado a MVC3 todavía, sin embargo. –

3

Seguramente, cada coche solo tiene un fabricante. Si ese es el caso, entonces debe tener un campo ManufacturerID al que pueda vincular el valor de la selección. Es decir, su selección debe tener el nombre del fabricante como texto y el id como valor. En su valor de ahorro, vincule ManufacturerID en lugar de Manufacturer.

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

Con

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

Y

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

Si el modelo se construye usando POCOs, tener una propiedad 'ManufacturerID' en' Car' no me parece correcto.¿Es esta la mejor forma de resolver este tipo de encuadernación? –

+0

No estoy seguro de lo que quieres decir. Normalmente, tendría un campo de clave externa para relacionar las entidades del fabricante y del automóvil. Es bastante estándar tener esto como un campo de "ID". Supongo que es posible que no elija exponer este campo en su modelo, pero ciertamente podría. Normalmente utilizo LINQtoSQL y puedo asegurarle que habría una propiedad de ManufacturerID y una entidad Manufacturer asociada en la entidad Car. – tvanfosson

2

Tal vez sea una tarde, pero se puede utilizar un aglutinante de modelo personalizado para lograrlo. Normalmente lo haría de la misma manera que @tvanofosson, pero tuve un caso en el que estaba agregando UserDetails a las tablas AspNetMembershipProvider. Como también utilizo POCO (y lo asocie desde EntityFramework), no quería usar un id porque no estaba justificado desde el punto de vista del negocio, así que creé un modelo solo para agregar/registrar usuarios. Este modelo tenía todas las propiedades para el usuario y también una propiedad de rol. Quería vincular un nombre de texto del rol a su representación de RoleModel. Eso es básicamente lo que hice:

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

Entonces he tenido que añadir lo siguiente a la Global.asax:

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

Y el uso de la vista:

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

espero que este le ayuda.

+0

He vuelto a publicar esto como una pregunta http://stackoverflow.com/questions/3642870/custom-model-binder-for-dropdownlist-not-selecting-correct-value. – nfplee

Cuestiones relacionadas