Compartir vía


Parte 7. Adición de búsqueda a una aplicación de ASP.NET Core MVC

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

Por Rick Anderson

En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas por género o nombre.

Actualice el método Index, que está en Controllers/MoviesController.cs, con el código siguiente:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

La siguiente línea del método de acción de Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie
             select m;

En este momento, solo se define la consulta y no se ejecuta en la base de datos.

Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}

El código s => s.Title!.ToUpper().Contains(searchString.ToUpper()) anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ basadas en métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains (que se usa en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican mediante una llamada a un método como Where, Contains u OrderBy. En su lugar, se aplaza la ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repita realmente o se llame al método ToListAsync. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.

Nota:

El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. SQLite con la intercalación predeterminada es una combinación que distingue mayúsculas y minúsculas y que no distingue mayúsculas y minúsculas, en función de la consulta. Para obtener información sobre cómo hacer que las consultas SQLite no distingan mayúsculas y minúsculas, vea lo siguiente:

Vaya a /Movies/Index. Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se muestran las películas filtradas.

Vista de índice

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id, el parámetro id coincidirá con el marcador de posición {id} opcional para el conjunto de rutas predeterminado en Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Cambie el parámetro por id y todas las apariciones de searchString por id.

El método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

El método Index actualizado con el parámetro id:

public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

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.

Vista de índice con la palabra Ghost (Fantasma) agregada a la dirección URL y una lista de las películas obtenidas, con dos películas, Ghostbusters y Ghostbusters 2

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 deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarlo para que tome un parámetro denominado searchString:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

La etiqueta HTML <form> usa el asistente de etiquetas de formulario, por lo que cuando se envía el formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y después pruebe el filtro.

Vista de índice con la palabra Ghost (Fantasma) escrita en el cuadro de texto del filtro Title (Título)

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. 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] Index siguiente.

[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 para el método Index. Hablaremos sobre esto más adelante en el tutorial.

Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index, mientras que el método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Ventana del explorador con la respuesta: From HttpPost Index: filter on ghost (Desde el índice HttpPost: filtrar en Ghost)

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index, 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. Tenga en cuenta que la dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET (localhost:{PUERTO}/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas de desarrollo del explorador o con la excelente herramienta Fiddler.

En la siguiente imagen se muestran las herramientas para desarrolladores del explorador Chrome con las pestañas Red y Encabezados seleccionadas:

Pestañas Red y Encabezados de las herramientas de desarrollo del explorador Chrome que muestran un cuerpo de solicitud con un valor searchString de fantasma

Las pestañas Red y Carga se seleccionan para ver los datos del formulario:

Pestañas de red y carga de las herramientas de desarrollo del explorador Chrome que muestran los datos del formulario

Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Ten en cuenta, como se mencionó en el tutorial anterior, que el asistente de etiquetas de formulario genera un token XSRF antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.

El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede capturar dicha información para marcarla o compartirla con otros usuarios. Corrija esto especificando que la solicitud debe ser HTTP GET en la etiqueta form que se encuentra en el archivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

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 Index, aunque tenga un método HttpPost Index.

Ventana del explorador que muestra searchString=ghost en la URL y las películas que se devuelven, Ghostbusters y Ghostbusters 2, que contienen la palabra Ghost (Fantasma)

Adición de búsqueda por género

Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}

El modelo de vista de película y género contendrá:

  • Una lista de películas.
  • SelectList, que contiene la lista de géneros. Esto permite al usuario seleccionar un género de la lista.
  • MovieGenre, que contiene el género seleccionado.
  • SearchString, que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // 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!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera 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 géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista de selección tenga géneros duplicados).

Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice

Actualice Index.cshtml, que está en Views/Movies/ , siguiendo estos pasos:

@model MvcMovie.Models.MovieGenreViewModel

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

<h1>Index</h1>

<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>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <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>

Examine la expresión lambda usada en el siguiente asistente de HTML:

@Html.DisplayNameFor(model => model.Movies![0].Title)

En el código anterior, el asistente de HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model, model.Movies o model.Movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo, @Html.DisplayFor(modelItem => item.Title)), se evalúan los valores de propiedad del modelo. El ! después de model.Movies es un operador que determina valores null, que se usa para declarar que Movies no es null.

Pruebe la aplicación buscando por género, por título de la película y por ambos:

Ventana del explorador en la que se muestran los resultados de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas por género o nombre.

Actualice el método Index, que está en Controllers/MoviesController.cs, con el código siguiente:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

La siguiente línea del método de acción de Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie
             select m;

En este momento, solo se define la consulta y no se ejecuta en la base de datos.

Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}

El código s => s.Title!.ToUpper().Contains(searchString.ToUpper()) anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ basadas en métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains (que se usa en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican mediante una llamada a un método como Where, Contains u OrderBy. En su lugar, se aplaza la ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repita realmente o se llame al método ToListAsync. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.

Nota:

El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. SQLite con la intercalación predeterminada es una combinación que distingue mayúsculas y minúsculas y que no distingue mayúsculas y minúsculas, en función de la consulta. Para obtener información sobre cómo hacer que las consultas SQLite no distingan mayúsculas y minúsculas, vea lo siguiente:

Vaya a /Movies/Index. Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se muestran las películas filtradas.

Vista de índice

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id, el parámetro id coincidirá con el marcador de posición {id} opcional para el conjunto de rutas predeterminado en Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Cambie el parámetro por id y todas las apariciones de searchString por id.

El método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

El método Index actualizado con el parámetro id:

public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

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.

Vista de índice con la palabra Ghost (Fantasma) agregada a la dirección URL y una lista de las películas obtenidas, con dos películas, Ghostbusters y Ghostbusters 2

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 deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarlo para que tome un parámetro denominado searchString:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

La etiqueta HTML <form> usa el asistente de etiquetas de formulario, por lo que cuando se envía el formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y después pruebe el filtro.

Vista de índice con la palabra Ghost (Fantasma) escrita en el cuadro de texto del filtro Title (Título)

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. 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] Index siguiente.

[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 para el método Index. Hablaremos sobre esto más adelante en el tutorial.

Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index, mientras que el método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Ventana del explorador con la respuesta: From HttpPost Index: filter on ghost (Desde el índice HttpPost: filtrar en Ghost)

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index, 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. Tenga en cuenta que la dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET (localhost:{PUERTO}/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las herramientas de desarrollo del explorador Chrome:

Pestaña Red de las Herramientas de desarrollo de Microsoft Edge en la que se muestra un cuerpo de la solicitud con un valor searchString de la palabra Ghost

Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Ten en cuenta, como se mencionó en el tutorial anterior, que el asistente de etiquetas de formulario genera un token XSRF antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.

El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede capturar dicha información para marcarla o compartirla con otros usuarios. Para corregir este problema, especifique que la solicitud debe ser HTTP GET, que está en el archivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

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 Index, aunque tenga un método HttpPost Index.

Ventana del explorador que muestra searchString=ghost en la URL y las películas que se devuelven, Ghostbusters y Ghostbusters 2, que contienen la palabra Ghost (Fantasma)

El marcado siguiente muestra el cambio en la etiqueta form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adición de búsqueda por género

Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}

El modelo de vista de película y género contendrá:

  • Una lista de películas.
  • SelectList, que contiene la lista de géneros. Esto permite al usuario seleccionar un género de la lista.
  • MovieGenre, que contiene el género seleccionado.
  • SearchString, que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // 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!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera 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 géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista de selección tenga géneros duplicados).

Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice

Actualice Index.cshtml, que está en Views/Movies/ , siguiendo estos pasos:

@model MvcMovie.Models.MovieGenreViewModel

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

<h1>Index</h1>

<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>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <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>

Examine la expresión lambda usada en el siguiente asistente de HTML:

@Html.DisplayNameFor(model => model.Movies![0].Title)

En el código anterior, el asistente de HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model, model.Movies o model.Movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo, @Html.DisplayFor(modelItem => item.Title)), se evalúan los valores de propiedad del modelo. El ! después de model.Movies es un operador que determina valores null, que se usa para declarar que Movies no es null.

Pruebe la aplicación buscando por género, por título de la película y por ambos:

Ventana del explorador en la que se muestran los resultados de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas por género o nombre.

Actualice el método Index, que está en Controllers/MoviesController.cs, con el código siguiente:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

La siguiente línea del método de acción de Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie
             select m;

En este momento, solo se define la consulta y no se ejecuta en la base de datos.

Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}

El código s => s.Title!.ToUpper().Contains(searchString.ToUpper()) anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ basadas en métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains (que se usa en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican mediante una llamada a un método como Where, Contains u OrderBy. En su lugar, se aplaza la ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repita realmente o se llame al método ToListAsync. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.

Nota:

El método Contains se ejecuta en la base de datos, no en el código de C#. La distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. SQLite con la intercalación predeterminada es una combinación que distingue mayúsculas y minúsculas y que no distingue mayúsculas y minúsculas, en función de la consulta. Para obtener información sobre cómo hacer que las consultas SQLite no distingan mayúsculas y minúsculas, vea lo siguiente:

Vaya a /Movies/Index. Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se muestran las películas filtradas.

Vista de índice

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id, el parámetro id coincidirá con el marcador de posición {id} opcional para el conjunto de rutas predeterminado en Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Cambie el parámetro por id y todas las apariciones de searchString por id.

El método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

El método Index actualizado con el parámetro id:

public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

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.

Vista de índice con la palabra Ghost (Fantasma) agregada a la dirección URL y una lista de las películas obtenidas, con dos películas, Ghostbusters y Ghostbusters 2

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 deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarlo para que tome un parámetro denominado searchString:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

La etiqueta HTML <form> usa el asistente de etiquetas de formulario, por lo que cuando se envía el formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y después pruebe el filtro.

Vista de índice con la palabra Ghost (Fantasma) escrita en el cuadro de texto del filtro Title (Título)

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. 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] Index siguiente.

[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 para el método Index. Hablaremos sobre esto más adelante en el tutorial.

Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index, mientras que el método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Ventana del explorador con la respuesta: From HttpPost Index: filter on ghost (Desde el índice HttpPost: filtrar en Ghost)

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index, 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. Tenga en cuenta que la dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET (localhost:{PUERTO}/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las herramientas de desarrollo del explorador Chrome:

Pestaña Red de las Herramientas de desarrollo de Microsoft Edge en la que se muestra un cuerpo de la solicitud con un valor searchString de la palabra Ghost

Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Ten en cuenta, como se mencionó en el tutorial anterior, que el asistente de etiquetas de formulario genera un token XSRF antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.

El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede capturar dicha información para marcarla o compartirla con otros usuarios. Para corregir este problema, especifique que la solicitud debe ser HTTP GET, que está en el archivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

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 Index, aunque tenga un método HttpPost Index.

Ventana del explorador que muestra searchString=ghost en la URL y las películas que se devuelven, Ghostbusters y Ghostbusters 2, que contienen la palabra Ghost (Fantasma)

El marcado siguiente muestra el cambio en la etiqueta form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adición de búsqueda por género

Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}

El modelo de vista de película y género contendrá:

  • Una lista de películas.
  • SelectList, que contiene la lista de géneros. Esto permite al usuario seleccionar un género de la lista.
  • MovieGenre, que contiene el género seleccionado.
  • SearchString, que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // 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!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera 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 géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista de selección tenga géneros duplicados).

Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice

Actualice Index.cshtml, que está en Views/Movies/ , siguiendo estos pasos:

@model MvcMovie.Models.MovieGenreViewModel

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

<h1>Index</h1>

<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>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <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>

Examine la expresión lambda usada en el siguiente asistente de HTML:

@Html.DisplayNameFor(model => model.Movies![0].Title)

En el código anterior, el asistente de HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model, model.Movies o model.Movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo, @Html.DisplayFor(modelItem => item.Title)), se evalúan los valores de propiedad del modelo. El ! después de model.Movies es un operador que determina valores null, que se usa para declarar que Movies no es null.

Pruebe la aplicación buscando por género, por título de la película y por ambos:

Ventana del explorador en la que se muestran los resultados de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas por género o nombre.

Actualice el método Index, que está en Controllers/MoviesController.cs, con el código siguiente:

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 primera línea del método de acción Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie
             select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.

Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.Contains(searchString));
}

El código s => s.Title!.Contains(searchString) anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ basadas en métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains (que se usa en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican mediante una llamada a un método como Where, Contains u OrderBy. En su lugar, se aplaza la ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repita realmente o se llame al método ToListAsync. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.

Nota: El método Contains se ejecuta en la base de datos, no en el código de c# que se muestra arriba. La distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la intercalación predeterminada, se distingue entre mayúsculas y minúsculas.

Navegue a /Movies/Index. Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se muestran las películas filtradas.

Vista de índice

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id, el parámetro id coincidirá con el marcador de posición {id} opcional para el conjunto de rutas predeterminado en Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Cambie el parámetro por id y todas las apariciones de searchString por 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 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.

Vista de índice con la palabra Ghost (Fantasma) agregada a la dirección URL y una lista de las películas obtenidas, con dos películas, Ghostbusters y Ghostbusters 2

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 deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarlo para que tome un parámetro denominado 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());
}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado a continuación:

@model IEnumerable<MvcMovie.Models.Movie>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

La etiqueta HTML <form> usa el asistente de etiquetas de formulario, por lo que cuando se envía el formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y después pruebe el filtro.

Vista de índice con la palabra Ghost (Fantasma) escrita en el cuadro de texto del filtro Title (Título)

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. 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] Index siguiente.

[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 para el método Index. Hablaremos sobre esto más adelante en el tutorial.

Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index, mientras que el método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Ventana del explorador con la respuesta: From HttpPost Index: filter on ghost (Desde el índice HttpPost: filtrar en Ghost)

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index, 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. Tenga en cuenta que la dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET (localhost:{PUERTO}/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las herramientas de desarrollo del explorador Chrome:

Pestaña Red de las Herramientas de desarrollo de Microsoft Edge en la que se muestra un cuerpo de la solicitud con un valor searchString de la palabra Ghost

Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Ten en cuenta, como se mencionó en el tutorial anterior, que el asistente de etiquetas de formulario genera un token XSRF antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.

El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede capturar dicha información para marcarla o compartirla con otros usuarios. Para corregir este problema, especifique que la solicitud debe ser HTTP GET, que está en el archivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

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 Index, aunque tenga un método HttpPost Index.

Ventana del explorador que muestra searchString=ghost en la URL y las películas que se devuelven, Ghostbusters y Ghostbusters 2, que contienen la palabra Ghost (Fantasma)

El marcado siguiente muestra el cambio en la etiqueta form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adición de búsqueda por género

Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie>? Movies { get; set; }
        public SelectList? Genres { get; set; }
        public string? MovieGenre { get; set; }
        public string? SearchString { get; set; }
    }
}

El modelo de vista de película y género contendrá:

  • Una lista de películas.
  • SelectList, que contiene la lista de géneros. Esto permite al usuario seleccionar un género de la lista.
  • MovieGenre, que contiene el género seleccionado.
  • SearchString, que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// GET: Movies
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
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera 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 géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista de selección tenga géneros duplicados).

Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice

Actualice Index.cshtml, que está en Views/Movies/ , siguiendo estos pasos:

@model MvcMovie.Models.MovieGenreViewModel

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

<h1>Index</h1>

<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>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <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>

Examine la expresión lambda usada en el siguiente asistente de HTML:

@Html.DisplayNameFor(model => model.Movies[0].Title)

En el código anterior, el asistente de HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model, model.Movies o model.Movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo, @Html.DisplayFor(modelItem => item.Title)), se evalúan los valores de propiedad del modelo.

Pruebe la aplicación buscando por género, por título de la película y por ambos:

Ventana del explorador en la que se muestran los resultados de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

En esta sección agregará capacidad de búsqueda para el método de acción Index que permite buscar películas por género o nombre.

Actualice el método Index, que está en Controllers/MoviesController.cs, con el código siguiente:

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 primera línea del método de acción Index crea una consulta LINQ para seleccionar las películas:

var movies = from m in _context.Movie
             select m;

En este momento solo se define la consulta, no se ejecuta en la base de datos.

Si el parámetro searchString contiene una cadena, la consulta de películas se modifica para filtrar según el valor de la cadena de búsqueda:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title.Contains(searchString));
}

El código s => s.Title.Contains() anterior es una expresión Lambda. Las lambdas se usan en consultas LINQ basadas en métodos como argumentos para métodos de operador de consulta estándar, como el método Where o Contains (que se usa en el código anterior). Las consultas LINQ no se ejecutan cuando se definen ni cuando se modifican mediante una llamada a un método como Where, Contains u OrderBy. En su lugar, se aplaza la ejecución de la consulta. Esto significa que la evaluación de una expresión se aplaza hasta que su valor realizado se repita realmente o se llame al método ToListAsync. Para más información sobre la ejecución de consultas en diferido, vea Ejecución de la consulta.

Nota: El método Contains se ejecuta en la base de datos, no en el código de c# que se muestra arriba. La distinción entre mayúsculas y minúsculas en la consulta depende de la base de datos y la intercalación. En SQL Server, Contains se asigna a SQL LIKE, que distingue entre mayúsculas y minúsculas. En SQLite, con la intercalación predeterminada, se distingue entre mayúsculas y minúsculas.

Navegue a /Movies/Index. Anexe una cadena de consulta como ?searchString=Ghost a la dirección URL. Se muestran las películas filtradas.

Vista de índice

Si se cambia la firma del método Index para que tenga un parámetro con el nombre id, el parámetro id coincidirá con el marcador de posición {id} opcional para el conjunto de rutas predeterminado en Startup.cs.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

Cambie el parámetro por id y todas las apariciones de searchString se modificarán por 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 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.

Vista de índice con la palabra Ghost (Fantasma) agregada a la dirección URL y una lista de las películas obtenidas, con dos películas, Ghostbusters y Ghostbusters 2

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 deberá agregar elementos de la interfaz de usuario con los que podrán filtrar las películas. Si cambió la firma del método Index para probar cómo pasar el parámetro ID enlazado a una ruta, vuelva a cambiarlo para que tome un parámetro denominado 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());
}

Abra el archivo Views/Movies/Index.cshtml y agregue el marcado <form> resaltado 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>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>

La etiqueta HTML <form> usa el asistente de etiquetas de formulario, por lo que cuando se envía el formulario, la cadena de filtro se registra en la acción Index del controlador de películas. Guarde los cambios y después pruebe el filtro.

Vista de índice con la palabra Ghost (Fantasma) escrita en el cuadro de texto del filtro Title (Título)

No hay ninguna sobrecarga [HttpPost] del método Index como cabría esperar. 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] Index siguiente.

[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 para el método Index. Hablaremos sobre esto más adelante en el tutorial.

Si agrega este método, el invocador de acción coincidiría con el método [HttpPost] Index, mientras que el método [HttpPost] Index se ejecutaría tal como se muestra en la imagen de abajo.

Ventana del explorador con la respuesta: From HttpPost Index: filter on ghost (Desde el índice HttpPost: filtrar en Ghost)

Sin embargo, aunque agregue esta versión de [HttpPost] al método Index, 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. Tenga en cuenta que la dirección URL de la solicitud HTTP POST es la misma que la dirección URL de la solicitud GET (localhost:{PUERTO}/Movies/Index): no hay información de búsqueda en la URL. La información de la cadena de búsqueda se envía al servidor como un valor de campo de formulario. Puede comprobarlo con las herramientas de desarrollo del explorador o con la excelente herramienta Fiddler. En la imagen de abajo se muestran las herramientas de desarrollo del explorador Chrome:

Pestaña Red de las Herramientas de desarrollo de Microsoft Edge en la que se muestra un cuerpo de la solicitud con un valor searchString de la palabra Ghost

Puede ver el parámetro de búsqueda y el token XSRF en el cuerpo de la solicitud. Ten en cuenta, como se mencionó en el tutorial anterior, que el asistente de etiquetas de formulario genera un token XSRF antifalsificación. Como no se van a modificar datos, no es necesario validar el token con el método del controlador.

El parámetro de búsqueda se encuentra en el cuerpo de solicitud y no en la dirección URL. Por eso no se puede capturar dicha información para marcarla o compartirla con otros usuarios. Para corregir este problema, especifique que la solicitud debe ser HTTP GET, que está en el archivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

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

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)

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 Index, aunque tenga un método HttpPost Index.

Ventana del explorador que muestra searchString=ghost en la URL y las películas que se devuelven, Ghostbusters y Ghostbusters 2, que contienen la palabra Ghost (Fantasma)

El marcado siguiente muestra el cambio en la etiqueta form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adición de búsqueda por género

Agregue la clase MovieGenreViewModel siguiente a la carpeta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie> Movies { get; set; }
        public SelectList Genres { get; set; }
        public string MovieGenre { get; set; }
        public string SearchString { get; set; }
    }
}

El modelo de vista de película y género contendrá:

  • Una lista de películas.
  • SelectList, que contiene la lista de géneros. Esto permite al usuario seleccionar un género de la lista.
  • MovieGenre, que contiene el género seleccionado.
  • SearchString, que contiene el texto que los usuarios escriben en el cuadro de texto de búsqueda.

Reemplace el método Index en MoviesController.cs por el código siguiente:

// GET: Movies
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
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

El código siguiente es una consulta LINQ que recupera 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 géneros se crea mediante la proyección de los distintos géneros (no queremos que nuestra lista de selección tenga géneros duplicados).

Cuando el usuario busca el elemento, se conserva el valor de búsqueda en el cuadro de búsqueda.

Adición de búsqueda por género a la vista de índice

Actualice Index.cshtml, que está en Views/Movies/ , siguiendo estos pasos:

@model MvcMovie.Models.MovieGenreViewModel

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

<h1>Index</h1>

<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>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <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>

Examine la expresión lambda usada en el siguiente asistente de HTML:

@Html.DisplayNameFor(model => model.Movies[0].Title)

En el código anterior, el asistente de HTML DisplayNameFor inspecciona la propiedad Title a la que se hace referencia en la expresión lambda para determinar el nombre para mostrar. Puesto que la expresión lambda se inspecciona en lugar de evaluarse, no recibirá una infracción de acceso cuando model, model.Movies o model.Movies[0] sean null o estén vacíos. Cuando se evalúa la expresión lambda (por ejemplo, @Html.DisplayFor(modelItem => item.Title)), se evalúan los valores de propiedad del modelo.

Pruebe la aplicación buscando por género, por título de la película y por ambos:

Ventana del explorador en la que se muestran los resultados de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2