Parte 7, aggiungere la ricerca a un'app MVC core ASP.NET

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Di Rick Anderson

In questa sezione si aggiunge la funzionalità di ricerca al metodo di azione Index che consente di cercare film in base al genere o al nome.

Aggiornare il Index metodo trovato all'interno Controllers/MoviesController.cs con il codice seguente:

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 riga seguente nel Index metodo di azione crea una query LINQ per selezionare i film:

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

La query viene solo definita in questo punto, ma non viene eseguita nel database.

Se il parametro searchString contiene una stringa, la query dei film viene modificata per filtrare in base al valore della stringa di ricerca:

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

Il codice s => s.Title!.ToUpper().Contains(searchString.ToUpper()) precedente è un'espressione lambda. Le espressioni lambda vengono usate nelle query LINQ basate su metodo come argomenti per metodi dell'operatore di query standard, ad esempio il Where metodo o Contains (usato nel codice precedente). Le query LINQ non vengono eseguite al momento della definizione o della modifica chiamando un metodo quale Where, Contains o OrderBy. L'esecuzione della query viene invece posticipata. Per esecuzione posticipata si intende che la valutazione di un'espressione viene ritardata finché non viene eseguita l'iterazione del valore o non viene chiamato il metodo ToListAsync. Per altre informazioni sull'esecuzione posticipata di query, vedere Esecuzione di query.

Nota

Il Contains metodo viene eseguito nel database, non nel codice C#. La distinzione tra maiuscole/minuscole nella query dipende dal database e dalle regole di confronto. In SQL Server Contains esegue il mapping a SQL LIKE che fa distinzione tra maiuscole e minuscole. SQLite con le regole di confronto predefinite è una combinazione di distinzione tra maiuscole e minuscole e di distinzione tra maiuscole e minuscole, a seconda della query. Per informazioni sull'esecuzione di query SQLite senza distinzione tra maiuscole e minuscole, vedere quanto segue:

Accedere a /Movies/Index. Accodare una stringa di query, ad esempio ?searchString=Ghost, all'URL. Vengono visualizzati i film filtrati.

Vista Index

Se si modifica la firma del Index metodo in modo che abbia un parametro denominato id, il id parametro corrisponderà al segnaposto facoltativo per le route predefinite {id} impostate in Program.cs.

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

Modificare il parametro in id e modificare tutte le occorrenze di searchString in id.

Il metodo Index precedente:

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

Il metodo Index aggiornato con il parametro 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());
}

È ora possibile passare il titolo della ricerca come dati di route (un segmento di URL), anziché come valore della stringa di query.

Vista Index con la parola ghost aggiunta all'URL e un elenco restituito di due film: Ghostbusters e Ghostbusters 2

Tuttavia, non è possibile supporre che gli utenti modifichino l'URL ogni volta che desiderano cercare un film. A questo punto si aggiungeranno elementi dell'interfaccia utente per filtrare i film. Se è stata modificata la firma del metodo Index per testare come passare il parametro ID associato alla route, impostarlo di nuovo in modo che accetti un parametro denominato 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());
}

Aprire il Views/Movies/Index.cshtml file e aggiungere il <form> markup evidenziato di seguito:

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

Il tag <form> HTML usa l'helper tag del modulo e quindi quando si invia il modulo, la stringa di filtro viene registrata nell'azione Index del controller di film. Salvare le modifiche e quindi testare il filtro.

Vista Index con la parola ghost digitata nella casella di testo del filtro del titolo

Non è presente alcun overload del metodo [HttpPost]Index come previsto. Non è necessario perché il metodo non modifica lo stato dell'app, ma filtra solo i dati.

È possibile aggiungere il metodo [HttpPost] Index seguente.

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

Il parametro notUsed viene usato per creare un overload per il metodo Index. Questo aspetto verrà trattato in una fase successiva dell'esercitazione.

Se si aggiunge questo metodo, l'invoker di azione trova la corrispondenza con il metodo [HttpPost] Index e il metodo [HttpPost] Index viene eseguito come illustrato nell'immagine seguente.

Finestra del browser con la risposta dell'applicazione From HttpPost Index: filter on ghost

Tuttavia, anche se si aggiunge questa versione [HttpPost] del metodo Index, esiste una limitazione sul modo sulla relativa implementazione. Si supponga che si desideri usare come segnalibro una ricerca specifica o inviare un collegamento agli amici su cui possono fare clic per visualizzare lo stesso elenco filtrato di film. Si noti che l'URL per la richiesta HTTP POST è lo stesso URL per la richiesta GET (localhost:{PORTA}/Movies/Index). Le informazioni sulla ricerca non sono disponibili nell'URL. Le informazioni sulla stringa di ricerca vengono inviate al server come un valore del campo modulo. È possibile eseguire una verifica con gli strumenti di sviluppo del browser o l'eccellente strumento Fiddler.

L'immagine seguente mostra il browser Chrome Strumenti di sviluppo con le schede Network e Headers selezionate:

Schede Network and Headers di Chrome browser Developer Tools con un corpo della richiesta con un valore searchString di ghost

Le schede Rete e Payload sono selezionate per visualizzare i dati del modulo:

Schede Rete e Payload di Chrome Browser Developer Tools con i dati del modulo

È possibile esaminare il parametro di ricerca e il token XSRF nel corpo della richiesta. Come indicato nell'esercitazione precedente, l'helper tag form genera un token antiforgery XSRF. Poiché non si stanno modificando i dati, non è necessario convalidare il token nel metodo del controller.

Poiché il parametro di ricerca si trova nel corpo della richiesta e non nell'URL, non è possibile acquisire queste informazioni sulla ricerca da usare come segnalibro o condividerle con altri utenti. Risolvere questo problema specificando che la richiesta deve trovarsi HTTP GET nel form tag trovato nel Views/Movies/Index.cshtml file.

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

A questo punto quando si invia una ricerca, l'URL contiene la stringa di query della ricerca. La ricerca passerà anche al metodo di azione HttpGet Index, anche se si dispone di un metodo HttpPost Index.

Finestra del browser che mostra searchString=ghost nell'URL e i film restituiti, Ghostbusters e Ghostbusters 2, contengono la parola ghost

Aggiungere la funzionalità di ricerca in base al genere

Aggiungere la classe MovieGenreViewModel seguente alla cartella 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; }
}

Il modello di vista movie-genre conterrà:

  • Un elenco di film.
  • SelectList contiene l'elenco dei generi. Consente all'utente di selezionare un genere dall'elenco.
  • MovieGenre che contiene il genere selezionato.
  • SearchString, che contiene il testo immesso dagli utenti nella casella di testo di ricerca.

Sostituire il metodo Index in MoviesController.cs con il codice seguente:

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

Il codice seguente è una query LINQ che recupera tutti i generi dal database.

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

L'elenco SelectList di generi viene creato presentando generi distinti per evitare che l'elenco di selezione includa generi duplicati.

Quando l'utente cerca l'elemento, viene mantenuto il valore di ricerca nella casella di ricerca.

Aggiungere la funzionalità di ricerca in base al genere alla vista Index

Aggiornare Index.cshtml in Views/Movies/ come segue:

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

Esaminare l'espressione lambda usata nell'helper HTML seguente:

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

Nel codice precedente, l'helper HTML DisplayNameFor controlla la proprietà Title a cui si fa riferimento nell'espressione lambda per determinare il nome visualizzato. Poiché l'espressione lambda viene controllata e anziché essere valutata, non viene generata una violazione di accesso quando model, model.Movies o model.Movies[0] sono null o vuoti. Quando invece l'espressione lambda viene valutata, ad esempio @Html.DisplayFor(modelItem => item.Title), vengono valutati i valori delle proprietà del modello. Il ! valore after model.Movies è l'operatore null-forgiving, che viene usato per dichiarare che Movies non è Null.

Eseguire il test dell'app effettuando una ricerca per genere, titolo del film ed entrambi:

Finestra del browser che mostra i risultati di https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

In questa sezione si aggiunge la funzionalità di ricerca al metodo di azione Index che consente di cercare film in base al genere o al nome.

Aggiornare il Index metodo trovato all'interno Controllers/MoviesController.cs con il codice seguente:

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 riga seguente nel Index metodo di azione crea una query LINQ per selezionare i film:

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

La query viene solo definita in questo punto, ma non viene eseguita nel database.

Se il parametro searchString contiene una stringa, la query dei film viene modificata per filtrare in base al valore della stringa di ricerca:

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

Il codice s => s.Title!.ToUpper().Contains(searchString.ToUpper()) precedente è un'espressione lambda. Le espressioni lambda vengono usate nelle query LINQ basate su metodo come argomenti per metodi dell'operatore di query standard, ad esempio il Where metodo o Contains (usato nel codice precedente). Le query LINQ non vengono eseguite al momento della definizione o della modifica chiamando un metodo quale Where, Contains o OrderBy. L'esecuzione della query viene invece posticipata. Per esecuzione posticipata si intende che la valutazione di un'espressione viene ritardata finché non viene eseguita l'iterazione del valore o non viene chiamato il metodo ToListAsync. Per altre informazioni sull'esecuzione posticipata di query, vedere Esecuzione di query.

Nota

Il Contains metodo viene eseguito nel database, non nel codice C#. La distinzione tra maiuscole/minuscole nella query dipende dal database e dalle regole di confronto. In SQL Server Contains esegue il mapping a SQL LIKE che fa distinzione tra maiuscole e minuscole. SQLite con le regole di confronto predefinite è una combinazione di distinzione tra maiuscole e minuscole e di distinzione tra maiuscole e minuscole, a seconda della query. Per informazioni sull'esecuzione di query SQLite senza distinzione tra maiuscole e minuscole, vedere quanto segue:

Accedere a /Movies/Index. Accodare una stringa di query, ad esempio ?searchString=Ghost, all'URL. Vengono visualizzati i film filtrati.

Vista Index

Se si modifica la firma del Index metodo in modo che abbia un parametro denominato id, il id parametro corrisponderà al segnaposto facoltativo per le route predefinite {id} impostate in Program.cs.

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

Modificare il parametro in id e modificare tutte le occorrenze di searchString in id.

Il metodo Index precedente:

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

Il metodo Index aggiornato con il parametro 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());
}

È ora possibile passare il titolo della ricerca come dati di route (un segmento di URL), anziché come valore della stringa di query.

Vista Index con la parola ghost aggiunta all'URL e un elenco restituito di due film: Ghostbusters e Ghostbusters 2

Tuttavia, non è possibile supporre che gli utenti modifichino l'URL ogni volta che desiderano cercare un film. A questo punto si aggiungeranno elementi dell'interfaccia utente per filtrare i film. Se è stata modificata la firma del metodo Index per testare come passare il parametro ID associato alla route, impostarlo di nuovo in modo che accetti un parametro denominato 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());
}

Aprire il Views/Movies/Index.cshtml file e aggiungere il <form> markup evidenziato di seguito:

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

Il tag <form> HTML usa l'helper tag del modulo e quindi quando si invia il modulo, la stringa di filtro viene registrata nell'azione Index del controller di film. Salvare le modifiche e quindi testare il filtro.

Vista Index con la parola ghost digitata nella casella di testo del filtro del titolo

Non è presente alcun overload del metodo [HttpPost]Index come previsto. Non è necessario perché il metodo non modifica lo stato dell'app, ma filtra solo i dati.

È possibile aggiungere il metodo [HttpPost] Index seguente.

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

Il parametro notUsed viene usato per creare un overload per il metodo Index. Questo aspetto verrà trattato in una fase successiva dell'esercitazione.

Se si aggiunge questo metodo, l'invoker di azione trova la corrispondenza con il metodo [HttpPost] Index e il metodo [HttpPost] Index viene eseguito come illustrato nell'immagine seguente.

Finestra del browser con la risposta dell'applicazione From HttpPost Index: filter on ghost

Tuttavia, anche se si aggiunge questa versione [HttpPost] del metodo Index, esiste una limitazione sul modo sulla relativa implementazione. Si supponga che si desideri usare come segnalibro una ricerca specifica o inviare un collegamento agli amici su cui possono fare clic per visualizzare lo stesso elenco filtrato di film. Si noti che l'URL per la richiesta HTTP POST è lo stesso URL per la richiesta GET (localhost:{PORTA}/Movies/Index). Le informazioni sulla ricerca non sono disponibili nell'URL. Le informazioni sulla stringa di ricerca vengono inviate al server come un valore del campo modulo. È possibile eseguire una verifica con gli strumenti di sviluppo del browser o l'eccellente strumento Fiddler. L'immagine seguente mostra gli strumenti di sviluppo del browser Chrome:

Scheda Rete di Microsoft Edge Developer Tools che mostra un corpo della richiesta con un valore searchString fantasma

È possibile esaminare il parametro di ricerca e il token XSRF nel corpo della richiesta. Come indicato nell'esercitazione precedente, l'helper tag form genera un token antiforgery XSRF. Poiché non si stanno modificando i dati, non è necessario convalidare il token nel metodo del controller.

Poiché il parametro di ricerca si trova nel corpo della richiesta e non nell'URL, non è possibile acquisire queste informazioni sulla ricerca da usare come segnalibro o condividerle con altri utenti. Risolvere questo problema specificando che la richiesta deve essere HTTP GET trovata nel Views/Movies/Index.cshtml file.

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

A questo punto quando si invia una ricerca, l'URL contiene la stringa di query della ricerca. La ricerca passerà anche al metodo di azione HttpGet Index, anche se si dispone di un metodo HttpPost Index.

Finestra del browser che mostra searchString=ghost nell'URL e i film restituiti, Ghostbusters e Ghostbusters 2, contengono la parola ghost

Il markup seguente mostra la modifica al tag form:

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

Aggiungere la funzionalità di ricerca in base al genere

Aggiungere la classe MovieGenreViewModel seguente alla cartella 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; }
}

Il modello di vista movie-genre conterrà:

  • Un elenco di film.
  • SelectList contiene l'elenco dei generi. Consente all'utente di selezionare un genere dall'elenco.
  • MovieGenre che contiene il genere selezionato.
  • SearchString, che contiene il testo immesso dagli utenti nella casella di testo di ricerca.

Sostituire il metodo Index in MoviesController.cs con il codice seguente:

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

Il codice seguente è una query LINQ che recupera tutti i generi dal database.

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

L'elenco SelectList di generi viene creato presentando generi distinti per evitare che l'elenco di selezione includa generi duplicati.

Quando l'utente cerca l'elemento, viene mantenuto il valore di ricerca nella casella di ricerca.

Aggiungere la funzionalità di ricerca in base al genere alla vista Index

Aggiornare Index.cshtml in Views/Movies/ come segue:

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

Esaminare l'espressione lambda usata nell'helper HTML seguente:

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

Nel codice precedente, l'helper HTML DisplayNameFor controlla la proprietà Title a cui si fa riferimento nell'espressione lambda per determinare il nome visualizzato. Poiché l'espressione lambda viene controllata e anziché essere valutata, non viene generata una violazione di accesso quando model, model.Movies o model.Movies[0] sono null o vuoti. Quando invece l'espressione lambda viene valutata, ad esempio @Html.DisplayFor(modelItem => item.Title), vengono valutati i valori delle proprietà del modello. Il ! valore after model.Movies è l'operatore null-forgiving, che viene usato per dichiarare che Movies non è Null.

Eseguire il test dell'app effettuando una ricerca per genere, titolo del film ed entrambi:

Finestra del browser che mostra i risultati di https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

In questa sezione si aggiunge la funzionalità di ricerca al metodo di azione Index che consente di cercare film in base al genere o al nome.

Aggiornare il Index metodo trovato all'interno Controllers/MoviesController.cs con il codice seguente:

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 riga seguente nel Index metodo di azione crea una query LINQ per selezionare i film:

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

La query viene solo definita in questo punto, ma non viene eseguita nel database.

Se il parametro searchString contiene una stringa, la query dei film viene modificata per filtrare in base al valore della stringa di ricerca:

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

Il codice s => s.Title!.ToUpper().Contains(searchString.ToUpper()) precedente è un'espressione lambda. Le espressioni lambda vengono usate nelle query LINQ basate su metodo come argomenti per metodi dell'operatore di query standard, ad esempio il Where metodo o Contains (usato nel codice precedente). Le query LINQ non vengono eseguite al momento della definizione o della modifica chiamando un metodo quale Where, Contains o OrderBy. L'esecuzione della query viene invece posticipata. Per esecuzione posticipata si intende che la valutazione di un'espressione viene ritardata finché non viene eseguita l'iterazione del valore o non viene chiamato il metodo ToListAsync. Per altre informazioni sull'esecuzione posticipata di query, vedere Esecuzione di query.

Nota

Il Contains metodo viene eseguito nel database, non nel codice C#. La distinzione tra maiuscole/minuscole nella query dipende dal database e dalle regole di confronto. In SQL Server Contains esegue il mapping a SQL LIKE che fa distinzione tra maiuscole e minuscole. SQLite con le regole di confronto predefinite è una combinazione di distinzione tra maiuscole e minuscole e di distinzione tra maiuscole e minuscole, a seconda della query. Per informazioni sull'esecuzione di query SQLite senza distinzione tra maiuscole e minuscole, vedere quanto segue:

Accedere a /Movies/Index. Accodare una stringa di query, ad esempio ?searchString=Ghost, all'URL. Vengono visualizzati i film filtrati.

Vista Index

Se si modifica la firma del Index metodo in modo che abbia un parametro denominato id, il id parametro corrisponderà al segnaposto facoltativo per le route predefinite {id} impostate in Program.cs.

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

Modificare il parametro in id e modificare tutte le occorrenze di searchString in id.

Il metodo Index precedente:

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

Il metodo Index aggiornato con il parametro 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());
}

È ora possibile passare il titolo della ricerca come dati di route (un segmento di URL), anziché come valore della stringa di query.

Vista Index con la parola ghost aggiunta all'URL e un elenco restituito di due film: Ghostbusters e Ghostbusters 2

Tuttavia, non è possibile supporre che gli utenti modifichino l'URL ogni volta che desiderano cercare un film. A questo punto si aggiungeranno elementi dell'interfaccia utente per filtrare i film. Se è stata modificata la firma del metodo Index per testare come passare il parametro ID associato alla route, impostarlo di nuovo in modo che accetti un parametro denominato 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());
}

Aprire il Views/Movies/Index.cshtml file e aggiungere il <form> markup evidenziato di seguito:

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

Il tag <form> HTML usa l'helper tag del modulo e quindi quando si invia il modulo, la stringa di filtro viene registrata nell'azione Index del controller di film. Salvare le modifiche e quindi testare il filtro.

Vista Index con la parola ghost digitata nella casella di testo del filtro del titolo

Non è presente alcun overload del metodo [HttpPost]Index come previsto. Non è necessario perché il metodo non modifica lo stato dell'app, ma filtra solo i dati.

È possibile aggiungere il metodo [HttpPost] Index seguente.

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

Il parametro notUsed viene usato per creare un overload per il metodo Index. Questo aspetto verrà trattato in una fase successiva dell'esercitazione.

Se si aggiunge questo metodo, l'invoker di azione trova la corrispondenza con il metodo [HttpPost] Index e il metodo [HttpPost] Index viene eseguito come illustrato nell'immagine seguente.

Finestra del browser con la risposta dell'applicazione From HttpPost Index: filter on ghost

Tuttavia, anche se si aggiunge questa versione [HttpPost] del metodo Index, esiste una limitazione sul modo sulla relativa implementazione. Si supponga che si desideri usare come segnalibro una ricerca specifica o inviare un collegamento agli amici su cui possono fare clic per visualizzare lo stesso elenco filtrato di film. Si noti che l'URL per la richiesta HTTP POST è lo stesso URL per la richiesta GET (localhost:{PORTA}/Movies/Index). Le informazioni sulla ricerca non sono disponibili nell'URL. Le informazioni sulla stringa di ricerca vengono inviate al server come un valore del campo modulo. È possibile eseguire una verifica con gli strumenti di sviluppo del browser o l'eccellente strumento Fiddler. L'immagine seguente mostra gli strumenti di sviluppo del browser Chrome:

Scheda Rete di Microsoft Edge Developer Tools che mostra un corpo della richiesta con un valore searchString fantasma

È possibile esaminare il parametro di ricerca e il token XSRF nel corpo della richiesta. Come indicato nell'esercitazione precedente, l'helper tag form genera un token antiforgery XSRF. Poiché non si stanno modificando i dati, non è necessario convalidare il token nel metodo del controller.

Poiché il parametro di ricerca si trova nel corpo della richiesta e non nell'URL, non è possibile acquisire queste informazioni sulla ricerca da usare come segnalibro o condividerle con altri utenti. Risolvere questo problema specificando che la richiesta deve essere HTTP GET trovata nel Views/Movies/Index.cshtml file.

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

A questo punto quando si invia una ricerca, l'URL contiene la stringa di query della ricerca. La ricerca passerà anche al metodo di azione HttpGet Index, anche se si dispone di un metodo HttpPost Index.

Finestra del browser che mostra searchString=ghost nell'URL e i film restituiti, Ghostbusters e Ghostbusters 2, contengono la parola ghost

Il markup seguente mostra la modifica al tag form:

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

Aggiungere la funzionalità di ricerca in base al genere

Aggiungere la classe MovieGenreViewModel seguente alla cartella 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; }
}

Il modello di vista movie-genre conterrà:

  • Un elenco di film.
  • SelectList contiene l'elenco dei generi. Consente all'utente di selezionare un genere dall'elenco.
  • MovieGenre che contiene il genere selezionato.
  • SearchString, che contiene il testo immesso dagli utenti nella casella di testo di ricerca.

Sostituire il metodo Index in MoviesController.cs con il codice seguente:

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

Il codice seguente è una query LINQ che recupera tutti i generi dal database.

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

L'elenco SelectList di generi viene creato presentando generi distinti per evitare che l'elenco di selezione includa generi duplicati.

Quando l'utente cerca l'elemento, viene mantenuto il valore di ricerca nella casella di ricerca.

Aggiungere la funzionalità di ricerca in base al genere alla vista Index

Aggiornare Index.cshtml in Views/Movies/ come segue:

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

Esaminare l'espressione lambda usata nell'helper HTML seguente:

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

Nel codice precedente, l'helper HTML DisplayNameFor controlla la proprietà Title a cui si fa riferimento nell'espressione lambda per determinare il nome visualizzato. Poiché l'espressione lambda viene controllata e anziché essere valutata, non viene generata una violazione di accesso quando model, model.Movies o model.Movies[0] sono null o vuoti. Quando invece l'espressione lambda viene valutata, ad esempio @Html.DisplayFor(modelItem => item.Title), vengono valutati i valori delle proprietà del modello. Il ! valore after model.Movies è l'operatore null-forgiving, che viene usato per dichiarare che Movies non è Null.

Eseguire il test dell'app effettuando una ricerca per genere, titolo del film ed entrambi:

Finestra del browser che mostra i risultati di https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

In questa sezione si aggiunge la funzionalità di ricerca al metodo di azione Index che consente di cercare film in base al genere o al nome.

Aggiornare il Index metodo trovato all'interno Controllers/MoviesController.cs con il codice seguente:

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 prima riga del metodo di azione Index crea una query LINQ per selezionare i film:

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

La query viene solo definita in questo punto, ma non viene eseguita nel database.

Se il parametro searchString contiene una stringa, la query dei film viene modificata per filtrare in base al valore della stringa di ricerca:

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

Il codice s => s.Title!.Contains(searchString) precedente è un'espressione lambda. Le espressioni lambda vengono usate nelle query LINQ basate su metodo come argomenti per metodi dell'operatore di query standard, ad esempio il Where metodo o Contains (usato nel codice precedente). Le query LINQ non vengono eseguite al momento della definizione o della modifica chiamando un metodo quale Where, Contains o OrderBy. L'esecuzione della query viene invece posticipata. Per esecuzione posticipata si intende che la valutazione di un'espressione viene ritardata finché non viene eseguita l'iterazione del valore o non viene chiamato il metodo ToListAsync. Per altre informazioni sull'esecuzione posticipata di query, vedere Esecuzione di query.

Nota: il Contains metodo viene eseguito nel database, non nel codice c# illustrato in precedenza. La distinzione tra maiuscole/minuscole nella query dipende dal database e dalle regole di confronto. In SQL Server Contains esegue il mapping a SQL LIKE che fa distinzione tra maiuscole e minuscole. In SQLite, con le regole di confronto predefinite si fa distinzione tra maiuscole e minuscole.

Accedere a /Movies/Index. Accodare una stringa di query, ad esempio ?searchString=Ghost, all'URL. Vengono visualizzati i film filtrati.

Vista Index

Se si modifica la firma del Index metodo in modo che abbia un parametro denominato id, il id parametro corrisponderà al segnaposto facoltativo per le route predefinite {id} impostate in Program.cs.

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

Modificare il parametro in id e modificare tutte le occorrenze di searchString in id.

Il metodo Index precedente:

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

Il metodo Index aggiornato con il parametro 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());
}

È ora possibile passare il titolo della ricerca come dati di route (un segmento di URL), anziché come valore della stringa di query.

Vista Index con la parola ghost aggiunta all'URL e un elenco restituito di due film: Ghostbusters e Ghostbusters 2

Tuttavia, non è possibile supporre che gli utenti modifichino l'URL ogni volta che desiderano cercare un film. A questo punto si aggiungeranno elementi dell'interfaccia utente per filtrare i film. Se è stata modificata la firma del metodo Index per testare come passare il parametro ID associato alla route, impostarlo di nuovo in modo che accetti un parametro denominato 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());
}

Aprire il Views/Movies/Index.cshtml file e aggiungere il <form> markup evidenziato di seguito:

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

Il tag <form> HTML usa l'helper tag del modulo e quindi quando si invia il modulo, la stringa di filtro viene registrata nell'azione Index del controller di film. Salvare le modifiche e quindi testare il filtro.

Vista Index con la parola ghost digitata nella casella di testo del filtro del titolo

Non è presente alcun overload del metodo [HttpPost]Index come previsto. Non è necessario perché il metodo non modifica lo stato dell'app, ma filtra solo i dati.

È possibile aggiungere il metodo [HttpPost] Index seguente.

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

Il parametro notUsed viene usato per creare un overload per il metodo Index. Questo aspetto verrà trattato in una fase successiva dell'esercitazione.

Se si aggiunge questo metodo, l'invoker di azione trova la corrispondenza con il metodo [HttpPost] Index e il metodo [HttpPost] Index viene eseguito come illustrato nell'immagine seguente.

Finestra del browser con la risposta dell'applicazione From HttpPost Index: filter on ghost

Tuttavia, anche se si aggiunge questa versione [HttpPost] del metodo Index, esiste una limitazione sul modo sulla relativa implementazione. Si supponga che si desideri usare come segnalibro una ricerca specifica o inviare un collegamento agli amici su cui possono fare clic per visualizzare lo stesso elenco filtrato di film. Si noti che l'URL per la richiesta HTTP POST è lo stesso URL per la richiesta GET (localhost:{PORTA}/Movies/Index). Le informazioni sulla ricerca non sono disponibili nell'URL. Le informazioni sulla stringa di ricerca vengono inviate al server come un valore del campo modulo. È possibile eseguire una verifica con gli strumenti di sviluppo del browser o l'eccellente strumento Fiddler. L'immagine seguente mostra gli strumenti di sviluppo del browser Chrome:

Scheda Rete di Microsoft Edge Developer Tools che mostra un corpo della richiesta con un valore searchString fantasma

È possibile esaminare il parametro di ricerca e il token XSRF nel corpo della richiesta. Come indicato nell'esercitazione precedente, l'helper tag form genera un token antiforgery XSRF. Poiché non si stanno modificando i dati, non è necessario convalidare il token nel metodo del controller.

Poiché il parametro di ricerca si trova nel corpo della richiesta e non nell'URL, non è possibile acquisire queste informazioni sulla ricerca da usare come segnalibro o condividerle con altri utenti. Risolvere questo problema specificando che la richiesta deve essere HTTP GET trovata nel Views/Movies/Index.cshtml file.

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

A questo punto quando si invia una ricerca, l'URL contiene la stringa di query della ricerca. La ricerca passerà anche al metodo di azione HttpGet Index, anche se si dispone di un metodo HttpPost Index.

Finestra del browser che mostra searchString=ghost nell'URL e i film restituiti, Ghostbusters e Ghostbusters 2, contengono la parola ghost

Il markup seguente mostra la modifica al tag form:

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

Aggiungere la funzionalità di ricerca in base al genere

Aggiungere la classe MovieGenreViewModel seguente alla cartella 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; }
    }
}

Il modello di vista movie-genre conterrà:

  • Un elenco di film.
  • SelectList contiene l'elenco dei generi. Consente all'utente di selezionare un genere dall'elenco.
  • MovieGenre che contiene il genere selezionato.
  • SearchString, che contiene il testo immesso dagli utenti nella casella di testo di ricerca.

Sostituire il metodo Index in MoviesController.cs con il codice seguente:

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

Il codice seguente è una query LINQ che recupera tutti i generi dal database.

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

L'elenco SelectList di generi viene creato presentando generi distinti per evitare che l'elenco di selezione includa generi duplicati.

Quando l'utente cerca l'elemento, viene mantenuto il valore di ricerca nella casella di ricerca.

Aggiungere la funzionalità di ricerca in base al genere alla vista Index

Aggiornare Index.cshtml in Views/Movies/ come segue:

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

Esaminare l'espressione lambda usata nell'helper HTML seguente:

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

Nel codice precedente, l'helper HTML DisplayNameFor controlla la proprietà Title a cui si fa riferimento nell'espressione lambda per determinare il nome visualizzato. Poiché l'espressione lambda viene controllata e anziché essere valutata, non viene generata una violazione di accesso quando model, model.Movies o model.Movies[0] sono null o vuoti. Quando invece l'espressione lambda viene valutata, ad esempio @Html.DisplayFor(modelItem => item.Title), vengono valutati i valori delle proprietà del modello.

Eseguire il test dell'app effettuando una ricerca per genere, titolo del film ed entrambi:

Finestra del browser che mostra i risultati di https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

In questa sezione si aggiunge la funzionalità di ricerca al metodo di azione Index che consente di cercare film in base al genere o al nome.

Aggiornare il Index metodo trovato all'interno Controllers/MoviesController.cs con il codice seguente:

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 prima riga del metodo di azione Index crea una query LINQ per selezionare i film:

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

La query viene solo definita in questo punto, ma non viene eseguita nel database.

Se il parametro searchString contiene una stringa, la query dei film viene modificata per filtrare in base al valore della stringa di ricerca:

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

Il codice s => s.Title.Contains() precedente è un'espressione lambda. Le espressioni lambda vengono usate nelle query LINQ basate su metodo come argomenti per metodi dell'operatore di query standard, ad esempio il Where metodo o Contains (usato nel codice precedente). Le query LINQ non vengono eseguite al momento della definizione o della modifica chiamando un metodo quale Where, Contains o OrderBy. L'esecuzione della query viene invece posticipata. Per esecuzione posticipata si intende che la valutazione di un'espressione viene ritardata finché non viene eseguita l'iterazione del valore o non viene chiamato il metodo ToListAsync. Per altre informazioni sull'esecuzione posticipata di query, vedere Esecuzione di query.

Nota: il Contains metodo viene eseguito nel database, non nel codice c# illustrato in precedenza. La distinzione tra maiuscole/minuscole nella query dipende dal database e dalle regole di confronto. In SQL Server Contains esegue il mapping a SQL LIKE che fa distinzione tra maiuscole e minuscole. In SQLite, con le regole di confronto predefinite si fa distinzione tra maiuscole e minuscole.

Accedere a /Movies/Index. Accodare una stringa di query, ad esempio ?searchString=Ghost, all'URL. Vengono visualizzati i film filtrati.

Vista Index

Se si modifica la firma del Index metodo in modo che abbia un parametro denominato id, il id parametro corrisponderà al segnaposto facoltativo per le route predefinite {id} impostate in Startup.cs.

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

Modificare il parametro in id e tutte le occorrenze di searchString cambiano in id.

Il metodo Index precedente:

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

Il metodo Index aggiornato con il parametro 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());
}

È ora possibile passare il titolo della ricerca come dati di route (un segmento di URL), anziché come valore della stringa di query.

Vista Index con la parola ghost aggiunta all'URL e un elenco restituito di due film: Ghostbusters e Ghostbusters 2

Tuttavia, non è possibile supporre che gli utenti modifichino l'URL ogni volta che desiderano cercare un film. A questo punto si aggiungeranno elementi dell'interfaccia utente per filtrare i film. Se è stata modificata la firma del metodo Index per testare come passare il parametro ID associato alla route, impostarlo di nuovo in modo che accetti un parametro denominato 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());
}

Aprire il Views/Movies/Index.cshtml file e aggiungere il <form> markup evidenziato di seguito:

    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>

Il tag <form> HTML usa l'helper tag del modulo e quindi quando si invia il modulo, la stringa di filtro viene registrata nell'azione Index del controller di film. Salvare le modifiche e quindi testare il filtro.

Vista Index con la parola ghost digitata nella casella di testo del filtro del titolo

Non è presente alcun overload del metodo [HttpPost]Index come previsto. Non è necessario perché il metodo non modifica lo stato dell'app, ma filtra solo i dati.

È possibile aggiungere il metodo [HttpPost] Index seguente.

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

Il parametro notUsed viene usato per creare un overload per il metodo Index. Questo aspetto verrà trattato in una fase successiva dell'esercitazione.

Se si aggiunge questo metodo, l'invoker di azione trova la corrispondenza con il metodo [HttpPost] Index e il metodo [HttpPost] Index viene eseguito come illustrato nell'immagine seguente.

Finestra del browser con la risposta dell'applicazione From HttpPost Index: filter on ghost

Tuttavia, anche se si aggiunge questa versione [HttpPost] del metodo Index, esiste una limitazione sul modo sulla relativa implementazione. Si supponga che si desideri usare come segnalibro una ricerca specifica o inviare un collegamento agli amici su cui possono fare clic per visualizzare lo stesso elenco filtrato di film. Si noti che l'URL per la richiesta HTTP POST è lo stesso URL per la richiesta GET (localhost:{PORTA}/Movies/Index). Le informazioni sulla ricerca non sono disponibili nell'URL. Le informazioni sulla stringa di ricerca vengono inviate al server come un valore del campo modulo. È possibile eseguire una verifica con gli strumenti di sviluppo del browser o l'eccellente strumento Fiddler. L'immagine seguente mostra gli strumenti di sviluppo del browser Chrome:

Scheda Rete di Microsoft Edge Developer Tools che mostra un corpo della richiesta con un valore searchString fantasma

È possibile esaminare il parametro di ricerca e il token XSRF nel corpo della richiesta. Come indicato nell'esercitazione precedente, l'helper tag form genera un token antiforgery XSRF. Poiché non si stanno modificando i dati, non è necessario convalidare il token nel metodo del controller.

Poiché il parametro di ricerca si trova nel corpo della richiesta e non nell'URL, non è possibile acquisire queste informazioni sulla ricerca da usare come segnalibro o condividerle con altri utenti. Risolvere questo problema specificando che la richiesta deve essere HTTP GET trovata nel Views/Movies/Index.cshtml file.

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

A questo punto quando si invia una ricerca, l'URL contiene la stringa di query della ricerca. La ricerca passerà anche al metodo di azione HttpGet Index, anche se si dispone di un metodo HttpPost Index.

Finestra del browser che mostra searchString=ghost nell'URL e i film restituiti, Ghostbusters e Ghostbusters 2, contengono la parola ghost

Il markup seguente mostra la modifica al tag form:

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

Aggiungere la funzionalità di ricerca in base al genere

Aggiungere la classe MovieGenreViewModel seguente alla cartella 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; }
    }
}

Il modello di vista movie-genre conterrà:

  • Un elenco di film.
  • SelectList contiene l'elenco dei generi. Consente all'utente di selezionare un genere dall'elenco.
  • MovieGenre che contiene il genere selezionato.
  • SearchString, che contiene il testo immesso dagli utenti nella casella di testo di ricerca.

Sostituire il metodo Index in MoviesController.cs con il codice seguente:

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

Il codice seguente è una query LINQ che recupera tutti i generi dal database.

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

L'elenco SelectList di generi viene creato presentando generi distinti per evitare che l'elenco di selezione includa generi duplicati.

Quando l'utente cerca l'elemento, viene mantenuto il valore di ricerca nella casella di ricerca.

Aggiungere la funzionalità di ricerca in base al genere alla vista Index

Aggiornare Index.cshtml in Views/Movies/ come segue:

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

Esaminare l'espressione lambda usata nell'helper HTML seguente:

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

Nel codice precedente, l'helper HTML DisplayNameFor controlla la proprietà Title a cui si fa riferimento nell'espressione lambda per determinare il nome visualizzato. Poiché l'espressione lambda viene controllata e anziché essere valutata, non viene generata una violazione di accesso quando model, model.Movies o model.Movies[0] sono null o vuoti. Quando invece l'espressione lambda viene valutata, ad esempio @Html.DisplayFor(modelItem => item.Title), vengono valutati i valori delle proprietà del modello.

Eseguire il test dell'app effettuando una ricerca per genere, titolo del film ed entrambi:

Finestra del browser che mostra i risultati di https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2