Compartir vía


Adición de lógica de validación al modelo Movie

por Rick Anderson

Nota:

Aquí hay disponible una versión actualizada de este tutorial en la que se usa ASP.NET MVC 5 y Visual Studio 2013. Es más segura, mucho más sencilla de seguir y muestra más características.

En esta sección, agregará lógica de validación al modelo Movie y garantizará que las reglas de validación se apliquen cada vez que un usuario intente crear o editar una película mediante la aplicación.

Respeto del principio DRY

Uno de los principales principios de diseño de ASP.NET MVC es DRY ("Una vez y solo una"). ASP.NET MVC le anima a que especifique la funcionalidad o el comportamiento una sola vez y luego lo refleje en el resto de la aplicación. Esto reduce la cantidad de código que necesita escribir y hace que el código que escribe sea menos propenso a errores y más fácil de mantener.

La compatibilidad de validación proporcionada por ASP.NET MVC y Code First de Entity Framework es un buen ejemplo del principio DRY en acción. Puede especificar las reglas de validación mediante declaración en un lugar (en la clase del modelo) y se aplicarán en toda la aplicación.

Ahora verá cómo puede aprovechar esta compatibilidad de validación en la aplicación de películas.

Adición de reglas de validación al modelo Movie

Para empezar, agregará lógica de validación a la clase Movie.

Abra el archivo Movie.cs. Agregue una instrucción using en la parte superior del archivo que haga referencia al espacio de nombres System.ComponentModel.DataAnnotations:

using System.ComponentModel.DataAnnotations;

Observe que el espacio de nombres no contiene System.Web. DataAnnotations proporciona un conjunto integrado de atributos de validación que puede aplicar mediante declaración a cualquier clase o propiedad.

Ahora, actualice la clase Movie para aprovechar las ventajas de los atributos de validación integrados Required, StringLength y Range. Use el código siguiente como ejemplo de dónde aplicar los atributos.

public class Movie {
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    public string Genre { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [StringLength(5)]
    public string Rating { get; set; }
}

Ejecute la aplicación y obtendrá de nuevo el siguiente error en tiempo de ejecución:

El modelo que respalda al contexto "MovieDBContext" ha cambiado desde que se creó la base de datos. Considere la posibilidad de usar Migraciones de Code First para actualizar la base de datos (https://go.microsoft.com/fwlink/?LinkId=238269).

Usará migraciones para actualizar el esquema. Compile la solución, abra la ventana Consola del Administrador de paquetes y escriba los siguientes comandos:

add-migration AddDataAnnotationsMig
update-database

Cuando este comando finaliza, Visual Studio abre el archivo de clase que define la nueva clase DbMigration derivada con el nombre especificado (AddDataAnnotationsMig) y, en el método Up, puede ver el código que actualiza las restricciones de esquema. Los campos Title y Genre ya no admiten valores NULL (es decir, debe escribir un valor) y el campo Rating tiene una longitud máxima de 5.

Los atributos de validación especifican el comportamiento que quiere aplicar en las propiedades del modelo al que se aplican. El atributo Required indica que una propiedad debe tener un valor; en este ejemplo, una película debe tener valores para las propiedades Title, ReleaseDate, Genre y Price para que sea válida. El atributo Range restringe un valor a un intervalo determinado. El atributo StringLength permite establecer la longitud máxima de una propiedad de cadena y, opcionalmente, su longitud mínima. Los tipos intrínsecos (como decimal, int, float, DateTime) son obligatorios de forma predeterminada y no necesitan el atributo Required.

Code First garantiza que las reglas de validación que especifique en una clase de modelo se aplican antes de que la aplicación guarde los cambios en la base de datos. Por ejemplo, el código siguiente iniciará una excepción cuando se llame al método SaveChanges, ya que faltan varios valores de propiedad Movie obligatorios y el precio es cero (que está fuera del intervalo válido).

MovieDBContext db = new MovieDBContext();

Movie movie = new Movie();
movie.Title = "Gone with the Wind";
movie.Price = 0.0M;

db.Movies.Add(movie);  
db.SaveChanges();        // <= Will throw server side validation exception  

Tener reglas de validación aplicadas automáticamente por .NET Framework ayuda a que la aplicación sea más sólida. También nos permite asegurarnos de que todo se valida y que no nos dejamos ningún dato incorrecto en la base de datos accidentalmente.

Esta es una lista de código completa para el archivo Movie.cs actualizado:

using System;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models {
    public class Movie {
        public int ID { get; set; }

        [Required]
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Required]
        public string Genre { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }

        [StringLength(5)]
        public string Rating { get; set; }
    }

    public class MovieDBContext : DbContext {
        public DbSet<Movie> Movies { get; set; }
    }
}

Interfaz de usuario de error de validación en ASP.NET MVC

Vuelva a ejecutar la aplicación y vaya a la dirección URL /Movies.

Haga clic en el vínculo Crear nueva para agregar una nueva película. Rellene el formulario con algunos valores no válidos y, después, haga clic en el botón Crear.

8_validationErrors

Nota:

Para admitir la validación de jQuery para configuraciones regionales diferentes del inglés que utilizan una coma (",") como separador decimal, debe incluir globalize.js y su archivo cultures/globalize.cultures.js específico (de https://github.com/jquery/globalize) y JavaScript para utilizar Globalize.parseFloat. En el código siguiente se muestran las modificaciones en el archivo Views\Movies\Edit.cshtml para que funcione con la referencia cultural "fr-FR":

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script src="~/Scripts/globalize.js"></script>
    <script src="~/Scripts/globalize.culture.fr-FR.js"></script>
    <script>
        $.validator.methods.number = function (value, element) {
            return this.optional(element) ||
                !isNaN(Globalize.parseFloat(value));
        }
        $(document).ready(function () {
            Globalize.culture('fr-FR');
        });
    </script>
    <script>
        jQuery.extend(jQuery.validator.methods, {    
            range: function (value, element, param) {        
                //Use the Globalization plugin to parse the value        
                var val = $.global.parseFloat(value);
                return this.optional(element) || (
                    val >= param[0] && val <= param[1]);
            }
        });

    </script>
}

Observe cómo el formulario ha usado automáticamente un color de borde rojo para resaltar los cuadros de texto que contienen datos no válidos y ha emitido un mensaje de error de validación adecuado junto a cada uno. Los errores se aplican en el lado cliente (con JavaScript y jQuery) y en el lado servidor (cuando un usuario tiene JavaScript deshabilitado).

Una ventaja importante es que no fue necesario cambiar ni una sola línea de código en la clase MoviesController ni en la vista Create.cshtml para habilitar esta interfaz de usuario de validación. El controlador y las vistas que creó en pasos anteriores de este tutorial seleccionaron automáticamente las reglas de validación que especificó mediante atributos de validación en las propiedades de la clase del modelo Movie.

Es posible que haya observado que para las propiedades Title y Genre, el atributo obligatorio no se aplica hasta que envíe el formulario (al pulsar el botón Crear) o escribe texto en el campo de entrada y lo quite. Para un campo que está inicialmente vacío (como los campos de la vista Crear) y que solo tiene el atributo obligatorio y ningún otro atributo de validación, puede hacer lo siguiente para desencadenar la validación:

  1. Acceder mediante tabulación al campo.
  2. Escribir un texto.
  3. Presione TAB para salir del campo.
  4. Volver a acceder mediante tabulación al campo.
  5. Quitar el texto.
  6. Presione TAB para salir del campo.

La secuencia anterior desencadenará la validación necesaria sin presionar el botón Enviar. Simplemente al pulsar el botón Enviar sin escribir en ninguno de los campos se desencadenará la validación del lado cliente. Los datos del formulario no se envían al servidor hasta que no hay ningún error de validación del lado cliente. Para comprobarlo, coloque un punto de interrupción en el método HTTP Post, o bien use la herramienta fiddler o las herramientas de desarrollo F12 de IE 9.

Screenshot that shows the M V C Movie Create page. An alert next to Title states that The Title field is required. An alert next to Genre states that The Genre field is required.

Cómo se produce la validación en la vista Create y el método de acción Create

Tal vez se pregunte cómo se generó la validación de la interfaz de usuario sin actualizar el código en el controlador o las vistas. En la lista siguiente se muestra el aspecto de los métodos Create de la clase MovieController. No han cambiado de la forma en que los ha creado antes en este tutorial.

//
// GET: /Movies/Create

public ActionResult Create()
{
    return View();
}

//
// POST: /Movies/Create

[HttpPost]
public ActionResult Create(Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Movies.Add(movie);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(movie);
}

El primer método de acción Create (HTTP GET) muestra el formulario de creación inicial. La segunda versión ([HttpPost]) controla el envío de formulario. El segundo método Create (la versión HttpPost) llama a ModelState.IsValid para comprobar si la película tiene errores de validación. Al llamar a este método se evalúan todos los atributos de validación que se hayan aplicado al objeto. Si el objeto tiene errores de validación, el método Create vuelve a mostrar el formulario. Si no hay ningún error, el método guarda la nueva película en la base de datos. En el ejemplo de película que se usa, el formulario no se publica en el servidor cuando se detectan errores de validación en el lado cliente; nunca se llama al segundo método Create. Si deshabilita JavaScript en el explorador, la validación de cliente se deshabilitará y el método Create HTTP POST llama a ModelState.IsValid para comprobar si la película tiene errores de validación.

Puede establecer un punto de interrupción en el método HttpPost Create y comprobar si nunca se llama al método. La validación del lado cliente no enviará los datos del formulario cuando se detecten errores de validación. Si deshabilita JavaScript en el explorador y después envía el formulario con errores, se alcanzará el punto de interrupción. Puede seguir obteniendo validación completa sin JavaScript. En la imagen siguiente se muestra cómo deshabilitar JavaScript en Internet Explorer.

Screenshot that shows the Internet Options window open to the security tab. Custom level is circled in red. In the Security Settings window, Active scripting is set to disable. The scroll bar is circled in red.

Screenshot that shows the H t t p post. If Model State dot Is Valid is highlighted.

En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Firefox.

Screenshot that shows the Options window. Content is selected and Enable Java Script is checked.

En la siguiente imagen se muestra cómo deshabilitar JavaScript en el explorador Chrome.

Screenshot that shows the Options page. Under the Hood is selected and circled in red. In the Content Settings, Java Script is set to Allow all sites to run Java Script.

A continuación se muestra la plantilla de vista Create.cshtml a la que ha aplicado scaffolding antes en este tutorial. Los métodos de acción que se muestran arriba la usan para mostrar el formulario inicial y para volver a mostrarlo en caso de error.

@model MvcMovie.Models.Movie

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Movie</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ReleaseDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ReleaseDate)
            @Html.ValidationMessageFor(model => model.ReleaseDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Genre)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Genre)
            @Html.ValidationMessageFor(model => model.Genre)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>
        <div class="editor-label">
    @Html.LabelFor(model => model.Rating)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.Rating)
    @Html.ValidationMessageFor(model => model.Rating)
</div>
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Observe cómo el código usa un asistente Html.EditorFor para generar el elemento <input> para cada propiedad Movie. Junto a este asistente, se llama al método asistente Html.ValidationMessageFor. Estos dos métodos de asistente funcionan con el objeto de modelo que pasa el controlador a la vista (en este caso, un objeto Movie). Buscan automáticamente los atributos de validación especificados en el modelo y muestran los mensajes de error según corresponda.

Lo mejor de este enfoque es que ni el controlador ni la plantilla de vista Create saben nada de las reglas de validación actuales que se aplican, ni conocen los mensajes de error específicos que se muestran. Las reglas de validación y las cadenas de error solo se especifican en la clase Movie. Estas mismas reglas de validación se aplican automáticamente a la vista Edit y a cualquier otra plantilla de vista creada que edite el modelo.

Si quiere cambiar la lógica de validación más tarde, puede hacerlo exactamente en un solo lugar mediante la adición de atributos de validación al modelo (en este ejemplo, la clase movie). No tendrá que preocuparse de que diferentes partes de la aplicación sean incoherentes con el modo en que se aplican las reglas: toda la lógica de validación se definirá en un solo lugar y se usará en todas partes. Esto mantiene el código muy limpio y hace que sea fácil de mantener y evolucionar. También significa que respeta totalmente el principio DRY.

Adición de formato al modelo Movie

Abra el archivo Movie.cs y examine la clase Movie. El espacio de nombres System.ComponentModel.DataAnnotations proporciona atributos de formato además del conjunto integrado de atributos de validación. Ya hemos aplicado un valor de enumeración DataType en la fecha de lanzamiento y los campos de precio. En el código siguiente se muestran las propiedades ReleaseDate y Price con el atributo DisplayFormat adecuado.

[DataType(DataType.Date)] 
public DateTime ReleaseDate { get; set; }

[DataType(DataType.Currency)] 
public decimal Price { get; set; }

Los atributos DataType no son atributos de validación; se usan para indicar al motor de vistas cómo representar el código HTML. En el ejemplo anterior, el atributo DataType.Date muestra las fechas de la película solo como fechas, sin la hora. Por ejemplo, los atributos DataType siguientes no validan el formato de los datos:

[DataType(DataType.EmailAddress)]
[DataType(DataType.PhoneNumber)]
[DataType(DataType.Url)]

Los atributos enumerados antes solo proporcionan sugerencias para que el motor de vistas aplique formato a los datos (y proporcione atributos como <a> para las direcciones URL y <a href="mailto:EmailAddress.com"> para el correo electrónico). Puede usar el atributo RegularExpression para validar el formato de los datos.

Un enfoque alternativo para usar los atributos DataType sería establecer explícitamente un valor DataFormatString. En el código siguiente se muestra la propiedad de fecha de lanzamiento con una cadena de formato de fecha (es decir, "d"). Lo usaría para especificar que no quiere la hora como parte de la fecha de lanzamiento.

[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime ReleaseDate { get; set; }

A continuación se muestra la clase Movie completa.

public class Movie {
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    public string Genre { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [StringLength(5)]
    public string Rating { get; set; }
}

Ejecute la aplicación y vaya al controlador Movies. La fecha de lanzamiento y el precio tienen un formato agradable. En la imagen siguiente se muestra la fecha de lanzamiento y el precio con "fr-FR" como referencia cultural.

8_format_SM

En la imagen siguiente se muestran los mismos datos con la referencia cultural predeterminada (Inglés de EE. UU.).

Screenshot that shows the M V C Movie Index page with four movies listed.

En la siguiente parte de la serie de tutoriales, revisaremos la aplicación y realizaremos algunas mejoras a los métodos Details y Delete generados automáticamente.