side net mvc dataannotations custom bootstrap attribute asp c# asp.net-mvc validation data-annotations unobtrusive-validation

c# - dataannotations - Best Practices ViewModel Validation en ASP.NET MVC



mvc required field validation (4)

Estoy usando DataAnnotations para validar mi ViewModel en el lado del cliente con jquery.validate.unobtrusive y en el lado del servidor en la aplicación ASP.NET MVC .

No hace mucho tiempo, descubrí que puedo escribir una validación como esta:

[Required(ErrorMessage = "{0} is required")] public string Name { get; set; }

De esa manera puedo definir fácilmente algunas cadenas generales en config o en recursos y siempre usarlo en DataAnnotations . Por lo tanto, será más fácil cambiar los mensajes de validación en toda mi aplicación en el futuro.

También sé que hay una biblioteca FluentValidation que permite agregar reglas de validación a ViewModel ya existente. Sé que hay un problema con Add / Edit ViewModels que podría tener campos similares pero diferentes ValidationRules.

Otro problema que proviene de la validación del cliente es que html recientemente agregado a DOM (usando la solicitud ajax ) debe ser analizado para permitir la validación. Así es como lo hago:

$(''#some-ajax-form'').data(''validator'', null); $.validator.unobtrusive.parse(''#some-ajax-form'');

Entonces tengo algunas preguntas:

  1. ¿Hay alguna otra práctica útil que pueda ayudar a centralizar todas las reglas de validación en la aplicación?
  2. ¿Cuál es la mejor manera de resolver Agregar / Editar ViewModel problema de validación de ViewModel ? ¿Puedo usar DataAnnotations con FluentValidation o separar Add and Edit ViewModels todavía es una mejor opción?
  3. ¿Hay alguna forma mejor de inicializar la validación en los nuevos elementos DOM que se recibieron con ajax llamada otra que menciono?

No estoy preguntando cómo crear mis propios DataValidators . Sé cómo hacerlo. Estoy buscando formas de usarlos de una forma más productiva y fácil de mantener.


Como han dicho otros, no existen tales trucos ni una forma fácil de centralizar sus validaciones.

Tengo un par de enfoques que pueden interesarte. Tenga en cuenta que así es como "nosotros" resolvimos el mismo problema antes. Depende de usted si puede encontrar nuestra solución sostenible y productiva.

Sé que hay un problema con Add / Edit ViewModels que podría tener campos similares pero diferentes ValidationRules.

Enfoque de herencia

Puede lograr la validación centralizada utilizando una clase base y usar subclases para validaciones específicas.

// Base class. That will be shared by the add and edit public class UserModel { public int ID { get; set; } public virtual string FirstName { get; set; } // Notice the virtual? // This validation is shared on both Add and Edit. // A centralized approach. [Required] public string LastName { get; set; } } // Used for creating a new user. public class AddUserViewModel : UserModel { // AddUser has its own specific validation for the first name. [Required] public override string FirstName { get; set; } // Notice the override? } // Used for updating a user. public class EditUserViewModel : UserModel { public override string FirstName { get; set; } }

Extender el enfoque ValidationAttribute

Usando ValidationAtribute personalizado, puede lograr la validación centralizada. Esta es solo la implementación básica, solo te estoy mostrando la idea.

using System.ComponentModel.DataAnnotations; public class CustomEmailAttribute : ValidationAttribute { public CustomEmailAttribute() { this.ErrorMessage = "Error Message Here"; } public override bool IsValid(object value) { string email = value as string; // Put validation logic here. return valid; } }

Lo usarías como tal

public class AddUserViewModel { [CustomEmail] public string Email { get; set; } [CustomEmail] public string RetypeEmail { get; set; } }

¿Hay alguna forma mejor de inicializar la validación en los nuevos elementos DOM que se recibieron con ajax llamada otra que menciono?

Así es como vuelvo a vincular validadores en elementos dinámicos.

/** * Rebinds the MVC unobtrusive validation to the newly written * form inputs. This is especially useful for forms loaded from * partial views or ajax. * * Credits: http://www.mfranc.com/javascript/unobtrusive-validation-in-partial-views/ * * Usage: Call after pasting the partial view * */ function refreshValidators(formSelector) { //get the relevant form var form = $(formSelector); // delete validator in case someone called form.validate() $(form).removeData("validator"); $.validator.unobtrusive.parse(form); };

Uso

// Dynamically load the add-user interface from a partial view. $(''#add-user-div'').html(partialView); // Call refresh validators on the form refreshValidators(''#add-user-div form'');


Existen varias formas de validación de clientes, como una que Microsoft usa para MVC, que funciona con la biblioteca ubobtrusive creada por sí misma para integrarse con DataAnnotations . Pero , después de algunos años de trabajar con esta útil herramienta, me cansé de eso, lo cual es aburrido y tedioso de emplear en los casos en que necesitamos ViewModels separado (y es probable que ViewModels para crear / editar plantillas).

Otra forma es usar MVVM que funciona bien con MVC ya que los dos paradigmas son bastante similares. En MVC tiene un modelo que está limitado solo en el lado del servidor cuando el cliente envía contenido al servidor . Mientras que MVVM vincula un modelo local con la interfaz de usuario directamente en el cliente . Echa un vistazo a Knockoutjs , el conocido que te ayuda a entender cómo trabajar con MVVM.

Con esto en mente, responderé sus preguntas en orden:

  1. No puede centralizar las reglas de validación en la aplicación a menos que cree clases compartidas y las reutilice llamando en Modelos / Modelos de Vista por separado.
  2. Si desea utilizar Microsoft Validator, separar Add / Edit ViewModels es una mejor opción debido a su legibilidad y a su manera más fácil de cambiar.
  3. Nunca dije que los Knockoutjs son mejores, son diferentes entre sí, solo le da cierta flexibilidad para crear vistas basadas en los requisitos del modelo. Esto también te aleja de las validaciones centralizadas :(

La validación discreta de Jquery funciona al aplicar atributos a los elementos INPUT que instruyen a la biblioteca del cliente para que valide ese elemento utilizando una regla que esté asignada al atributo respectivo. Por ejemplo: el atributo html data-val-required es reconocido por la biblioteca no intrusiva y hace que valide ese elemento con la regla correspondiente.

En .NET MVC , puede hacer que esto suceda automáticamente para algunas reglas específicas al aplicar atributos a las propiedades de su modelo. Los atributos como Required y MaxLength funcionan porque los helpers de Html saben cómo leer esos atributos y agregar atributos de HTML correspondientes a su salida que la biblioteca discreta entiende.

Si agrega reglas de validación a sus modelos en IValidatableObject o usa FluentValidation , HTML Helper no verá estas reglas y, por lo tanto, no intentará traducirlas a atributos discretos.

En otras palabras, la coordinación "gratuita" que ha visto hasta ahora aplicando atributos a su modelo y obteniendo la validación del cliente está limitada a los atributos de validación, y además, está limitada (de forma predeterminada) solo a aquellos atributos que se asignan directamente a reglas discretas.

El lado positivo es que puede crear sus propios atributos de validación personalizados, e implementando IClientValidatable , Html Helper agregará un atributo discreto con el nombre de su elección que luego podrá enseñar a la discreta biblioteca a respetar.

Este es un atributo personalizado que utilizamos que garantiza que una fecha caiga después de otra fecha:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable { string otherPropertyName; public DateGreaterThanAttribute(string otherPropertyName, string errorMessage = null) : base(errorMessage) { this.otherPropertyName = otherPropertyName; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { ValidationResult validationResult = ValidationResult.Success; // Using reflection we can get a reference to the other date property, in this example the project start date var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName); // Let''s check that otherProperty is of type DateTime as we expect it to be if (otherPropertyInfo.PropertyType.Equals(new DateTime().GetType())) { DateTime toValidate = (DateTime)value; DateTime referenceProperty = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null); // if the end date is lower than the start date, than the validationResult will be set to false and return // a properly formatted error message if (toValidate.CompareTo(referenceProperty) < 1) { validationResult = new ValidationResult(this.GetErrorMessage(validationContext)); } } else { // do nothing. We''re not checking for a valid date here } return validationResult; } public override string FormatErrorMessage(string name) { return "must be greater than " + otherPropertyName; } private string GetErrorMessage(ValidationContext validationContext) { if (!this.ErrorMessage.IsNullOrEmpty()) return this.ErrorMessage; else { var thisPropName = !validationContext.DisplayName.IsNullOrEmpty() ? validationContext.DisplayName : validationContext.MemberName; var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName); var otherPropName = otherPropertyInfo.Name; // Check to see if there is a Displayname attribute and use that to build the message instead of the property name var displayNameAttrs = otherPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), false); if (displayNameAttrs.Length > 0) otherPropName = ((DisplayNameAttribute)displayNameAttrs[0]).DisplayName; return "{0} must be on or after {1}".FormatWith(thisPropName, otherPropName); } } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { //string errorMessage = this.FormatErrorMessage(metadata.DisplayName); string errorMessage = ErrorMessageString; // The value we set here are needed by the jQuery adapter ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule(); dateGreaterThanRule.ErrorMessage = errorMessage; dateGreaterThanRule.ValidationType = "dategreaterthan"; // This is the name the jQuery adapter will use //"otherpropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE! dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName); yield return dateGreaterThanRule; } }

Podemos aplicar el atributo al modelo como tal:

[DateGreaterThan("Birthdate", "You have to be born before you can die")] public DateTime DeathDate { get; set; }

Esto hace que el helper Html represente los dos atributos siguientes en el elemento INPUT cuando llama a Html.EditorFor en una propiedad de modelo que tiene este atributo:

data-val-dategreaterthan="You have to be born before you can die" data-val-dategreaterthan-otherpropertyname="Birthdate"

Hasta aquí todo bien, pero ahora debo enseñar validación discreta sobre qué hacer con esos atributos. Primero, tengo que crear una regla con nombre para la validación de jquery:

// Value is the element to be validated, params is the array of name/value pairs of the parameters extracted from the HTML, element is the HTML element that the validator is attached to jQuery.validator.addMethod("dategreaterthan", function (value, element, params) { return Date.parse(value) > Date.parse($(params).val()); });

Y luego agregue un adaptador discreto para esa regla que asigna el atributo a la regla:

jQuery.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) { options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname; options.messages["dategreaterthan"] = options.message; });

Después de haber hecho todo esto, puedo obtener esta regla de validación para "gratis" en cualquier otro lugar de mi aplicación simplemente aplicando ese atributo al modelo.

Para abordar su pregunta sobre cómo aplicar las reglas de forma condicional en función de si el modelo se utiliza en una operación de agregar o editar: esto probablemente se puede hacer agregando una lógica adicional a sus atributos personalizados y teniendo el método IsValid método de reglas GetClientValidation para obtener algún contexto del modelo utilizando la reflexión. Pero, sinceramente, eso me parece un desastre. Para esto, solo confiaría en la validación del servidor y en las reglas que elija aplicar utilizando el método IValidatableObject.Validate() .


Para responder primero a su 3ª pregunta: No, no hay una manera más fácil que lo que está haciendo. Dos líneas de código para que funcione apenas pueden ser más fáciles. Aunque hay un complemento que podría usar, como se explica en la pregunta, la validación no intrusiva no funciona con contenido dinámico

Su primera pregunta, cómo centralizar la validación, normalmente utilizo un archivo de clase separado para almacenar todas mis reglas de validación. De esta forma, no tengo que navegar a través de cada archivo de clase para encontrar las reglas, sino tenerlas todas en un solo lugar. Si eso es mejor, es materia de elección. La razón principal por la que comencé a utilizarlo es para poder agregar validación a las clases generadas automáticamente, como las clases de Entity Framework.

Así que tengo un archivo llamado ModelValidation.cs en mi capa de datos, y tengo código para todos mis modelos, como

/// <summary> /// Validation rules for the <see cref="Test"/> object /// </summary> /// <remarks> /// 2015-01-26: Created /// </remarks> [MetadataType(typeof(TestValidation))] public partial class Test { } public class TestValidation { /// <summary>Name is required</summary> [Required] [StringLength(100)] public string Name { get; set; } /// <summary>Text is multiline</summary> [DataType(DataType.MultilineText)] [AllowHtml] public string Text { get; set; } }

Ahora, como ha notado, no proporciono el mensaje de error real. Utilizo las convenciones de Haacked para agregar los mensajes. Hace que sea sencillo agregar reglas de validación localizadas.

Básicamente se trata de un archivo recource que contiene algo como:

Test_Name = "Provide name" Test_Name_Required = "Name is required"

Y estos mensajes y nombres se usarán cuando llame al código de MVC view regular de MVC view como

<div class="editor-container"> <div class="editor-label"> @Html.LabelFor(model => model.Name) <!--"Provide name"--> </div> <div class="editor-field"> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name) <!--"Name is required"--> </div> </div>

Su segunda pregunta acerca de la validación diferente para agregar / editar se puede manejar de dos maneras. La mejor manera sería usar las vistas tal como están diseñadas. Eso significa que no pasa sus modelos reales a las vistas, sino que crea un modelo de vista que contiene solo los datos. De modo que tiene un modelo de vista para Create con las reglas de validación apropiadas y un modelo de vista para Edit con las reglas adecuadas, y cuando lo pasan, inserte el resultado en su modelo real. Sin embargo, esto requiere mucho más código y trabajo manual, por lo que puedo imaginar que no estás realmente dispuesto a hacerlo así.

Otra opción sería usar la validación condicional como lo explica viperguynaz. Ahora, en lugar de un booleano, mis clases que requieren un cambio entre editar / agregar tienen una primary key Id int . Así que verifico si Id>0 para determinar si es una edición o no.

ACTUALIZAR:

Si desea actualizar la validación en cada llamada ajax, puede usar jQuery ajaxComplete . Esto revalidará todos los formularios después de cada solicitud de Ajax.

$( document ).ajaxComplete(function() { $(''form'').each(function() { var $el = $(this); $el.data(''validator'', null); $.validator.unobtrusive.parse($el); }) });

Si esto es algo que desea, depende de la frecuencia con la que reciba un formulario a través de AJAX . Si tiene muchas solicitudes de AJAX , como sondear un estado cada 10 segundos, entonces no quiere esto. Si tiene una solicitud AJAX ocasional, que en su mayoría contiene un formulario, puede usarlo.

Si su AJAX devuelve un formulario que desea validar, entonces sí, es una buena práctica actualizar la validación. Pero creo que una mejor pregunta sería "¿Realmente necesito enviar el formulario por AJAX?" AJAX es divertido y útil, pero debe usarse con cuidado y reflexión.