Métodos del controlador y vistas en ASP.NET Core MVC

Ya tenemos un buen comienzo para nuestra aplicación de películas, pero la presentación no es ideal. No queremos ver la hora (12:00:00 AM en la imagen de abajo) y el texto de la columna Release Date] deberían ser dos palabras separadas por un espacio.

Lista de películas con el campo ReleaseDate

Abramos el archivo Models/Movie.cs agreguemos las líneas para que quede de la manera siguiente:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

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

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}

Vamos a cubrir las DataAnnotations en el siguiente tutorial. El atributo Display especifica que vamos a mostrar como nombre de un campo (en este caso “Release Date” en lugar de “ReleaseDate”). El atributo DataType especifica el tipo de datos (Date) para que la información guardada en el campo sobre la hora no se muestre.

La data annotation [Column(TypeName = "decimal(18, 2)")] es requerida para que el Entity FrameworkCore pueda mapear correctamente la propiedad Price para manejar información monetaria en la base de datos. Para más información, ver Data Types.

Naveguemos al controlador Moviesy movamos el mouse sobre el link Edit para ver la dirección URL de destino.

Link editar, la barra de estado muestra la URL

Los enlaces Edit, Details y Delete son generados por el Tag Helper de MVC de Anchor en el archivo Views/Movies/Index.cshtml.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
    </td>
</tr>

Los Tag Helpers de ASP.NET MVC Core permiten que el código del lado del servidor participe en crear y renderizar los elementos HTML en los archivos Razor. En el código de arriba, el Tag Helper de Anchor genera de manera dinámica el valor del atributo HTML href a partir del método de acción del controlador y el id de ruta. Puedes usar la opción Ver código fuente de tu navegador favorito o utilizar las herramientas de desarrollo para examinar el código generado. Una porción de ese código se muestra a continuación:

<td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>

Recordemos el formato para las rutas en el archivo Startup.cs

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

ASP.NET Core traduce http://localhost:1234/Movies/Edit/4 en una solicitud para el método de acción Edit del controller Movies con el parámetro de id con un valor de 4. (Los métodos del controlador tambien se conocen como métodos de acción.)

Los Tag Helpers son una de las características más importantes de ASP.NET Core. Puedes consultar los recursos adicionales al final de la página para aprender más de ellos.

Abramos el controller Moviesy examinemos los dos métodos de acción Edit. El código siguiente muestra el método HTTP GET Edit, el cual obtiene la película y utiliza sus datos para llenar el formulario de edición generado por el archivo de Razor Edit.cshtml.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

El siguiente código muestra el método HTTP POST Edit, el cual procesa el formulario posteado con los valores para la película:

// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}

El atributo [Bind] es una manera de protegerse contra el sobreposteo. Solamente deberíamos incluir en este atributo las propiedades que queremos que se puedan cambiar al editar. Puedes leer más información sobre esto en los siguientes artículos en inglés:

Notemos que el segundo método de acción Edit es precedido por el atributo [HttpPost]:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

El atributo [HttpPost] especifica que este método Edit solamente puede ser invocado por solicitudes POST. Puedes aplicar el atributo [HttpGet] al primer método Edit pero esto no es necesario porque [HttpGet] es el comportamiento por defecto.

El atributo [ValidateAntiForgeryToken] se usa para prevenir la falsificación de solicitudes y va emparejado con un token anti-falsificaciones que se genera en el archivo de vista edit (Views/Movies/Edit.cshtml). El archivo de vista genera el token anti-falsificaciones con la ayuda del Tag Helper de Formulario.

<form asp-action="Edit">

El Tag Helper de formulario genera un token anti-falsificación oculto que debe coincidir con el token generado por el atributo [ValidateAntiForgeryToken] en el método Edit del controller `Movies. Para más información, ver Anti-Request Forgery (en inglés).

El método HttpGet Edit toma el parámetro Id, lo busca en las películas utilizando el método de Entity Framework SingleOrDefaultAsync, y retorna la película seleccionada a la vista Edit. Si una película no se encuentra, se retorna NotFound (HTTP 404).

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}

Cuando el sistema de scaffolding creó la vista Edit, examinó la clase Movie y creó el código para generar los elementos <label> y <input> para cada propiedad de la clase. El siguiente ejemplo muestra la vista Edit que fue generada por el sistema de Scaffolding de Visual Studio:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<form asp-action="Edit">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <input type="hidden" asp-for="ID" />
        <div class="form-group">
            <label asp-for="Title" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Genre" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Price" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
        </div>
        <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>
</form>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Notemos como la plantilla de vista tiene una sentencia @model MvcMovie.Models.Movie en la parte de arriba del archivo. @model MvcMovie.Models.Movie especifica que la vista espera que el modelo para esta plantilla sea del tipo Movie.

El código de scaffolding utiliza muchos métodos Tag Helpers para aligerar el HTML generado. El Tag Helper de Label muestra el nombre del campo (“Title”, “ReleaseDate”, “Genre” o “Price”). Si tenemos un atributo [DisplayName] va a tomar lo que ahí esté escrito y mostrarlo. El Tag Helper de Input genera un elemento HTML <input>. El Tag Helper de Validacion muestra cualquier mensaje de validación asociado con esa propiedad, por ejemplo si es requerida y nos olvidamos de escribir su valor mostrará “Is required”.

Ejecutemos la aplicación y naveguemos a la URL /Movies. Hagamos click en el enlace Edit. En el navegador, veamos el código fuente de la página. El código HTML generado para el elemento <form> se muestra a continuación:

<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" 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" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup 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>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>

Procesando la solicitud POST

A continuación se muestra la versión [HttpPost] del método de acción Edit:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

El atributo [ValidateAntiForgeryToken] valida el token XSRF generado por el generador de tokens anti-falsificación en el Tag Helper de Formulario.

El sistema de model binding toma los valores posteados por el formulario y crea un objeto Movie con esos valores que se le pasa al método como el parámetro movie. El método ModelState.IsValid verifica que los datos enviados por el formulario son válidos para modificar (editar o actualizar) un objeto de tipo Movie. Si los datos son válidos son guardados. La pelicula actualizada (editada) se guarda en la base de datos llamando al método SaveChangesAsync() que está en el DbContext. Después de guardar los datos, el código redirecciona al usuario al método de acción Index de la clase MovieController, lo cual muestra la colección de películas, incluyendo los cambios que acabamos de hacer.

Antes de que el formulario sea posteado al servidor, la validación del lado del cliente verifica todas las reglas de validación en los campos. Si hay algun error de validación, se muestra el mensaje de error y el formulario no se postea. Si JavaScript está desactivado en el navegador, no vamos a tener validación del lado del cliente pero el servidor va a detectar que los valores posteados no son válidos y se va a volver a mostrar el formulario con sus valores y con mensajes de error. Más adelante en esta serie de tutoriales vamos a examinar la validación de modelos con más detalles.

El Tag Helper de Validación en la plantilla de vista Views/Models/Edit.cshtml se toma el trabajo de mostrar los mensajes de error apropiados.

El formulario de películas con sus errores de validación

Todos los métodos HttpGet en el controlador de películas siguen un patrón similar. Obtienen un objeto Movie (o una lista de objetos en el caso de Index), y pasan ese objeto (modelo) a la vista. El método Create pasa un objeto Movie vacío a la vista Create. Todos los métodos que crean, editan, borran o modifican datos lo hacen en la sobrecarga del método marcada con el atributo [HttpPost]. Modificar datos en un método HTTP GET es un problema de seguridad. Modificar datos en un método HTTP GET también viola las mejores prácticas de uso del protocolo HTTP y del patrón arquitectural REST, el cual especifica que las solicitudes GET no deberían cambiar el estado de tu aplicación. En otras palabras, hacer una operación Get debería ser una operación segura, que no tenga efectos secundarios y no modifique nuestros datos persistentes.

Vamos a ver como hacer una búsqueda de películas utilizando controles de la UI y parámetros de la URL en la siguiente parte del tutorial.