2009-05-04 10 views
5

Estoy tratando de usar el DataAnnotationsModelBinder para utilizar las anotaciones de datos para la validación del lado del servidor en ASP.NET MVC.¿Cómo funciona DataAnnotationsModelBinder con ViewModels personalizados?

Todo funciona bien siempre y cuando mi modelo de vista es sólo una clase simple con propiedades inmediatas, tales como

public class Foo 
{ 
    public int Bar {get;set;} 
} 

Sin embargo, el DataAnnotationsModelBinder provoca una NullReferenceException al intentar utilizar un complejo ViewModel, como

public class Foo 
{ 
    public class Baz 
    { 
     public int Bar {get;set;} 
    } 

    public Baz MyBazProperty {get;set;} 
} 

Este es un gran problema para las vistas que rinden más de una entidad LINQ porque realmente prefiero usar ViewModel personalizados que incluyen varias entidades LINQ en lugar de matrices ViewData sin tipo .

El DefaultModelBinder no tiene este problema, por lo que parece un error en DataAnnotationsModelBinder. ¿Hay alguna solución a esto?

Editar: Una posible solución es, por supuesto, para exponer las propiedades del objeto niño en la clase ViewModel como esto:

public class Foo 
{ 
    private Baz myBazInstance; 

    [Required] 
    public string ExposedBar 
    { 
     get { return MyBaz.Bar; } 
     set { MyBaz.Bar = value; } 
    } 

    public Baz MyBaz 
    { 
     get { return myBazInstance ?? (myBazInstance = new Baz()); } 
     set { myBazInstance = value; } 
    } 

    #region Nested type: Baz 

    public class Baz 
    { 
     [Required] 
     public string Bar { get; set; } 
    } 

    #endregion 
} 

#endregion 

pero preferiría no tener que escribir todo el código adicional. El DefaultModelBinder funciona bien con tales jerarquías, por lo que supongo que el DataAnnotationsModelBinder también debería funcionar.

Segunda edición: Parece que esto es de hecho un error en DataAnnotationsModelBinder. Sin embargo, existe la esperanza de que esto se solucione antes de que venga la próxima versión del marco de ASP.NET MVC. Ver this forum thread para más detalles.

+0

tengo un modelo similar pero generalmente edito objetos individuales a la vez. por ejemplo: tengo un objeto de anuncio que puede o no tener una serie de archivos adjuntos (imágenes, archivos PDF), pero edito el anuncio por sí mismo y no obligo al validador a descender a los objetos secundarios del anuncio. Luego editaré los objetos secundarios por separado - misma vista, pero diferente acción POST. Ahora estoy interesado. ¿Cómo funcionan tus acciones para validar enormes árboles de objetos? y ¿cómo funciona la interfaz de usuario para eso? –

+0

En cuanto a las vistas: He descrito cómo hacerlo en http://devermind.com/linq/aspnet-mvc-using-custom-viewmodels-with-post-action-methods En cuanto a la validación: Hasta ahora he estado utilizando la validación del lado del servidor de la misma manera que se describe en el tutorial de Scott Gu: http://weblogs.asp.net/scottgu/archive/2009/03/10/free-asp-net-mvc-ebook-tutorial.aspx. Mi controlador luego recoge los errores de validación de diferentes entidades como esta: ModelState.AddRuleViolations (model.User.GetRuleViolations(), "User"); ModelState.AddRuleViolations (model.Company.GetRuleViolations(), "Company"); –

+0

Estoy seguro de que es un error en algún lugar de mi código de modelo de encuadernación, pero no he tenido tiempo de rastrearlo. Los entregables más urgentes han estado en el camino. :( –

Respuesta

8

Me enfrenté exactamente al mismo problema hoy. Al igual que usted, no ato mi Vista directamente a mi Modelo, sino que utilizo una clase intermedia ViewDataModel que contiene una instancia del Modelo y cualquier parámetro/configuración que me gustaría enviar a la vista.

Terminé modificando BindProperty en el DataAnnotationsModelBinder para eludir el NullReferenceException, y personalmente no me gusta que las propiedades solo estén vinculadas si fueran válidas (vea los motivos a continuación).

protected override void BindProperty(ControllerContext controllerContext, 
             ModelBindingContext bindingContext, 
             PropertyDescriptor propertyDescriptor) { 
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name); 

    // Only bind properties that are part of the request 
    if (bindingContext.ValueProvider.DoesAnyKeyHavePrefix(fullPropertyKey)) { 
     var innerContext = new ModelBindingContext() { 
      Model = propertyDescriptor.GetValue(bindingContext.Model), 
      ModelName = fullPropertyKey, 
      ModelState = bindingContext.ModelState, 
      ModelType = propertyDescriptor.PropertyType, 
      ValueProvider = bindingContext.ValueProvider 
     }; 

     IModelBinder binder = Binders.GetBinder(propertyDescriptor.PropertyType); 
     object newPropertyValue = ConvertValue(propertyDescriptor, binder.BindModel(controllerContext, innerContext)); 
     ModelState modelState = bindingContext.ModelState[fullPropertyKey]; 
     if (modelState == null) 
     { 
      var keys = bindingContext.ValueProvider.FindKeysWithPrefix(fullPropertyKey); 
      if (keys != null && keys.Count() > 0) 
       modelState = bindingContext.ModelState[keys.First().Key]; 
     } 
     // Only validate and bind if the property itself has no errors 
     //if (modelState.Errors.Count == 0) { 
      SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); 
      if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) { 

       OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); 
      } 
     //} 

     // There was an error getting the value from the binder, which was probably a format 
     // exception (meaning, the data wasn't appropriate for the field) 
     if (modelState.Errors.Count != 0) { 
      foreach (var error in modelState.Errors.Where(err => err.ErrorMessage == "" && err.Exception != null).ToList()) { 
       for (var exception = error.Exception; exception != null; exception = exception.InnerException) { 
        if (exception is FormatException) { 
         string displayName = GetDisplayName(propertyDescriptor); 
         string errorMessage = InvalidValueFormatter(propertyDescriptor, modelState.Value.AttemptedValue, displayName); 
         modelState.Errors.Remove(error); 
         modelState.Errors.Add(errorMessage); 
         break; 
        } 
       } 
      } 
     } 
    } 
} 

También he modificado para que siempre une los datos sobre la propiedad no importa si es válido o no. De esta forma, puedo pasar el modelo a la vista sin que se restablezcan las propiedades inválidas a nulo.

controlador Extracto

[AcceptVerbs(HttpVerbs.Post)] 
public ActionResult Edit(ProfileViewDataModel model) 
{ 
    FormCollection form = new FormCollection(this.Request.Form); 
    wsPerson service = new wsPerson(); 
    Person newPerson = service.Select(1, -1); 
    if (ModelState.IsValid && TryUpdateModel<IPersonBindable>(newPerson, "Person", form.ToValueProvider())) 
    { 
     //call wsPerson.save(newPerson); 
    } 
    return View(model); //model.Person is always bound no null properties (unless they were null to begin with) 
} 

Mi clase Modelo (persona) viene de un servicio web así que no puedo poner atributos en ellos directamente, la forma en que esto se resolvió de la siguiente manera:

Ejemplo con DataAnnotations anidados

[Validation.MetadataType(typeof(PersonValidation))] 
public partial class Person : IPersonBindable { } //force partial. 

public class PersonValidation 
{ 
    [Validation.Immutable] 
    public int Id { get; set; } 
    [Validation.Required] 
    public string FirstName { get; set; } 
    [Validation.StringLength(35)] 
    [Validation.Required] 
    public string LastName { get; set; } 
    CategoryItemNullable NearestGeographicRegion { get; set; } 
} 

[Validation.MetadataType(typeof(CategoryItemNullableValidation))] 
public partial class CategoryItemNullable { } 

public class CategoryItemNullableValidation 
{ 
    [Validation.Required] 
    public string Text { get; set; } 
    [Validation.Range(1,10)] 
    public string Value { get; set; } 
} 

Ahora si se unen af orm campo a [ViewDataModel.]Person.NearestGeographicRegion.Text & [ViewDataModel.]Person.NearestGeographicRegion.Value el modelo Estado comienza a validarlos correctamente y DataAnnotationsModelBinder también los vincula correctamente.

Esta respuesta no es definitiva, es el producto de rascarme la cabeza esta tarde. No se ha probado correctamente, aunque pasó las pruebas unitarias en the project Brian Wilson comenzó y la mayoría de mis propias pruebas limitadas. Para un cierre real en este asunto, me gustaría escuchar Brad Wilson ideas sobre esta solución.

+0

Parece que funciona muy bien para mí también, al menos a juzgar por mi primera prueba rápida. Muchas gracias. Le informaré a Brad de su corrección de errores, quizás tendrá algo de tiempo para echar un vistazo. –

+0

Genial para saber que también funciona para usted. Una última sugerencia si se une a matrices de objetos personalizadas (consulte: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx) A diferencia de ese artículo que menciona con este modelo de encuadernador, no necesita el campo de índice, de hecho, se equivocará pero funciona bien sin. –

3

La solución para este problema es simple, como ha señalado Martijn.

En el método BindProperty, se encuentra esta línea de código:

if (modelState.Errors.Count == 0) { 

Debe ser cambiado a:

if (modelState == null || modelState.Errors.Count == 0) { 

tenemos la intención de incluir DataAnnotations apoyo en la MVC 2, que se incluye el DataAnnotationsModelBinder. Esta característica será parte del primer CTP.

+0

¡Gracias de nuevo, Brad! :) –

Cuestiones relacionadas