Examen de los métodos de acción y las vistas para el controlador Movie
por Rick Anderson
Nota:
Hay disponible aquí una versión actualizada de este tutorial que 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, examinará los métodos de acción y las vistas para el controlador Movie. Después, agregará una página de búsqueda personalizada.
Ejecute la aplicación y vaya al controlador Movies
; para ello, anexe /Movies a la URL en la barra de direcciones del explorador. Mantenga presionado el puntero del mouse sobre un vínculo Editar para ver la dirección URL a la que se vincula.
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 })
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. El último argumento es un objeto anónimo que genera los datos de ruta (en este caso, el id. 4).
El vínculo generado que se muestra en la imagen anterior es http://localhost:xxxxx/Movies/Edit/4
. La ruta predeterminada (establecida en App_Start\RouteConfig.cs) toma el patrón de URL {controller}/{action}/{id}
. Por tanto, ASP.NET traduce http://localhost:xxxxx/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.
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:xxxxx/Movies/Edit?ID=4
también pasa el parámetro ID
de 4 al método de acción Edit
del controlador Movies
.
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 = 0)
{
Movie movie = db.Movies.Find(id);
if (movie == null)
{
return HttpNotFound();
}
return View(movie);
}
//
// POST: /Movies/Edit/5
[HttpPost]
public ActionResult Edit(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. (Los métodos de acción asignados implícitamente al atributo HttpGet
se denominarán métodos HttpGet
).
El método HttpGet
Edit
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. El parámetro ID especifica un valor predeterminado de cero si se llama al método Edit
sin un parámetro. 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 Editar que se ha generado:
@model MvcMovie.Models.Movie
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
<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>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<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; 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 de 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 <input>
HTML. 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"> <fieldset>
<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="editor-label">
<label for="Title">Title</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Title" name="Title" type="text" value="Rio Bravo" />
<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="ReleaseDate">ReleaseDate</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-date="The field ReleaseDate must be a date." data-val-required="The ReleaseDate field is required." id="ReleaseDate" name="ReleaseDate" type="text" value="4/15/1959 12:00:00 AM" />
<span class="field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Genre">Genre</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Genre" name="Genre" type="text" value="Western" />
<span class="field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Price">Price</label>
</div>
<div class="editor-field">
<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="2.99" />
<span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Save" />
</p>
</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 Editar.
Procesamiento de la solicitud POST
En la siguiente lista se muestra la versión HttpPost
del método de acción Edit
.
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
El enlazador de modelos de ASP.NET MVC 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 la instancia de db(MovieDBContext
. Los nuevos datos de película se guardan en la base de datos mediante una llamada 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.
Si los valores publicados no son válidos, se vuelven a mostrar en el formulario. Los asistentes Html.ValidationMessageFor
de la plantilla de vista Edit.cshtml se encargan de mostrar los mensajes de error adecuados.
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>
}
El campo decimal puede requerir una coma, no un separador decimal. Como corrección temporal, puede agregar el elemento de globalización al archivo web.config raíz del proyecto. En el código siguiente se 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>
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 modelo a la vista. El método Create
pasa un objeto de película vacío a la vista Create. 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. 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.
Adición de un método de búsqueda y una vista de búsqueda
En esta sección agregará un método de acción SearchIndex
que permite buscar películas por género o nombre. Esto estará disponible mediante la dirección URL /Movies/SearchIndex. La solicitud mostrará un formulario HTML que contiene elementos de entrada que un usuario puede escribir para buscar una película. Cuando un usuario envía el formulario, el método de acción obtendrá los valores de búsqueda publicados por el usuario y los utilizará para buscar en la base de datos.
Representación del formulario SearchIndex
Para empezar, agregue un método de acción SearchIndex
a la clase MoviesController
existente. El método devolverá una vista que contiene un formulario HTML. Este es el código :
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
La primera línea del método SearchIndex
crea una consulta LINQ para seleccionar las películas:
var movies = from m in db.Movies
select m;
Ya se ha definido la consulta, pero aún no se ha ejecutado en la base de datos.
Si el parámetro searchString
contiene una cadena, la consulta de películas se modifica para filtrar por el valor de la cadena de búsqueda, con el código siguiente:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
El código s => s.Title
anterior es una expresión Lambda. En las consultas LINQ basadas en métodos se usan expresiones lambda como argumentos para métodos de operador de consulta estándar, como el método Where del código anterior. Las consultas LINQ no se ejecutan cuando se definen o se modifican mediante una llamada a un método, como Where
o OrderBy
. En su lugar, la ejecución de la consulta se aplaza, lo que significa que la evaluación de una expresión se retrasa hasta que se itera realmente por su valor realizado o se llama al método ToList
. En el ejemplo SearchIndex
, la consulta se ejecuta en la vista SearchIndex. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.
Ahora puede implementar la vista SearchIndex
que mostrará el formulario al usuario. Haga clic con el botón derecho en el método SearchIndex
y,después, haga clic en Agregar vista. En el cuadro de diálogo Agregar vista, especifique que va a pasar un objeto Movie
a la plantilla de vista como su clase de modelo. En la lista Plantilla de scaffolding, elija Lista y, después, haga clic en Agregar.
Al hacer clic en el botón Agregar, se crea la plantilla de vista Views\Movies\SearchIndex.cshtml. Como ha seleccionado Lista en la lista Plantilla de scaffolding, Visual Studio ha generado automáticamente (con scaffolding) contenido predeterminado en la vista. El scaffolding ha creado un formulario HTML. Ha examinado la clase Movie
y ha creado código para representar elementos <label>
para cada propiedad de la clase. En la lista siguiente se muestra la vista Create que se ha generado:
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
Title
</th>
<th>
ReleaseDate
</th>
<th>
Genre
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
@Html.ActionLink("Details", "Details", new { id=item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
Ejecute la aplicación y vaya a /Movies/SearchIndex. Anexe una cadena de consulta como ?searchString=ghost
a la dirección URL. Se muestran las películas filtradas.
Si se cambia la firma del método SearchIndex
para que tenga un parámetro con el nombre id
, el parámetro id
coincidirá con el marcador de posición {id}
para el conjunto de rutas predeterminado establecido en el archivo Global.asax.
{controller}/{action}/{id}
El método original SearchIndex
tiene el siguiente aspecto:
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
El método modificado SearchIndex
tendría el siguiente aspecto:
public ActionResult SearchIndex(string id)
{
string searchString = id;
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
Ahora puede pasar el título de la búsqueda como datos de ruta (un segmento de dirección URL) en lugar de como un valor de cadena de consulta.
Sin embargo, no se puede esperar que los usuarios modifiquen la dirección URL cada vez que quieran buscar una película. Por tanto, ahora agregará la interfaz de usuario con la que podrán filtrar las películas. Si ha cambiado la firma del método SearchIndex
para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarla para que el método SearchIndex
tome un parámetro de cadena denominado searchString
:
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
Abra el archivo Views\Movies\SearchIndex.cshtml y, justo después de @Html.ActionLink("Create New", "Create")
, agregue lo siguiente:
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString")<br />
<input type="submit" value="Filter" /></p>
}
En el ejemplo siguiente se muestra una parte del archivo Views\Movies\SearchIndex.cshtml con el marcado de filtrado agregado.
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString") <br />
<input type="submit" value="Filter" /></p>
}
</p>
El asistente Html.BeginForm
crea una etiqueta <form>
de apertura. El asistente Html.BeginForm
hace que el formulario se publique cuando el usuario envía el formulario al hacer clic en el botón Filter.
Ejecute la aplicación e intente buscar una película.
No hay ninguna sobrecarga HttpPost
del método SearchIndex
. No es necesario, porque el método no cambia el estado de la aplicación, simplemente filtra los datos.
Después, puede agregar el método HttpPost SearchIndex
siguiente. En ese caso, el invocador de acción coincidiría con el método HttpPost SearchIndex
, mientras que el método HttpPost SearchIndex
se ejecutaría como se muestra en la imagen siguiente.
[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}
Sin embargo, aunque agregue esta versión de HttpPost
al método SearchIndex
, hay una limitación en cómo se ha implementado todo esto. Supongamos que quiere marcar una búsqueda en particular o que quiere enviar un vínculo a sus amigos donde puedan hacer clic para ver la misma lista filtrada de películas. Observe que la dirección URL de la solicitud HTTP POST es la misma que la de la solicitud GET (localhost:xxxxx/Movies/SearchIndex); no hay información de búsqueda en la propia URL. En este momento, la información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Esto significa que no se puede capturar esa información de búsqueda para marcar o enviar a amigos en una dirección URL.
La solución consiste en usar una sobrecarga de BeginForm
que especifica que la solicitud POST debe agregar la información de búsqueda a la URL y que se debe enrutar a la versión HttpGet del método SearchIndex
. Reemplace el método BeginForm
sin parámetros existente por lo siguiente:
@using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get))
Ahora, cuando se envía una búsqueda, la URL contiene la cadena de consulta de búsqueda. La búsqueda también será dirigida al método de acción HttpGet SearchIndex
, aunque tenga un método HttpPost SearchIndex
.
Agregar búsqueda por género
Si ha agregado la versión HttpPost
del método SearchIndex
, elimínela ahora.
A continuación, agregará una característica para permitir que los usuarios busquen películas por género. Reemplace el método SearchIndex
con el código siguiente:
public ActionResult SearchIndex(string movieGenre, string searchString)
{
var GenreLst = new List<string>();
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
GenreLst.AddRange(GenreQry.Distinct());
ViewBag.movieGenre = new SelectList(GenreLst);
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
}
Esta versión del método SearchIndex
toma un parámetro adicional, movieGenre
. Las primeras líneas de código crean un objeto List
para contener géneros de películas de la base de datos.
El código siguiente es una consulta LINQ que recupera todos los géneros de la base de datos.
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
El código usa el método AddRange
de la colección List
genérica para agregar todos los géneros distintos a la lista. (Sin el modificador Distinct
, se agregarían géneros duplicados; por ejemplo, las comedias se agregarían dos veces en este ejemplo). Después, el código almacena la lista de géneros en el objeto ViewBag
.
En el código siguiente se muestra cómo comprobar el parámetro movieGenre
. Si no está vacío, el código restringe aún más la consulta de películas para limitar las seleccionadas al género especificado.
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
Adición de marcado a la vista SearchIndex para admitir la búsqueda por género
Agregue un asistente Html.DropDownList
al archivo Views\Movies\SearchIndex.cshtml, justo antes del asistente TextBox
. A continuación se muestra el marcado completado:
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get)){
<p>Genre: @Html.DropDownList("movieGenre", "All")
Title: @Html.TextBox("SearchString")
<input type="submit" value="Filter" /></p>
}
</p>
Ejecute la aplicación y vaya a /Movies/SearchIndex. Pruebe una búsqueda por género, por nombre de película y por ambos criterios.
En esta sección ha examinado los métodos de acción CRUD y las vistas generadas por el marco de trabajo. Ha creado un método de acción de búsqueda y una vista que permite a los usuarios buscar por título de película y por género. En la sección siguiente, verá cómo agregar una propiedad al modelo Movie
y cómo agregar un inicializador que creará automáticamente una base de datos de prueba.