15 de junio de 2015

DataAnnotations y Buddy Class MVC

Cuando se trabaja en un proyecto web por lo general se tiene bien definida una capa que se encarga de las entidades de dominio, en esa capa se definen clases que corresponden con la información de una o varias tablas de tu base de datos (ya sea que tu definas cada clase a partir de una base de datos existente o que utilices Entity Framework).

Lo cierto es que cada entidad de dominio tiene validaciones que deben cumplirse, es ahí donde se utiliza DataAnnotations para validar la información que se ingresa vía entidades de dominio a nuestra base de datos. DataAnnotations ayuda a definir las reglas para las clases del modelo o propiedades para la validación de datos y la visualización de los mensajes adecuados a los usuarios finales, se encuentra en el espacio de nombres System.ComponentModel.DataAnnotations.

Data Annotation Validator Attributes

[DataType]: especifica el tipo de dato que se espera recibir.
[DisplayName]: especifica el titulo que se presentara en la vista.
[DisplayFormat]: specify the display format for a property like different format for Date property.
[Required]: especifica que una propiedad debe especificarla obligatoriamente.
[ReqularExpression]: especifica la validación utilizando una expresión regular.
[Range]: especifica el rango de valores validos.
[StringLength]: especifica la longitud máxima y mínima de un string.
[MaxLength]: especifica la longitud máxima de una propiedad.
[Bind]: especifica aquellas propiedades a las que ASP.NET MVC va llevar información desde la vista hasta el modelo, es decir, la propiedad aparecerá en la vista y estará disponible para que el usuario introduzca un valor pero ese valor que se introduzca en el campo de texto ID(por ejemplo) no se va enlazar con la entidad de negocio, ni siquiera es recibida por el controlador[HttpPost], es como engañar al usuario pero más bien se utiliza como medida de seguridad.
[ScaffoldColumn]: esconde una propiedad del helper HTML como EditorForModel y DisplayForModel, este atributo acepta un parámetro booleano, recuerda que como esta propiedad no está disponible al usuario a través de una vista, al aceptar los datos desde la vista en el controlador[HttpPost] quizás deberás especificar un valor para la propiedad.

Las propiedades candidatas para ScaffoldColumn son claves primarias y claves foráneas, también aquellas que requieran cálculos a partir de la existencia de otros valores.

Recordar que ambos atributos funcionan al utilizar el helper @Html.EditorForModel(), si tú haces tus propios formularios el atributo ScaffoldColumn o el atributo Bind no tienen sentido, a lo mucho te servirá de orientación sobre las propiedades de tu modelo, pero se supone que eso ya está debidamente documentado…o debería. Como recomendación, es mejor crear una ViewModel que responda con los campos que quieren mostrarse al usuario para no abusar y detallar los atributos DataAnnotations que en verdad nos sean útiles, a mas no haber alternativa puede utilizarse una Buddy Class.

Aquí va un ejemplo para una entidad de dominio Employee:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Employee.Models
{
 [Bind(Exclude = "EmpId")]
 public class Employee
 {
 [ScaffoldColumn(false)]
 public int EmpId { get; set; }
 
 [DisplayName("Employee Name")]
 [DataType(DataType.Text)]
 [Required(ErrorMessage = "Employee Name is required")]
 [StringLength(100,MinimumLength=3)]
 [CannotContainNumbers]
 public String EmpName { get; set; } 
 
 [Required(ErrorMessage = "Employee Address is required")] 
 [DataType(DataType.Text)] 
 [StringLength(300)]
  public string Address { get; set; } 
 
 [Required(ErrorMessage = "Salary is required")] 
 [Range(3000, 10000000,ErrorMessage = "Salary must be between 3000 and 10000000")]
  public int Salary{ get; set; } 
 
 [Required(ErrorMessage = "Please enter your email address")] 
 [DataType(DataType.EmailAddress)]
 [Display(Name = "Email address")]
 [MaxLength(50)]
 [RegularExpression(@"[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}", ErrorMessage = "Please enter    correct email")]
  public string Email { get; set; }

 [DataType(DataType.PhoneNumber)]
 [RegularExpression("^[0-9]{8}$")]
 [StringLength(32)]
  public string Phone { get; set; }
 
 [DataType(DataType.Date)]
  public DateTime Birthday { get; set; }
 
 [DataType(DataType.MultilineText)]
 [StringLength(255)]
  public string Remarks { get; set; }

}}

Es así de fácil, el único problema(o problemon dependiendo de tu nivel) es que las validaciones se hacen en el lado del servidor, esto significa que, al momento de enviar un formulario que contiene errores de validación, el control pasa al servidor sólo para regresar con mensajes de error. Para evitar este ida y vuelta puede agregar capacidades de validaciones del lado del cliente a la vista entonces… 

Adición de capacidades de validación de cliente.

Cuando ya estén definidas las validaciones para el modelo utilizando data annotations, estas van a ser utilizadas automáticamente por los Html Helpers en las vistas, pero antes debemos asegurarnos de agregar estas etiquetas

tambien puede servirte:
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/jqueryui")

Recuerda que se hacen referencia a Bundles pues facilita la importación de librerías, también se utiliza el helper Url.Content () para obtener URLs de tres archivos de script, a saber. jquery-X.X.X.js, jquery.validate.js y jquery.validate.unobtrusive.js.

Estos archivos son necesarios para llevar a cabo la validación del lado del cliente. Ahora, web.config abierto y asegurar que existen las siguientes especificaciones en la sección :

 
  
  
  
  
  
 

Y revisar que el archivo ~\App_Start\BundleConfig.cs incluya lo siguiente
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
            "~/Scripts/jquery.validate.unobtrusive*",
            "~/Scripts/jquery.validate*"));

Si usamos los atributos estándar (no creamos atributos propios) ya tenemos la validación en cliente automática. Es decir al mismo tiempo que el usuario vaya tecleando los valores o cambie el foco ya ser irán mostrando los distintos errores que haya y si hay errores no podrá enviar el formulario.

Por supuesto la validación en cliente no tiene nada que ver con la seguridad, es un tema de usabilidad (darle feedback al usuario de forma más rápida) así que el uso de validación en cliente no inhibe de realizarla en servidor. ASP.NET MVC la realiza siempre en servidor (y nosotros por nuestra parte debemos comprobar siempre el valor de la propiedad IsValid del ModelState).

Si por alguna razón se desea desactivar la validación en cliente en alguna vista, basta con llamar al método Html.EnableClientValidation con el parámetro a false:
@{ Html.EnableClientValidation(false); }
@using (Html.BeginForm())
      {
            // Codigo del form
     }

Este sería un ejemplo de la vista utilizando la entidad de dominio Employee debidamente validada, para evitar confusiones aclaro que cree la vista fuertemente tipada
@model Employee.Models
@{ 
 ViewBag.Title = "Employee Details"; 
 Layout = "~/Views/Shared/_Layout.cshtml";
 }
 @using (Html.BeginForm())
 {
 
@Html.LabelFor(m => m.EmpName)
@Html.TextBoxFor(m => m.EmpName) @Html.ValidationMessageFor(m => m.EmpName)
@Html.LabelFor(m => m.Address)
@Html.TextBoxFor(m => m.Address) @Html.ValidationMessageFor(m => m.Address)
@Html.LabelFor(m => m.Salary)
@Html.TextBoxFor(m => m.Salary) @Html.ValidationMessageFor(m => m.Salary)
@Html.LabelFor(m => m.Email)
@Html.TextBoxFor(m => m.Email) @Html.ValidationMessageFor(m => m.Email)
}

A continuación expongo los atributos de validación que DataAnnotations nos entrega y que muestran cierta complejidad, recuerda que también puedes utilizar expresiones regulares o bien crear tus validaciones personalizadas.

Validacion de password
[Required]
[DataType(DataType.Password)]
[DisplayName("Password")]
 public string Password { get; set; }

[Required]
[DataType(DataType.Password)]
[DisplayName("Confirm Password")]
[SameAs("Password", ErrorMessage = "It should be similar to Password")]
 public string ConfirmPassword { get; set; }

Validacion de precios
[Required(ErrorMessage = "Price is required")]
[Range(0.01, 999999999, ErrorMessage = "Price must be greater than 0.00")]
[DisplayName("Price ($)")]
 public decimal Price { get; set; }

[Integer(ErrorMessage="This is needs to be integer")]
 public int CustomerId { get; set; }

[Integer]
[Min(1, ErrorMessage="Unless you are benjamin button you are lying.")]
[Required]
 public int Age { get; set; }

Validacion de fechas.

Para obtener el formato dd/mm/yyyy = 24/12/2015 se puede utilizar el atributo
[DisplayFormat(ApplyFormatInEditMode = true,
 DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime? StartDate { get; set; }

para obtener el formato fecha y hora se puede utilizar el atributo
[DataType(DataType.DateTime)]
public DateTime? StartDate { get; set; }

Validación de extensión de archivos
[FileExtensions("png|jpg|jpeg|gif")]
 public string ProfilePictureLocation { get; set; }

Validaciones utilizando expresiones regulares.

La primera opción es crear el conjunto de validaciones que utilizan expresiones regulares en un archivo independiente e importar este archivo desde la clase que defina la entidad de negocio, para este ejemplo se validara la dirección de correo electrónico

namespace MyProject.Attributes.Validation{
 using System.ComponentModel.DataAnnotations;
 
 public class EmailAttribute : RegularExpressionAttribute{

 public EmailAttribute()
 : base(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
   {
   this.ErrorMessage = "Please provide a valid email address";
   }
 }
}

Y en la clase que define la entidad de negocio
using MyProject.Attributes.Validation;
 
public class MyModel
{
 [Required]
 [Email]
  public string EmailAddress { get; set; }
}

La segunda opción es definir la expresión regular en la misma propiedad dentro de la definición de entidad de negocio
[RegularExpression(@"a-z, A-Z", ErrorMessage = "Solo letras")]
public string LastName { get; set; }

[RegularExpression(@"^[0-9]{0,15}$", ErrorMessage = "Definir solo numeros")]
public string PhoneNumber { get; set; }

[RegularExpression(@"^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$", ErrorMessage = "formato invalido")]
public string eMail { get; set; }

Creando Validaciones Personalizadas.

como primera recomendacion lo mejor es crear un folder donde se guardaran las validaciones personalizadas, estas deben hererdar la clase ValidationAttribute y se sobrescribe el método ValidationResult IsValid
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Web;  
using System.ComponentModel.DataAnnotations;  
using System.Text.RegularExpressions;  
  
namespace Custom_DataAnnotation_Attribute.Models {  
 public class CustomEmailValidator : ValidationAttribute {  
  protected override ValidationResult IsValid(object value, ValidationContext validationContext)  
  {  
   if (value != null){  
      string email = value.ToString();  
 
   if (Regex.IsMatch(email, @"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", RegexOptions.IgnoreCase))  
            {  
  return ValidationResult.Success;  
             }  
  else  
             {  
  return new ValidationResult("Please Enter a Valid Email.");  
               }  
            }  
   else  
            {  
   return new ValidationResult("" + validationContext.DisplayName + " is required");  
            }  
        }  

Y desde la entidad de negocio se utilizaría así
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Web;  
using System.ComponentModel.DataAnnotations;  
  
namespace Custom_DataAnnotation_Attribute.Models  
{  
 public class EmployeeModel  
 {  
  public string Name { get; set; }  
  
 [CustomEmailValidator]  
  public string Email { get; set; }  
  public string Password { get; set; }  
  public string Mobile { get; set; }          
 }  
}  

También se puede crear una validación personalizada en la cual la clase utilice un constructor, así
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
 
namespace CustomValidation.CustomValidator
{
 public class CustomPasswordValidator:ValidationAttribute
  {
   private readonly int minLen;
   private readonly int maxLen;
 
//The constructor accepts two parameters. These parameters have to be supplied while applying this attribute.
//We are also passing a default message to base class. This is default message.
 
 public CustomPasswordValidator(int minLength, int maxLength)
 : base("{0} length should be between " + minLength + " and " + maxLength + "")
   {
     minLen = minLength;
     maxLen = maxLength;
   }
 
//We have override the IsValid method which accepts value and ValidationContext object.
//value is the input provided by user from Form. The value which is posted from form.
//Validation context object has details about the property on which this attribute is used.

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
   {
 
//Validating user input.
//The value is null, if the user leaves age field balnk on form.
//We are returing error message in the else part of this if.
//This check works like Required attribute.

if (value != null) {
//Converting the value to integer from object type.
string userValue = value.ToString();
 
//Comparing the length of password entered with the minimum limit.
if (userValue.Length < minLen){
//If the length of password is less than the applied length, validation message is thrown.
return new ValidationResult("Password cannot be less than 6 letters.");
                }
//Comparing the length of password entered with the maximum limit.
else if (userValue.Length > maxLen){
//If the length of password is greater than the applied length, validation message is thrown.
return new ValidationResult("Password cannot be greater than 12 letters");
}
 else
{
//If the supplied password passes all the validations success result is returned.
 return ValidationResult.Success;
 }
}
else
{
//If the user does not provide his password. The mandatory error message is shown.
return new ValidationResult("Password is manadatory Field.");
            } } } }

Y se aplica de la siguiente manera:
[DataType(DataType.Password)]
[CustomPasswordValidator(6,12)]
public string Password { get; set; }

un ultimo ejemplo para validar string con longitud definida
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
 
namespace CustomValidation.CustomNameValidator
{
    //Inherited the base class ValidationAttribute to create our own Custom Attribute.
    public class CustomNameValidator : ValidationAttribute
    {
        private readonly int maxWords;
 
        //The constructor accepts single parameter i.e. maximum limit  to check the count of words. 
        //This can be used to validate FirstName to have specified number of words.
        //We are also passing a default message to base class. This is default message. 
        public CustomNameValidator(int maximumLimit)
            : base("{0} should be less than " + maximumLimit + " letters.")
        {
            //We are setting a private variable MaxWords with maximum limit which is specified while defining attribute.
            maxWords = maximumLimit;
        }
 
        //We have override the IsValid method which accepts value and ValidationContext object.
        //value is the input provided by user from Form. The value which is posted from form.
        //Validation context object has details about the property on which this attribute is used.
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            //Checking if the value is null.
            if (value != null)
            {
                //casting the object value to string.
                var valueAsString = value.ToString();
                //comparing the input value length and the length specified while defining attributes.
                if (valueAsString.Length < maxWords)
                {
                    //If the input length is within the maximum length allowed then attribute will return Success.
                    return ValidationResult.Success;
                }
                else
                {
                    //If the input length is not withing the maximum length, then error message is returned.
                    //As we have passed the default message above. We have passed a placeholder with it.
                    //The display name is passed as parameter. And error message is displayed.
                    var errorMessage = FormatErrorMessage(validationContext.DisplayName);
                    return new ValidationResult(errorMessage);
                }
            }
            else
            {
                //If value is null, we are returning error message. This validation works like Required attribute.
                //In below line we are passing display name and message as well.
                return new ValidationResult("" + validationContext.DisplayName + " is manadatory field");
            }
        }
    }
}

Aplicando:
[CustomNameValidator.CustomNameValidator(40)]
 public string LastName { get; set; }

Donde especificamos las DataAnnotations? Hay escenarios en los que no tenemos acceso a la clase en la que deseamos introducir las anotaciones. Un ejemplo claro lo encontramos cuando nos interesa especificar las restricciones en una clase generada por un proceso automático, como el diseñador de EDM de Entity framework; cualquier cambio realizado sobre el código generado será sobrescrito sin piedad al modificar el modelo, supongamos tengo esta tabla que cuya entidad fue creada vía Entity Framework

CREATE TABLE [dbo].[Friend](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [nvarchar](100) NOT NULL,
    [LastName] [nvarchar](100) NOT NULL,
    [BirthDate] [date] NOT NULL,
 CONSTRAINT [PK_Friend] PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
 
GO

Esta tabla solo tendrá una lista de personas, luego he añadido mi ADO.NET Entity Data Set, en mi carpeta Modelo, que crea un objeto para esta tabla. Como he señalado, cualquier cambio tal como una validación sobre esta clase se van a reemplazar cada vez que se vuelve a generar. Este es un problema cuando se quiere agregar algunos atributos de la clase. En estos casos, es una práctica frecuente definir los metadatos en clases “buddy”, que son copias exactas de la entidad a anotar, pero que serán utilizadas únicamente como contenedores de anotaciones.

Las clases buddy se vinculan con la entidad original utilizando el atributo MetadataType de la siguiente forma:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
 
namespace MVCBuddyClass.Models{
    
[MetadataType(typeof(FriendMeta))]
 public partial class Friend
    {    }
 
public class FriendMeta{

 [DisplayName("First Name")]
 public string FirstName { get; set; }
 [DisplayName("Last Name")]
 public string LastName { get; set; }
 [DisplayName("Date of Birth")]
 public DateTime BrithDate { get; set; }
    }
}

Observa que para poder utilizar esta técnica es necesario que la entidad a la que queremos añadir anotaciones sea creada como parcial. En caso contrario no podríamos indicarle con MetadataType dónde se encuentran definidos sus atributos de validación.

El código anterior funciona, pero hay programadores que piensan que la manera correcta es utilizar una internal sealed class, necesaria para emparentar los atributos.

[MetadataType(typeof(CategoryMetaData))]

public partial class Category{
 internal sealed class CategoryMetaData{
 private CategoryMetaData()
 { }
 
[Required]
 public string Name { get; set; }
 
[StringLength(20)]
 public string Code { get; set; }
 
[Display(Name="Category Description")]
 public string Description { get; set; }
 }
}

ViewModel Class La mejor opción es utilizar View Model class pues permite especificar las propiedades que queremos que aparezcan en la vista, sobre estas propiedades se pueden definir validaciones utilizando DataAnnotations