Examen de los métodos y vistas de la acción de edición para el controlador de películas

por Rick Anderson

Nota:

Existe una versión actualizada de este tutorial, disponible aquí, donde se usa la versión más reciente de Visual Studio. El nuevo tutorial usa ASP.NET Core MVC, que proporciona muchas mejoras en este tutorial.

En este tutorial se muestra ASP.NET Core MVC con controladores y vistas. Razor Pages es una nueva alternativa en ASP.NET Core 2.0, un modelo de programación basado en páginas que facilita la compilación de interfaces de usuario web y hace que sean más productivas. Se recomienda probar el tutorial de las páginas de Razor antes que la versión MVC. El tutorial de las páginas de Razor:

  • Es más fácil de seguir.
  • Abarca más características.
  • Es el método preferido para el desarrollo de nuevas aplicaciones.

En esta sección, examinará los métodos y vistas de la acción Edit generados para el controlador de películas. Pero primero tomaremos una breve desviación para que la fecha de lanzamiento tenga mejor aspecto. Abra el archivo Models/Movie.cs y agregue las líneas resaltadas que se muestran a continuación:

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

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

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }

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

También puede hacer que la fecha entre en el ámbito de la referencia cultural del esta manera:

[Display(Name = "Release Date")]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }

En el tutorial siguiente se habla sobre DataAnnotations. El atributo Display especifica qué se muestra como nombre de un campo (en este caso, "Release Date" en lugar de "ReleaseDate"). El atributo DataType especifica el tipo de los datos, en este caso es una fecha, por lo que no se muestra la información horaria almacenada en el campo. El atributo DisplayFormat es necesario para un error en el explorador Chrome que representa los formatos de fecha incorrectamente.

Ejecute la aplicación y vaya al controlador Movies. Mantenga presionado el puntero del mouse sobre un vínculo Editar para ver la dirección URL a la que se vincula.

EditLink_sm

El método Html.ActionLink ha generado el vínculo Editar en la vista Views\Movies\Index.cshtml:

@Html.ActionLink("Edit", "Edit", new { id=item.ID })

Html.ActionLink

El objeto Html es un asistente que se expone mediante una propiedad en la clase base System.Web.Mvc.WebViewPage. El método ActionLink del asistente facilita la generación dinámica de hipervínculos HTML que se vinculan a métodos de acción en controladores. El primer argumento del método ActionLink es el texto del vínculo que se va a representar (por ejemplo, <a>Edit Me</a>). El segundo argumento es el nombre del método de acción que se va a invocar (en este caso, la acción Edit). El argumento final es un objeto anónimo que genera los datos de ruta (en este caso, el identificador de 4).

El vínculo generado que se muestra en la imagen anterior es http://localhost:1234/Movies/Edit/4. La ruta predeterminada (establecida en App_Start\RouteConfig.cs) toma el patrón de la dirección URL {controller}/{action}/{id}. Por lo tanto, ASP.NET traduce http://localhost:1234/Movies/Edit/4 en una solicitud al método de acción Edit del controlador Movies con el parámetro ID igual a 4. Examine el código siguiente del archivo App_Start\RouteConfig.cs. El método MapRoute se usa para enrutar las solicitudes HTTP al controlador y al método de acción correctos y proporcionar el parámetro ID opcional. El método MapRoute también lo usan los asistentes HtmlHelpers, como ActionLink, para generar direcciones URL dados el controlador, el método de acción y los datos de ruta.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", 
            id = UrlParameter.Optional }
    );
}

También puede pasar parámetros de método de acción mediante una cadena de consulta. Por ejemplo, la dirección URL http://localhost:1234/Movies/Edit?ID=3 también pasa el parámetro ID de 3 al método de acción Edit del controlador Movies.

EditQueryString

Abra el controlador Movies. A continuación se muestran los dos métodos de acción Edit.

// GET: /Movies/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    return View(movie);
}

// POST: /Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Entry(movie).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(movie);
}

Observe que el segundo método de acción Edit va precedido del atributo HttpPost. Este atributo especifica que la sobrecarga del método Edit solo se puede invocar para las solicitudes POST. Podría aplicar el atributo HttpGet al primer método de edición, pero no es necesario hacerlo porque es el valor predeterminado. (Nos referiremos a los métodos de acción asignados implícitamente al atributo HttpGet como métodos HttpGet). El atributo Bind es otro mecanismo de seguridad importante que impide que los hackers publiquen datos en exceso en el modelo. Solo debe incluir propiedades en el atributo bind que quiera cambiar. Puede leer sobre la sobrepublicación y el atributo bind en la nota de seguridad acerca de la sobrepublicación. En el modelo simple que se usa en este tutorial, se enlazarán todos los datos del modelo. El atributo ValidateAntiForgeryToken se usa para impedir la falsificación de una solicitud y se empareja con @Html.AntiForgeryToken() en el archivo de vista de edición (Views\Movies\Edit.cshtml); a continuación se muestra una parte:

@model MvcMovie.Models.Movie

@{
    ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()    
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.ID)

        <div class="form-group">
            @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Title)
                @Html.ValidationMessageFor(model => model.Title)
            </div>
        </div>

@Html.AntiForgeryToken() genera un token antifalsificación de formulario oculto que debe coincidir en el método Edit del controlador Movies. Puede obtener más información sobre la falsificación de solicitud entre sitios (también conocida como XSRF o CSRF) en mi tutorial Prevención XSRF/CSRF en MVC.

El método HttpGetEdit toma el parámetro ID de la película, busca la película con el método Find de Entity Framework y devuelve la película seleccionada a la vista de edición. Si no se encuentra una película, se devuelve HttpNotFound. Cuando el sistema de scaffolding creó la vista de edición, examinó la clase Movie y creó código para representar los elementos <label> y <input> para cada propiedad de la clase. En el ejemplo siguiente se muestra la vista de edición que generó el sistema de scaffolding de Visual Studio:

@model MvcMovie.Models.Movie

@{
    ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()    
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.ID)

        <div class="form-group">
            @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Title)
                @Html.ValidationMessageFor(model => model.Title)
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.ReleaseDate, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.ReleaseDate)
                @Html.ValidationMessageFor(model => model.ReleaseDate)
            </div>
        </div>
        @*Genre and Price removed for brevity.*@        
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Observe cómo la plantilla de vista tiene una instrucción @model MvcMovie.Models.Movie en la parte superior del archivo; esto especifica que la vista espera que el modelo de la plantilla de vista sea de tipo Movie.

El código con scaffolding usa varios métodos del asistente para simplificar el marcado HTML. El asistente Html.LabelFor muestra el nombre del campo: "Title" (Título), "ReleaseDate" (Fecha de lanzamiento), "Genre" (Género) o "Price" (Precio). El asistente Html.EditorFor representa un elemento HTML <input>. El asistente Html.ValidationMessageFor muestra cualquier mensaje de validación asociado a esa propiedad.

Ejecute la aplicación y navegue a la URL /Movies. Haga clic en un vínculo Edit (Editar). En el explorador, vea el código fuente de la página. A continuación se muestra el código HTML del elemento de formulario.

<form action="/movies/Edit/4" method="post">
   <input name="__RequestVerificationToken" type="hidden" value="UxY6bkQyJCXO3Kn5AXg-6TXxOj6yVBi9tghHaQ5Lq_qwKvcojNXEEfcbn-FGh_0vuw4tS_BRk7QQQHlJp8AP4_X4orVNoQnp2cd8kXhykS01" />  <fieldset class="form-horizontal">
      <legend>Movie</legend>

      <input data-val="true" data-val-number="The field ID must be a number." data-val-required="The ID field is required." id="ID" name="ID" type="hidden" value="4" />

      <div class="control-group">
         <label class="control-label" for="Title">Title</label>
         <div class="controls">
            <input class="text-box single-line" id="Title" name="Title" type="text" value="GhostBusters" />
            <span class="field-validation-valid help-inline" data-valmsg-for="Title" data-valmsg-replace="true"></span>
         </div>
      </div>

      <div class="control-group">
         <label class="control-label" for="ReleaseDate">Release Date</label>
         <div class="controls">
            <input class="text-box single-line" data-val="true" data-val-date="The field Release Date must be a date." data-val-required="The Release Date field is required." id="ReleaseDate" name="ReleaseDate" type="date" value="1/1/1984" />
            <span class="field-validation-valid help-inline" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
         </div>
      </div>

      <div class="control-group">
         <label class="control-label" for="Genre">Genre</label>
         <div class="controls">
            <input class="text-box single-line" id="Genre" name="Genre" type="text" value="Comedy" />
            <span class="field-validation-valid help-inline" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
         </div>
      </div>

      <div class="control-group">
         <label class="control-label" for="Price">Price</label>
         <div class="controls">
            <input class="text-box single-line" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" type="text" value="7.99" />
            <span class="field-validation-valid help-inline" data-valmsg-for="Price" data-valmsg-replace="true"></span>
         </div>
      </div>

      <div class="form-actions no-color">
         <input type="submit" value="Save" class="btn" />
      </div>
   </fieldset>
</form>

Los elementos <input> se muestran en un elemento <form> HTML cuyo atributo action se establece para publicar en la dirección URL /Movies/Edit. Los datos del formulario se publicarán en el servidor cuando se haga clic en el botón Guardar. La segunda línea muestra el token XSRF ocultogenerado por la llamada @Html.AntiForgeryToken().

Procesamiento de la solicitud POST

En la siguiente lista se muestra la versión HttpPost del método de acción Edit.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Entry(movie).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(movie);
}

El atributo ValidateAntiForgeryToken valida el token XSRF generado por la llamada @Html.AntiForgeryToken() en la vista.

El enlazador de modelos MVC de ASP.NET toma los valores de formulario publicados y crea un objeto Movie que se pasa como el parámetro movie. El método ModelState.IsValid comprueba que los datos presentados en el formulario pueden usarse para modificar (editar o actualizar) un objeto Movie. Si los datos son válidos, los datos de película se guardan en la colección Movies de db(instancia MovieDBContext). Los nuevos datos de película se guardan en la base de datos llamando al método SaveChanges de MovieDBContext. Después de guardar los datos, el código redirige al usuario al método de acción Index de la clase MoviesController, que muestra la colección de películas, incluidos los cambios que se acaban de hacer.

En cuanto la validación del lado cliente determina que el valor de un campo no es válido, se muestra un mensaje de error. Si JavaScript está deshabilitado, la validación del lado cliente está deshabilitada. Sin embargo, el servidor detecta que los valores publicados no son válidos y los valores de formulario se vuelven a reproducir con mensajes de error.

La validación se examina con más detalle más adelante en el tutorial.

Los asistentes Html.ValidationMessageFor de la plantilla de vista Edit.cshtml se encargan de mostrar los mensajes de error adecuados.

abcNotValid

Todos los métodos HttpGet siguen un patrón similar. Obtienen un objeto de película (o una lista de objetos, en el caso de Index) y pasan el objeto (modelo) a la vista. El método Create pasa un objeto de película vacío a la vista Crear. Todos los métodos que crean, editan, eliminan o modifican los datos lo hacen en la sobrecarga HttpPost del método. La modificación de datos en un método HTTP GET es un riesgo de seguridad, como se describe en la entrada de blog Sugerencia de MVC de ASP.NET n.º 46: No usar Eliminar vínculos porque crean vulnerabilidades de seguridad. La modificación de datos en un método GET también infringe procedimientos recomendados de HTTP y el patrón de arquitectura REST, que especifica que las solicitudes GET no deben cambiar el estado de la aplicación. En otras palabras, realizar una operación GET debería ser una operación segura sin efectos secundarios, que no modifica los datos persistentes.

Validación jQuery para configuraciones regionales distintas del inglés

Si usa un equipo en inglés de EE. UU., puede omitir esta sección e ir al siguiente tutorial. Puede descargar la versión de Globalize de este tutorial aquí. Para ver un excelente tutorial de dos partes sobre la internacionalización, consulte Internacionalización de MVC 5 de ASP.NET de Nadeem.

Nota:

Para admitir la validación de jQuery para configuraciones regionales diferentes del inglés que utilizan una coma (",") como separador decimal y formatos de fecha diferentes del inglés, 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. Puede obtener la validación diferente del inglés de jQuery desde NuGet. (No instale Globalize si usa una configuración regional de inglés).

  1. En el menú Herramientas, haga clic en Administrador de paquetes NuGet y luego en Administrar paquetes NuGet para la solución.

    Screenshot of the Tools menu to begin jQuery validation for non English locales.

  2. En el panel izquierdo, seleccione Examinar*.*(vea la imagen siguiente).

  3. En el cuadro de entrada, escriba Globalize*.

    Screenshot of the input box to enter Globalize.

    Elija jQuery.Validation.Globalize, elija MvcMovie y haga clic en Instalar. El archivo Scripts\jquery.globalize\globalize.js se agregará al proyecto. La carpeta *Scripts\jquery.globalize\cultures* contendrá muchos archivos JavaScript de referencia cultural. Tenga en cuenta que la instalación de este paquete puede tardar cinco minutos.

    El código siguiente muestra las modificaciones en el archivo Views\Movies\Edit.cshtml:

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

<script src="~/Scripts/globalize/globalize.js"></script>
<script src="~/Scripts/globalize/cultures/globalize.culture.@(System.Threading.Thread.CurrentThread.CurrentCulture.Name).js"></script>
<script>
    $.validator.methods.number = function (value, element) {
        return this.optional(element) ||
            !isNaN(Globalize.parseFloat(value));
    }
    $(document).ready(function () {
        Globalize.culture('@(System.Threading.Thread.CurrentThread.CurrentCulture.Name)');
    });
</script>
<script>
    jQuery.extend(jQuery.validator.methods, {
        range: function (value, element, param) {
            //Use the Globalization plugin to parse the value
            var val = Globalize.parseFloat(value);
            return this.optional(element) || (
                val >= param[0] && val <= param[1]);
        }
    });
    $.validator.methods.date = function (value, element) {
        return this.optional(element) ||
            Globalize.parseDate(value) ||
            Globalize.parseDate(value, "yyyy-MM-dd");
    }
</script>
}

Para evitar repetir este código en cada vista de edición, puede moverlo al archivo de diseño. Para optimizar la descarga de scripts, consulte el tutorial Agrupación y minificación.

Para obtener más información, consulte Internacionalización de MVC 3 de ASP.NET e Internacionalización de MVC 3 de ASP.NET: segunda parte (NerdDinner).

Como solución temporal, si no consigue que la validación funcione en su configuración regional, puede forzar que el equipo use el inglés de EE. UU. o puede desactivar JavaScript en su explorador. Para forzar que el equipo use inglés de EE. UU., puede agregar el elemento de globalización al archivo web.config de la raíz de los proyectos. El código siguiente muestra el elemento de globalización con la referencia cultural establecida en inglés de Estados Unidos.

<system.web>
    <globalization culture ="en-US" />
    <!--elements removed for clarity-->
  </system.web>

En el siguiente tutorial, implementaremos la funcionalidad de búsqueda.