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.
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 Movies
y movamos el mouse sobre el link
Edit para ver la dirección URL de destino.
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 Movies
y 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:
- Protect your controller
- ViewModels que proveen una alternativa para prevenir el sobreposteo.
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.
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.