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:

Películas 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:

Comando rename

Los destinos del renombrado se van a resaltar:

Comando rename searchString resaltado

Cambiamos el parámetro a id y todas las ocurrencias de searchString pasan a cambiar a id.

Comando rename cambiado 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 Indexactualizado 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.

Películas filtradas usando segmento de ruta

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:

Filtro en la UI

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:

Método de acción Index version POST en el navegador

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:

Herramientas de desarrollo, solicitud POST

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:

Intellisense tags del formulario

Intellisense métodos del formulario

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.

Búsqueda de películas usando GET

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.