Agregando Búsqueda a una aplicación ASP.NET Core MVC
En esta sección vamos a agregar capacidades de búsqueda al método de
acción Index
que te permite buscar películas por género o nombre.
Actualicemos el Index
con el siguiente código:
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
La priemra línea del método de acción Index
crea una consulta
LINQ
para seleccionar la películas:
var movies = from m in _context.Movie
select m;
Esta consulta solo está definida en este punto, no ha sido ejecutada contra la base.
Si el parámetro searchString
contiene una cadena de texto,
la consulta de películas es modificada para filtrar en el valor
de esa cadena de texto:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
Eso que dice s => s.Title.Contains()
en el código de más arriba es una
Expresión Lambda. Las lambdas se usan en
consultas de LINQ basadas en métodos como argumentos a métodos
como el Where
o el Contains
(usado en el código de arriba).
Las consultas de LINQ que se hacen a Entity Framework no se ejecutan cuando son definidas o cuando son
modificadas llamando a un método como Where
, Contains
o OrderBy
.
En lugar de eso la ejecución de la consulta es diferida. Esto significa
que la evaluación de una expresión se retrasa hasta que alguién llama
una expresión de iteración sobre la misma (foreach por ejemplo)
o se llama al método ToListAsync
. Para más información ver
Query Execution(en inglés)
Nota: El método Contains se va a ejecutar en la base de datos, no en el código C# mostrado arriba. La sensibilidad a las mayúsculas va a depender de la base de datos y la intercalación (collation) de la base de datos. En SQL Server, Contains se mapea al SQL LIKE que es insensible a las mayúsculas. En SQLlite, con la intercalación por defecto es sensible a las mayúsculas.
Naveguemos hasta /Movies/Index
. Si le agregamos una query string tal
como ?searchString=Ghost
a la URL, las películas se van a ver
filtradas:
Si cambiamos la firma del método Index
para que tenga un parámetro
id
va a coincidir con el parámetro opcional que definimos en nuestras
rutas por defecto en el archivo Startup.cs:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Podemos renombrar rapidamente el parámetro searchString
a Id
con el comando renombrar de Visual Studio.
Hagamos click derecho en searchString
y luego > Renombrar:
Los destinos del renombrado se van a resaltar:
Cambiamos el parámetro a id
y todas las ocurrencias de searchString
pasan a cambiar a id
.
El método Index
anterior:
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
El método Index
actualizado con el parámetro id
:
public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
return View(await movies.ToListAsync());
}
Ahora podemos pasar el título como parte de los datos de ruta (un segmento de la URL) en lugar de una query string.
Sin embargo, no podemos esperar que los usuarios modifiquen la URL
cada vez que quieran buscar una película. Entonces ahora vamos a
agregar elementos en la UI para ayudarlos a filtrar esas películas.
Si ya hemos cambiado la firma del método Index
para probar
como pasar un parámetro de ruta ID
, cambiemoslo de nuevo
para que se llame searchString
:
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Abramos el archivo Views/Movies/Index.cshtml, y agreguemos
la parte de <form>
que se muestra a continuación:
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
La etiqueta HTML <form>
utiliza el
Tag Helper de Formulario, para que cuando enviemos el formulario,
la cadena de filtro sea posteada al método de acción Index
de nuestro controlador de películas. Guardemos los cambios
y probemos el filtro:
No hay una sobrecarga [HttpPost]
para el método Index
como
podríamos esperar. No la necesitamos, porque el método no está cambiando
el estado de nuestra aplicación, solo filtrando datos para su
visualización.
Podemos agregar la sobrecarga [HttpPost] Index
de la siguiente manera:
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
El parámetro notUsed
se usa para crear una sobrecarga
al método Index
. Hablaremos de esto más tarde en el tutorial.
Si agregamos este método, el invocador de acción va a coincidir
con el método [HttpPost Index]
y cuando hagamos una búsqueda va
a ejecutarse como la siguiente imagen:
Sin embargo, aún cuando agreguemos esta versión de HttpPost
del
método Index
, hay una limitación en esta implementación.
Imaginemos que queremos agregar a favoritos / marcadores
una búsqueda en particular o que le queremos enviar un enlace
a nuestros amigos que ellos puedan hacer click para ver la misma
lista filtrada de películas. Notemos que la URL para la solicitud
HTTP POST
es la misma que la de la solicitud GET
(localhost:xxxx/Movies/Index). No hay información
de búsqueda en la URL. La información de búsqueda se
envía al servidor como un
campo del formulario.
Podemos verificar esto viendo las herramientas de desarrollo del navegador
o con el excelente Fiddler.
La imagen debajo muestra las herramientas de desarrollo de Chrome:
Podemos el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Notemos, como se menciona en la parte anterior del tutorial, el Tag Helper de Formulario genera un token anti-falsificación XSRF. No estamos modificando datos, asi que no necesitamos validar ese token en el método del controlador.
Porque el parámetor de búsqueda está en el cuerpo de la solicitud en lugar
de la URL, no podemos capturar la información de búsqueda o compartirla con otros. Vamos a arreglar esto especificando que la solicitud debe ser
HTTP GET
.
Notemos como el IntelliSense nos ayuda a actualizar el código HTML:
Notemos la fuente distintiva en el <form>
(en color violeta)
esto significa que esa etiqueta HTML puede utilizar
Tag Helpers
Ahora cuando mandemos la búsqueda, la URL va a contener la
cadena de búsqueda. La búsqueda va a ir por el método de acción
HttpGet Index
, aún cuando tengamos un método HttpPost Index
.
A continuación mostramos el cambio el tag de formulario:
<form asp-controller="Movies" asp-action="Index" method="get">
Agregando la búsqueda por género
Agreguemos la siguiente clase MovieGenreViewModel
al directorio
Models:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> movies;
public SelectList genres;
public string movieGenre { get; set; }
}
}
El género de una película va a contener:
- Una lista de películas
- Una
SelectList
conteniendo la lista de géneros. Esto le va a permitir al usuario seleccionar un género de la lista. - La propiedad
movieGenre
, que contiene al género seleccionado actualmente.
Reemplacemos el método Index
en MovieController.cs
con el siguiente código:
// Requires using Microsoft.AspNetCore.Mvc.Rendering;
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel();
movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
movieGenreVM.movies = await movies.ToListAsync();
return View(movieGenreVM);
}
El siguiente código es una consulta LINQ
que trae todos los
géneros de la base de datos:
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
La SelectList
de todos los géneros se crea proyectando
los géneros distintos (no queremos que nuestra lista tenga
géneros duplicados).
movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync())
Agregando la búsqueda por género a la vista de Index
Actualicemos Index.cshtml
de la manera siguiente:
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="movieGenre" asp-items="Model.genres">
<option value="">All</option>
</select>
Title: <input type="text" name="SearchString">
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.movies)
{
<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>
<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>
}
</tbody>
</table>
Examinemos la expresión lambda usada por el Helper HTML:
@Html.DisplayNameFor(model => model.movies[0].Title)
En el código anterior, el Helper HTML DisplayNameFor
examina
la propieda Title
referenciada en la expresión lambda para
determinar el nombre para mostrar. Dado que la expresión
lambda es examinada en lugar de evaluada, no recibimos
una NullReferenceException cuando model
, model.movies
o
model.movies[0]
son null
o vacíos. Cuando la expresión
lambda es evaluada (por ejemplo,
@Html.DisplayFor(modelItem => item.Title)
), los valores
de la propiedad del modelo tambien son evaluados.
Podemos probar la app búscando por género, por título, o ambos.
Vamos a ver como agregar un campo nuevo a nuestro modelo en la siguiente parte del tutorial.