Partager via


Partie 7, ajouter la recherche à une application MVC ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la stratégie de support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 9 de cet article.

Par Rick Anderson

Dans cette section, vous ajoutez une fonctionnalité de recherche à la méthode d’action Index qui vous permet de rechercher des films par genre ou par nom.

Mettez à jour la méthode Index trouvée dans Controllers/MoviesController.cs avec le code suivant :

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 ligne suivante dans la méthode d’action Index crée une requête LINQ pour sélectionner les films :

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

La requête est seulement définie à ce stade ; elle n’a pas été exécutée sur la base de données.

Si le paramètre searchString contient une chaîne, la requête de films est modifiée de façon à filtrer sur la valeur de la chaîne de recherche :

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

Le code s => s.Title!.ToUpper().Contains(searchString.ToUpper()) ci-dessus est une expression lambda. Les expressions lambda sont utilisées dans les requêtes LINQ basées sur une méthode en tant qu’arguments pour les méthodes d’opérateur de requête standard, comme la méthode Where ou Contains (utilisées dans le code ci-dessus). Les requêtes LINQ ne sont pas exécutées quand elles sont définies ou quand elles sont modifiées en appelant une méthode, comme Where, Contains ou OrderBy. Au lieu de cela, l’exécution de la requête est différée. Cela signifie que l’évaluation d’une expression est retardée jusqu’à ce que sa valeur réalisée fasse l’objet d’une itération réelle ou que la méthode ToListAsync soit appelée. Pour plus d’informations sur l’exécution différée des requêtes, consultez Exécution de requête.

Remarque

La méthode Contains est exécutée sur la base de données, et non pas dans le code C#. Le respect de la casse pour la requête dépend de la base de données et du classement. Sur SQL Server, Contains est mappée à SQL LIKE, qui ne respecte pas la casse. SQLite avec le classement par défaut est un mélange de respect de la casse et de respect de la casse IN, en fonction de la requête. Pour plus d’informations sur la création de requêtes SQLite sans respect de la casse, consultez les rubriques suivantes :

Accédez à /Movies/Index. Ajoutez une chaîne de requête comme ?searchString=Ghost à l’URL. Les films filtrés sont affichés.

Vue Index

Si vous changez la signature de la méthode Index pour y inclure un paramètre nommé id, le paramètreid correspondra à l’espace réservé facultatif {id} pour les routes par défaut définies dans Program.cs.

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

Remplacez le paramètre par id et toutes les occurrences de searchString par id.

La méthode Index précédente :

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 méthode Index mise à jour avec le paramètre 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());
}

Vous pouvez maintenant passer le titre de la recherche en tant que données de routage (un segment de l’URL) et non pas en tant que valeur de chaîne de requête.

Vue Index avec le mot « ghost » ajouté à l’URL et une liste de films retournée contenant deux films, Ghostbusters et Ghostbusters 2

Cependant, vous ne pouvez pas attendre des utilisateurs qu’ils modifient l’URL à chaque fois qu’ils veulent rechercher un film. Vous allez donc maintenant ajouter des éléments d’interface utilisateur pour les aider à filtrer les films. Si vous avez changé la signature de la méthode Index pour tester comment passer le paramètre ID lié à une route, rétablissez-la de façon à ce qu’elle prenne un paramètre nommé 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());
}

Ouvrez le fichier Views/Movies/Index.cshtml et ajoutez la balise <form> mise en surbrillance ci-dessous :

@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 balise HTML <form> utilise le Tag Helper de formulaire, de façon que quand vous envoyez le formulaire, la chaîne de filtrage soit envoyée à l’action Index du contrôleur de films. Enregistrez vos modifications puis testez le filtre.

Vue Index avec le mot « ghost » tapé dans la zone de texte de filtre des titres

Contrairement à ce que vous pourriez penser, une surcharge de [HttpPost] dans la méthode Index n’est pas nécessaire. Vous n’en avez pas besoin, car la méthode ne change pas l’état de l’application, elle filtre seulement les données.

Vous pourriez ajouter la méthode [HttpPost] Index suivante.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

Le paramètre notUsed est utilisé pour créer une surcharge pour la méthode Index. Nous parlons de ceci plus loin dans le didacticiel.

Si vous ajoutez cette méthode, le demandeur de l’action correspondrait à la méthode [HttpPost] Index et la méthode [HttpPost] Index s’exécuterait comme indiqué dans l’image ci-dessous.

Fenêtre de navigateur avec la réponse de l’application de « From HttpPost Index: » avec filtrage sur « ghost »

Cependant, même si vous ajoutez cette version [HttpPost] de la méthode Index, il existe une limitation dans la façon dont tout ceci a été implémenté. Imaginez que vous voulez insérer un signet pour une recherche spécifique, ou que vous voulez envoyer un lien à vos amis sur lequel ils peuvent cliquer pour afficher la même liste filtrée de films. Notez que l’URL de la requête HTTP POST est identique à l’URL de la requête GET (localhost:{PORT}/Movies/Index) : il n’existe aucune information de recherche dans l’URL. Les informations de la chaîne de recherche sont envoyées au serveur en tant que valeur d’un champ de formulaire. Vous pouvez vérifier ceci avec les outils de développement du navigateur ou avec l’excellent outil Fiddler.

L’image suivante montre les outils de développement du navigateur Chrome avec les onglets Réseau et En-têtes sélectionnés :

Onglets Réseau et En-têtes des outils de développement du navigateur Chrome montrant un corps de requête avec une valeur « ghost » pour searchString

Les onglets Réseau et Charge utile sont sélectionnés pour afficher les données de formulaire :

Onglets Réseau et Charge utile des outils de développement du navigateur Chrome affichant des données de formulaire

Vous pouvez voir le paramètre de recherche et le jeton XSRF dans le corps de la demande. Notez que, comme indiqué dans le tutoriel précédent, le Form Tag Helper génère un jeton XSRF anti-falsification. Nous ne modifions pas les données : nous n’avons donc pas besoin de valider le jeton dans la méthode du contrôleur.

Comme le paramètre de recherche se trouve dans le corps de la demande et pas dans l’URL, vous ne pouvez pas capturer ces informations de recherche pour les insérer dans un signet ou les partager avec d’autres personnes. Nous résolvons ce problème en spécifiant que la requête doit être HTTP GET dans la balise form qui se trouve dans le fichier 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">

Maintenant, quand vous soumettez une recherche, l’URL contient la chaîne de requête de la recherche. La recherche accède également à la méthode d’action HttpGet Index, même si vous avez une méthode HttpPost Index.

Fenêtre de navigateur montrant la chaîne de recherche « ghost » dans l’URL et les films retournés, Ghostbusters et Ghostbusters 2, qui contiennent le mot « ghost »

Ajouter la recherche par genre

Ajoutez la classe MovieGenreViewModel suivante au dossier 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; }
}

Le modèle de vue movie-genre contiendra :

  • Une liste de films.
  • Une SelectList contenant la liste des genres. Cela permet à l’utilisateur de sélectionner un genre dans la liste.
  • MovieGenre, qui contient le genre sélectionné.
  • SearchString, qui contient le texte que les utilisateurs entrent dans la zone de texte de recherche.

Remplacez la méthode Index dans MoviesController.cs par le code suivant :

// 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);
}

Le code suivant est une requête LINQ qui récupère tous les genres de la base de données.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

La SelectList de genres est créée en projetant les genres distincts (nous ne voulons pas que notre liste de sélection ait des genres en doublon).

Quand l’utilisateur recherche l’élément, la valeur de recherche est conservée dans la zone de recherche.

Ajouter la recherche par genre à la vue Index

Mettez à jour Index.cshtml trouvé dans Views/Movies/ comme suit :

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

Examinez l’expression lambda utilisée dans le HTML Helper suivant :

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

Dans le code précédent, le Helper HTML DisplayNameFor inspecte la propriété Title référencée dans l’expression lambda pour déterminer le nom d’affichage. Comme l’expression lambda est inspectée et non pas évaluée, vous ne recevez pas de violation d’accès quand model, model.Movies ou model.Movies[0] sont null ou vides. Quand l’expression lambda est évaluée (par exemple @Html.DisplayFor(modelItem => item.Title)), les valeurs des propriétés du modèle sont évaluées. ! après model.Movies est l’opérateur null-forgiving, qui est utilisé pour déclarer qui Movies n’est pas nul.

Testez l’application en effectuant une recherche par genre, par titre de film et selon ces deux critères :

Fenêtre du navigateur affichant les résultats de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Dans cette section, vous ajoutez une fonctionnalité de recherche à la méthode d’action Index qui vous permet de rechercher des films par genre ou par nom.

Mettez à jour la méthode Index trouvée dans Controllers/MoviesController.cs avec le code suivant :

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 ligne suivante dans la méthode d’action Index crée une requête LINQ pour sélectionner les films :

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

La requête est seulement définie à ce stade ; elle n’a pas été exécutée sur la base de données.

Si le paramètre searchString contient une chaîne, la requête de films est modifiée de façon à filtrer sur la valeur de la chaîne de recherche :

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

Le code s => s.Title!.ToUpper().Contains(searchString.ToUpper()) ci-dessus est une expression lambda. Les expressions lambda sont utilisées dans les requêtes LINQ basées sur une méthode en tant qu’arguments pour les méthodes d’opérateur de requête standard, comme la méthode Where ou Contains (utilisées dans le code ci-dessus). Les requêtes LINQ ne sont pas exécutées quand elles sont définies ou quand elles sont modifiées en appelant une méthode, comme Where, Contains ou OrderBy. Au lieu de cela, l’exécution de la requête est différée. Cela signifie que l’évaluation d’une expression est retardée jusqu’à ce que sa valeur réalisée fasse l’objet d’une itération réelle ou que la méthode ToListAsync soit appelée. Pour plus d’informations sur l’exécution différée des requêtes, consultez Exécution de requête.

Remarque

La méthode Contains est exécutée sur la base de données, et non pas dans le code C#. Le respect de la casse pour la requête dépend de la base de données et du classement. Sur SQL Server, Contains est mappée à SQL LIKE, qui ne respecte pas la casse. SQLite avec le classement par défaut est un mélange de respect de la casse et de respect de la casse IN, en fonction de la requête. Pour plus d’informations sur la création de requêtes SQLite sans respect de la casse, consultez les rubriques suivantes :

Accédez à /Movies/Index. Ajoutez une chaîne de requête comme ?searchString=Ghost à l’URL. Les films filtrés sont affichés.

Vue Index

Si vous changez la signature de la méthode Index pour y inclure un paramètre nommé id, le paramètreid correspondra à l’espace réservé facultatif {id} pour les routes par défaut définies dans Program.cs.

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

Remplacez le paramètre par id et toutes les occurrences de searchString par id.

La méthode Index précédente :

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 méthode Index mise à jour avec le paramètre 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());
}

Vous pouvez maintenant passer le titre de la recherche en tant que données de routage (un segment de l’URL) et non pas en tant que valeur de chaîne de requête.

Vue Index avec le mot « ghost » ajouté à l’URL et une liste de films retournée contenant deux films, Ghostbusters et Ghostbusters 2

Cependant, vous ne pouvez pas attendre des utilisateurs qu’ils modifient l’URL à chaque fois qu’ils veulent rechercher un film. Vous allez donc maintenant ajouter des éléments d’interface utilisateur pour les aider à filtrer les films. Si vous avez changé la signature de la méthode Index pour tester comment passer le paramètre ID lié à une route, rétablissez-la de façon à ce qu’elle prenne un paramètre nommé 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());
}

Ouvrez le fichier Views/Movies/Index.cshtml et ajoutez la balise <form> mise en surbrillance ci-dessous :

@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 balise HTML <form> utilise le Tag Helper de formulaire, de façon que quand vous envoyez le formulaire, la chaîne de filtrage soit envoyée à l’action Index du contrôleur de films. Enregistrez vos modifications puis testez le filtre.

Vue Index avec le mot « ghost » tapé dans la zone de texte de filtre des titres

Contrairement à ce que vous pourriez penser, une surcharge de [HttpPost] dans la méthode Index n’est pas nécessaire. Vous n’en avez pas besoin, car la méthode ne change pas l’état de l’application, elle filtre seulement les données.

Vous pourriez ajouter la méthode [HttpPost] Index suivante.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

Le paramètre notUsed est utilisé pour créer une surcharge pour la méthode Index. Nous parlons de ceci plus loin dans le didacticiel.

Si vous ajoutez cette méthode, le demandeur de l’action correspondrait à la méthode [HttpPost] Index et la méthode [HttpPost] Index s’exécuterait comme indiqué dans l’image ci-dessous.

Fenêtre de navigateur avec la réponse de l’application de « From HttpPost Index: » avec filtrage sur « ghost »

Cependant, même si vous ajoutez cette version [HttpPost] de la méthode Index, il existe une limitation dans la façon dont tout ceci a été implémenté. Imaginez que vous voulez insérer un signet pour une recherche spécifique, ou que vous voulez envoyer un lien à vos amis sur lequel ils peuvent cliquer pour afficher la même liste filtrée de films. Notez que l’URL de la requête HTTP POST est identique à l’URL de la requête GET (localhost:{PORT}/Movies/Index) : il n’existe aucune information de recherche dans l’URL. Les informations de la chaîne de recherche sont envoyées au serveur en tant que valeur d’un champ de formulaire. Vous pouvez vérifier ceci avec les outils de développement du navigateur ou avec l’excellent outil Fiddler. L’illustration ci-dessous montre les outils de développement du navigateur Chrome :

Onglet Réseau des outils de développement de Microsoft Edge montrant un corps de demande avec une valeur « ghost » pour searchString

Vous pouvez voir le paramètre de recherche et le jeton XSRF dans le corps de la demande. Notez que, comme indiqué dans le tutoriel précédent, le Form Tag Helper génère un jeton XSRF anti-falsification. Nous ne modifions pas les données : nous n’avons donc pas besoin de valider le jeton dans la méthode du contrôleur.

Comme le paramètre de recherche se trouve dans le corps de la demande et pas dans l’URL, vous ne pouvez pas capturer ces informations de recherche pour les insérer dans un signet ou les partager avec d’autres personnes. Nous résolvons ce problème en spécifiant que la requête doit être HTTP GET trouvable dans le fichier 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">

Maintenant, quand vous soumettez une recherche, l’URL contient la chaîne de requête de la recherche. La recherche accède également à la méthode d’action HttpGet Index, même si vous avez une méthode HttpPost Index.

Fenêtre de navigateur montrant la chaîne de recherche « ghost » dans l’URL et les films retournés, Ghostbusters et Ghostbusters 2, qui contiennent le mot « ghost »

La mise en forme suivante montre la modification apportée à la balise form :

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

Ajouter la recherche par genre

Ajoutez la classe MovieGenreViewModel suivante au dossier 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; }
}

Le modèle de vue movie-genre contiendra :

  • Une liste de films.
  • Une SelectList contenant la liste des genres. Cela permet à l’utilisateur de sélectionner un genre dans la liste.
  • MovieGenre, qui contient le genre sélectionné.
  • SearchString, qui contient le texte que les utilisateurs entrent dans la zone de texte de recherche.

Remplacez la méthode Index dans MoviesController.cs par le code suivant :

// 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);
}

Le code suivant est une requête LINQ qui récupère tous les genres de la base de données.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

La SelectList de genres est créée en projetant les genres distincts (nous ne voulons pas que notre liste de sélection ait des genres en doublon).

Quand l’utilisateur recherche l’élément, la valeur de recherche est conservée dans la zone de recherche.

Ajouter la recherche par genre à la vue Index

Mettez à jour Index.cshtml trouvé dans Views/Movies/ comme suit :

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

Examinez l’expression lambda utilisée dans le HTML Helper suivant :

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

Dans le code précédent, le Helper HTML DisplayNameFor inspecte la propriété Title référencée dans l’expression lambda pour déterminer le nom d’affichage. Comme l’expression lambda est inspectée et non pas évaluée, vous ne recevez pas de violation d’accès quand model, model.Movies ou model.Movies[0] sont null ou vides. Quand l’expression lambda est évaluée (par exemple @Html.DisplayFor(modelItem => item.Title)), les valeurs des propriétés du modèle sont évaluées. ! après model.Movies est l’opérateur null-forgiving, qui est utilisé pour déclarer qui Movies n’est pas nul.

Testez l’application en effectuant une recherche par genre, par titre de film et selon ces deux critères :

Fenêtre du navigateur affichant les résultats de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Dans cette section, vous ajoutez une fonctionnalité de recherche à la méthode d’action Index qui vous permet de rechercher des films par genre ou par nom.

Mettez à jour la méthode Index trouvée dans Controllers/MoviesController.cs avec le code suivant :

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 ligne suivante dans la méthode d’action Index crée une requête LINQ pour sélectionner les films :

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

La requête est seulement définie à ce stade ; elle n’a pas été exécutée sur la base de données.

Si le paramètre searchString contient une chaîne, la requête de films est modifiée de façon à filtrer sur la valeur de la chaîne de recherche :

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

Le code s => s.Title!.ToUpper().Contains(searchString.ToUpper()) ci-dessus est une expression lambda. Les expressions lambda sont utilisées dans les requêtes LINQ basées sur une méthode en tant qu’arguments pour les méthodes d’opérateur de requête standard, comme la méthode Where ou Contains (utilisées dans le code ci-dessus). Les requêtes LINQ ne sont pas exécutées quand elles sont définies ou quand elles sont modifiées en appelant une méthode, comme Where, Contains ou OrderBy. Au lieu de cela, l’exécution de la requête est différée. Cela signifie que l’évaluation d’une expression est retardée jusqu’à ce que sa valeur réalisée fasse l’objet d’une itération réelle ou que la méthode ToListAsync soit appelée. Pour plus d’informations sur l’exécution différée des requêtes, consultez Exécution de requête.

Remarque

La méthode Contains est exécutée sur la base de données, et non pas dans le code C#. Le respect de la casse pour la requête dépend de la base de données et du classement. Sur SQL Server, Contains est mappée à SQL LIKE, qui ne respecte pas la casse. SQLite avec le classement par défaut est un mélange de respect de la casse et de respect de la casse IN, en fonction de la requête. Pour plus d’informations sur la création de requêtes SQLite sans respect de la casse, consultez les rubriques suivantes :

Accédez à /Movies/Index. Ajoutez une chaîne de requête comme ?searchString=Ghost à l’URL. Les films filtrés sont affichés.

Vue Index

Si vous changez la signature de la méthode Index pour y inclure un paramètre nommé id, le paramètreid correspondra à l’espace réservé facultatif {id} pour les routes par défaut définies dans Program.cs.

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

Remplacez le paramètre par id et toutes les occurrences de searchString par id.

La méthode Index précédente :

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 méthode Index mise à jour avec le paramètre 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());
}

Vous pouvez maintenant passer le titre de la recherche en tant que données de routage (un segment de l’URL) et non pas en tant que valeur de chaîne de requête.

Vue Index avec le mot « ghost » ajouté à l’URL et une liste de films retournée contenant deux films, Ghostbusters et Ghostbusters 2

Cependant, vous ne pouvez pas attendre des utilisateurs qu’ils modifient l’URL à chaque fois qu’ils veulent rechercher un film. Vous allez donc maintenant ajouter des éléments d’interface utilisateur pour les aider à filtrer les films. Si vous avez changé la signature de la méthode Index pour tester comment passer le paramètre ID lié à une route, rétablissez-la de façon à ce qu’elle prenne un paramètre nommé 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());
}

Ouvrez le fichier Views/Movies/Index.cshtml et ajoutez la balise <form> mise en surbrillance ci-dessous :

@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 balise HTML <form> utilise le Tag Helper de formulaire, de façon que quand vous envoyez le formulaire, la chaîne de filtrage soit envoyée à l’action Index du contrôleur de films. Enregistrez vos modifications puis testez le filtre.

Vue Index avec le mot « ghost » tapé dans la zone de texte de filtre des titres

Contrairement à ce que vous pourriez penser, une surcharge de [HttpPost] dans la méthode Index n’est pas nécessaire. Vous n’en avez pas besoin, car la méthode ne change pas l’état de l’application, elle filtre seulement les données.

Vous pourriez ajouter la méthode [HttpPost] Index suivante.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

Le paramètre notUsed est utilisé pour créer une surcharge pour la méthode Index. Nous parlons de ceci plus loin dans le didacticiel.

Si vous ajoutez cette méthode, le demandeur de l’action correspondrait à la méthode [HttpPost] Index et la méthode [HttpPost] Index s’exécuterait comme indiqué dans l’image ci-dessous.

Fenêtre de navigateur avec la réponse de l’application de « From HttpPost Index: » avec filtrage sur « ghost »

Cependant, même si vous ajoutez cette version [HttpPost] de la méthode Index, il existe une limitation dans la façon dont tout ceci a été implémenté. Imaginez que vous voulez insérer un signet pour une recherche spécifique, ou que vous voulez envoyer un lien à vos amis sur lequel ils peuvent cliquer pour afficher la même liste filtrée de films. Notez que l’URL de la requête HTTP POST est identique à l’URL de la requête GET (localhost:{PORT}/Movies/Index) : il n’existe aucune information de recherche dans l’URL. Les informations de la chaîne de recherche sont envoyées au serveur en tant que valeur d’un champ de formulaire. Vous pouvez vérifier ceci avec les outils de développement du navigateur ou avec l’excellent outil Fiddler. L’illustration ci-dessous montre les outils de développement du navigateur Chrome :

Onglet Réseau des outils de développement de Microsoft Edge montrant un corps de demande avec une valeur « ghost » pour searchString

Vous pouvez voir le paramètre de recherche et le jeton XSRF dans le corps de la demande. Notez que, comme indiqué dans le tutoriel précédent, le Form Tag Helper génère un jeton XSRF anti-falsification. Nous ne modifions pas les données : nous n’avons donc pas besoin de valider le jeton dans la méthode du contrôleur.

Comme le paramètre de recherche se trouve dans le corps de la demande et pas dans l’URL, vous ne pouvez pas capturer ces informations de recherche pour les insérer dans un signet ou les partager avec d’autres personnes. Nous résolvons ce problème en spécifiant que la requête doit être HTTP GET trouvable dans le fichier 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">

Maintenant, quand vous soumettez une recherche, l’URL contient la chaîne de requête de la recherche. La recherche accède également à la méthode d’action HttpGet Index, même si vous avez une méthode HttpPost Index.

Fenêtre de navigateur montrant la chaîne de recherche « ghost » dans l’URL et les films retournés, Ghostbusters et Ghostbusters 2, qui contiennent le mot « ghost »

La mise en forme suivante montre la modification apportée à la balise form :

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

Ajouter la recherche par genre

Ajoutez la classe MovieGenreViewModel suivante au dossier 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; }
}

Le modèle de vue movie-genre contiendra :

  • Une liste de films.
  • Une SelectList contenant la liste des genres. Cela permet à l’utilisateur de sélectionner un genre dans la liste.
  • MovieGenre, qui contient le genre sélectionné.
  • SearchString, qui contient le texte que les utilisateurs entrent dans la zone de texte de recherche.

Remplacez la méthode Index dans MoviesController.cs par le code suivant :

// 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);
}

Le code suivant est une requête LINQ qui récupère tous les genres de la base de données.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

La SelectList de genres est créée en projetant les genres distincts (nous ne voulons pas que notre liste de sélection ait des genres en doublon).

Quand l’utilisateur recherche l’élément, la valeur de recherche est conservée dans la zone de recherche.

Ajouter la recherche par genre à la vue Index

Mettez à jour Index.cshtml trouvé dans Views/Movies/ comme suit :

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

Examinez l’expression lambda utilisée dans le HTML Helper suivant :

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

Dans le code précédent, le Helper HTML DisplayNameFor inspecte la propriété Title référencée dans l’expression lambda pour déterminer le nom d’affichage. Comme l’expression lambda est inspectée et non pas évaluée, vous ne recevez pas de violation d’accès quand model, model.Movies ou model.Movies[0] sont null ou vides. Quand l’expression lambda est évaluée (par exemple @Html.DisplayFor(modelItem => item.Title)), les valeurs des propriétés du modèle sont évaluées. ! après model.Movies est l’opérateur null-forgiving, qui est utilisé pour déclarer qui Movies n’est pas nul.

Testez l’application en effectuant une recherche par genre, par titre de film et selon ces deux critères :

Fenêtre du navigateur affichant les résultats de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Dans cette section, vous ajoutez une fonctionnalité de recherche à la méthode d’action Index qui vous permet de rechercher des films par genre ou par nom.

Mettez à jour la méthode Index trouvée dans Controllers/MoviesController.cs avec le code suivant :

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 première ligne de la méthode d’action Index crée une requête LINQ pour sélectionner les films :

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

La requête est seulement définie à ce stade, elle n’a pas été exécutée sur la base de données.

Si le paramètre searchString contient une chaîne, la requête de films est modifiée de façon à filtrer sur la valeur de la chaîne de recherche :

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

Le code s => s.Title!.Contains(searchString) ci-dessus est une expression lambda. Les expressions lambda sont utilisées dans les requêtes LINQ basées sur une méthode en tant qu’arguments pour les méthodes d’opérateur de requête standard, comme la méthode Where ou Contains (utilisées dans le code ci-dessus). Les requêtes LINQ ne sont pas exécutées quand elles sont définies ou quand elles sont modifiées en appelant une méthode, comme Where, Contains ou OrderBy. Au lieu de cela, l’exécution de la requête est différée. Cela signifie que l’évaluation d’une expression est retardée jusqu’à ce que sa valeur réalisée fasse l’objet d’une itération réelle ou que la méthode ToListAsync soit appelée. Pour plus d’informations sur l’exécution différée des requêtes, consultez Exécution de requête.

Remarque : La méthode Contains est exécutée sur la base de données, et non pas dans le code C# ci-dessus. Le respect de la casse pour la requête dépend de la base de données et du classement. Sur SQL Server, Contains est mappée à SQL LIKE, qui ne respecte pas la casse. Dans SQLite, avec le classement par défaut, elle respecte la casse.

Accédez à /Movies/Index. Ajoutez une chaîne de requête comme ?searchString=Ghost à l’URL. Les films filtrés sont affichés.

Vue Index

Si vous changez la signature de la méthode Index pour y inclure un paramètre nommé id, le paramètreid correspondra à l’espace réservé facultatif {id} pour les routes par défaut définies dans Program.cs.

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

Remplacez le paramètre par id et toutes les occurrences de searchString par id.

La méthode Index précédente :

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 méthode Index mise à jour avec le paramètre 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());
}

Vous pouvez maintenant passer le titre de la recherche en tant que données de routage (un segment de l’URL) et non pas en tant que valeur de chaîne de requête.

Vue Index avec le mot « ghost » ajouté à l’URL et une liste de films retournée contenant deux films, Ghostbusters et Ghostbusters 2

Cependant, vous ne pouvez pas attendre des utilisateurs qu’ils modifient l’URL à chaque fois qu’ils veulent rechercher un film. Vous allez donc maintenant ajouter des éléments d’interface utilisateur pour les aider à filtrer les films. Si vous avez changé la signature de la méthode Index pour tester comment passer le paramètre ID lié à une route, rétablissez-la de façon à ce qu’elle prenne un paramètre nommé 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());
}

Ouvrez le fichier Views/Movies/Index.cshtml et ajoutez la balise <form> mise en surbrillance ci-dessous :

@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 balise HTML <form> utilise le Tag Helper de formulaire, de façon que quand vous envoyez le formulaire, la chaîne de filtrage soit envoyée à l’action Index du contrôleur de films. Enregistrez vos modifications puis testez le filtre.

Vue Index avec le mot « ghost » tapé dans la zone de texte de filtre des titres

Contrairement à ce que vous pourriez penser, une surcharge de [HttpPost] dans la méthode Index n’est pas nécessaire. Vous n’en avez pas besoin, car la méthode ne change pas l’état de l’application, elle filtre seulement les données.

Vous pourriez ajouter la méthode [HttpPost] Index suivante.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

Le paramètre notUsed est utilisé pour créer une surcharge pour la méthode Index. Nous parlons de ceci plus loin dans le didacticiel.

Si vous ajoutez cette méthode, le demandeur de l’action correspondrait à la méthode [HttpPost] Index et la méthode [HttpPost] Index s’exécuterait comme indiqué dans l’image ci-dessous.

Fenêtre de navigateur avec la réponse de l’application de « From HttpPost Index: » avec filtrage sur « ghost »

Cependant, même si vous ajoutez cette version [HttpPost] de la méthode Index, il existe une limitation dans la façon dont tout ceci a été implémenté. Imaginez que vous voulez insérer un signet pour une recherche spécifique, ou que vous voulez envoyer un lien à vos amis sur lequel ils peuvent cliquer pour afficher la même liste filtrée de films. Notez que l’URL de la requête HTTP POST est identique à l’URL de la requête GET (localhost:{PORT}/Movies/Index) : il n’existe aucune information de recherche dans l’URL. Les informations de la chaîne de recherche sont envoyées au serveur en tant que valeur d’un champ de formulaire. Vous pouvez vérifier ceci avec les outils de développement du navigateur ou avec l’excellent outil Fiddler. L’illustration ci-dessous montre les outils de développement du navigateur Chrome :

Onglet Réseau des outils de développement de Microsoft Edge montrant un corps de demande avec une valeur « ghost » pour searchString

Vous pouvez voir le paramètre de recherche et le jeton XSRF dans le corps de la demande. Notez que, comme indiqué dans le tutoriel précédent, le Form Tag Helper génère un jeton XSRF anti-falsification. Nous ne modifions pas les données : nous n’avons donc pas besoin de valider le jeton dans la méthode du contrôleur.

Comme le paramètre de recherche se trouve dans le corps de la demande et pas dans l’URL, vous ne pouvez pas capturer ces informations de recherche pour les insérer dans un signet ou les partager avec d’autres personnes. Nous résolvons ce problème en spécifiant que la requête doit être HTTP GET trouvable dans le fichier 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">

Maintenant, quand vous soumettez une recherche, l’URL contient la chaîne de requête de la recherche. La recherche accède également à la méthode d’action HttpGet Index, même si vous avez une méthode HttpPost Index.

Fenêtre de navigateur montrant la chaîne de recherche « ghost » dans l’URL et les films retournés, Ghostbusters et Ghostbusters 2, qui contiennent le mot « ghost »

La mise en forme suivante montre la modification apportée à la balise form :

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

Ajouter la recherche par genre

Ajoutez la classe MovieGenreViewModel suivante au dossier 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; }
    }
}

Le modèle de vue movie-genre contiendra :

  • Une liste de films.
  • Une SelectList contenant la liste des genres. Cela permet à l’utilisateur de sélectionner un genre dans la liste.
  • MovieGenre, qui contient le genre sélectionné.
  • SearchString, qui contient le texte que les utilisateurs entrent dans la zone de texte de recherche.

Remplacez la méthode Index dans MoviesController.cs par le code suivant :

// 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);
}

Le code suivant est une requête LINQ qui récupère tous les genres de la base de données.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

La SelectList de genres est créée en projetant les genres distincts (nous ne voulons pas que notre liste de sélection ait des genres en doublon).

Quand l’utilisateur recherche l’élément, la valeur de recherche est conservée dans la zone de recherche.

Ajouter la recherche par genre à la vue Index

Mettez à jour Index.cshtml trouvé dans Views/Movies/ comme suit :

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

Examinez l’expression lambda utilisée dans le HTML Helper suivant :

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

Dans le code précédent, le Helper HTML DisplayNameFor inspecte la propriété Title référencée dans l’expression lambda pour déterminer le nom d’affichage. Comme l’expression lambda est inspectée et non pas évaluée, vous ne recevez pas de violation d’accès quand model, model.Movies ou model.Movies[0] sont null ou vides. Quand l’expression lambda est évaluée (par exemple @Html.DisplayFor(modelItem => item.Title)), les valeurs des propriétés du modèle sont évaluées.

Testez l’application en effectuant une recherche par genre, par titre de film et selon ces deux critères :

Fenêtre du navigateur affichant les résultats de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Dans cette section, vous ajoutez une fonctionnalité de recherche à la méthode d’action Index qui vous permet de rechercher des films par genre ou par nom.

Mettez à jour la méthode Index trouvée dans Controllers/MoviesController.cs avec le code suivant :

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 première ligne de la méthode d’action Index crée une requête LINQ pour sélectionner les films :

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

La requête est seulement définie à ce stade, elle n’a pas été exécutée sur la base de données.

Si le paramètre searchString contient une chaîne, la requête de films est modifiée de façon à filtrer sur la valeur de la chaîne de recherche :

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

Le code s => s.Title.Contains() ci-dessus est une expression lambda. Les expressions lambda sont utilisées dans les requêtes LINQ basées sur une méthode en tant qu’arguments pour les méthodes d’opérateur de requête standard, comme la méthode Where ou Contains (utilisées dans le code ci-dessus). Les requêtes LINQ ne sont pas exécutées quand elles sont définies ou quand elles sont modifiées en appelant une méthode, comme Where, Contains ou OrderBy. Au lieu de cela, l’exécution de la requête est différée. Cela signifie que l’évaluation d’une expression est retardée jusqu’à ce que sa valeur réalisée fasse l’objet d’une itération réelle ou que la méthode ToListAsync soit appelée. Pour plus d’informations sur l’exécution différée des requêtes, consultez Exécution de requête.

Remarque : La méthode Contains est exécutée sur la base de données, et non pas dans le code C# ci-dessus. Le respect de la casse pour la requête dépend de la base de données et du classement. Sur SQL Server, Contains est mappée à SQL LIKE, qui ne respecte pas la casse. Dans SQLite, avec le classement par défaut, elle respecte la casse.

Accédez à /Movies/Index. Ajoutez une chaîne de requête comme ?searchString=Ghost à l’URL. Les films filtrés sont affichés.

Vue Index

Si vous changez la signature de la méthode Index pour y inclure un paramètre nommé id, le paramètreid correspondra à l’espace réservé facultatif {id} pour les routes par défaut définies dans Startup.cs.

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

Remplacez le paramètre par id et toutes les occurrences de searchString par id.

La méthode Index précédente :

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 méthode Index mise à jour avec le paramètre 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());
}

Vous pouvez maintenant passer le titre de la recherche en tant que données de routage (un segment de l’URL) et non pas en tant que valeur de chaîne de requête.

Vue Index avec le mot « ghost » ajouté à l’URL et une liste de films retournée contenant deux films, Ghostbusters et Ghostbusters 2

Cependant, vous ne pouvez pas attendre des utilisateurs qu’ils modifient l’URL à chaque fois qu’ils veulent rechercher un film. Vous allez donc maintenant ajouter des éléments d’interface utilisateur pour les aider à filtrer les films. Si vous avez changé la signature de la méthode Index pour tester comment passer le paramètre ID lié à une route, rétablissez-la de façon à ce qu’elle prenne un paramètre nommé 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());
}

Ouvrez le fichier Views/Movies/Index.cshtml et ajoutez la balise <form> mise en surbrillance ci-dessous :

    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 balise HTML <form> utilise le Tag Helper de formulaire, de façon que quand vous envoyez le formulaire, la chaîne de filtrage soit envoyée à l’action Index du contrôleur de films. Enregistrez vos modifications puis testez le filtre.

Vue Index avec le mot « ghost » tapé dans la zone de texte de filtre des titres

Contrairement à ce que vous pourriez penser, une surcharge de [HttpPost] dans la méthode Index n’est pas nécessaire. Vous n’en avez pas besoin, car la méthode ne change pas l’état de l’application, elle filtre seulement les données.

Vous pourriez ajouter la méthode [HttpPost] Index suivante.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

Le paramètre notUsed est utilisé pour créer une surcharge pour la méthode Index. Nous parlons de ceci plus loin dans le didacticiel.

Si vous ajoutez cette méthode, le demandeur de l’action correspondrait à la méthode [HttpPost] Index et la méthode [HttpPost] Index s’exécuterait comme indiqué dans l’image ci-dessous.

Fenêtre de navigateur avec la réponse de l’application de « From HttpPost Index: » avec filtrage sur « ghost »

Cependant, même si vous ajoutez cette version [HttpPost] de la méthode Index, il existe une limitation dans la façon dont tout ceci a été implémenté. Imaginez que vous voulez insérer un signet pour une recherche spécifique, ou que vous voulez envoyer un lien à vos amis sur lequel ils peuvent cliquer pour afficher la même liste filtrée de films. Notez que l’URL de la requête HTTP POST est identique à l’URL de la requête GET (localhost:{PORT}/Movies/Index) : il n’existe aucune information de recherche dans l’URL. Les informations de la chaîne de recherche sont envoyées au serveur en tant que valeur d’un champ de formulaire. Vous pouvez vérifier ceci avec les outils de développement du navigateur ou avec l’excellent outil Fiddler. L’illustration ci-dessous montre les outils de développement du navigateur Chrome :

Onglet Réseau des outils de développement de Microsoft Edge montrant un corps de demande avec une valeur « ghost » pour searchString

Vous pouvez voir le paramètre de recherche et le jeton XSRF dans le corps de la demande. Notez que, comme indiqué dans le tutoriel précédent, le Form Tag Helper génère un jeton XSRF anti-falsification. Nous ne modifions pas les données : nous n’avons donc pas besoin de valider le jeton dans la méthode du contrôleur.

Comme le paramètre de recherche se trouve dans le corps de la demande et pas dans l’URL, vous ne pouvez pas capturer ces informations de recherche pour les insérer dans un signet ou les partager avec d’autres personnes. Nous résolvons ce problème en spécifiant que la requête doit être HTTP GET trouvable dans le fichier 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)

Maintenant, quand vous soumettez une recherche, l’URL contient la chaîne de requête de la recherche. La recherche accède également à la méthode d’action HttpGet Index, même si vous avez une méthode HttpPost Index.

Fenêtre de navigateur montrant la chaîne de recherche « ghost » dans l’URL et les films retournés, Ghostbusters et Ghostbusters 2, qui contiennent le mot « ghost »

La mise en forme suivante montre la modification apportée à la balise form :

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

Ajouter la recherche par genre

Ajoutez la classe MovieGenreViewModel suivante au dossier 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; }
    }
}

Le modèle de vue movie-genre contiendra :

  • Une liste de films.
  • Une SelectList contenant la liste des genres. Cela permet à l’utilisateur de sélectionner un genre dans la liste.
  • MovieGenre, qui contient le genre sélectionné.
  • SearchString, qui contient le texte que les utilisateurs entrent dans la zone de texte de recherche.

Remplacez la méthode Index dans MoviesController.cs par le code suivant :

// 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);
}

Le code suivant est une requête LINQ qui récupère tous les genres de la base de données.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

La SelectList de genres est créée en projetant les genres distincts (nous ne voulons pas que notre liste de sélection ait des genres en doublon).

Quand l’utilisateur recherche l’élément, la valeur de recherche est conservée dans la zone de recherche.

Ajouter la recherche par genre à la vue Index

Mettez à jour Index.cshtml trouvé dans Views/Movies/ comme suit :

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

Examinez l’expression lambda utilisée dans le HTML Helper suivant :

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

Dans le code précédent, le Helper HTML DisplayNameFor inspecte la propriété Title référencée dans l’expression lambda pour déterminer le nom d’affichage. Comme l’expression lambda est inspectée et non pas évaluée, vous ne recevez pas de violation d’accès quand model, model.Movies ou model.Movies[0] sont null ou vides. Quand l’expression lambda est évaluée (par exemple @Html.DisplayFor(modelItem => item.Title)), les valeurs des propriétés du modèle sont évaluées.

Testez l’application en effectuant une recherche par genre, par titre de film et selon ces deux critères :

Fenêtre du navigateur affichant les résultats de https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2