Часть 7. Добавление поиска в приложение MVC ASP.NET Core

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

Автор: Рик Андерсон (Rick Anderson)

В этом разделе вы добавите в метод действия Index возможности поиска, которые позволяют выполнять поиск фильмов по жанру или названию.

Обновите метод, найденный Index внутри Controllers/MoviesController.cs , с помощью следующего кода:

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

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

Следующая строка в методе Index действия создает запрос LINQ для выбора фильмов:

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

Этот запрос только определяется в этой точке и не выполняется для базы данных.

Если параметр searchString содержит строку, запрос фильмов изменяется для фильтрации по значению в строке поиска:

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

Приведенный выше код s => s.Title!.Contains(searchString) представляет собой лямбда-выражение. Лямбда-выражения используются в запросах LINQ на основе методов в качестве аргументов стандартных методов операторов запроса, таких как метод Where или Contains (используется в приведенном выше коде). Запросы LINQ не выполняются, если они определяются или изменяются путем вызова метода, например Where, Contains или OrderBy. Вместо этого выполнение запроса откладывается. Это означает, что вычисление выражения откладывается до тех пор, пока не будет выполнена итерация его реализованного значения или пока не будет вызван метод ToListAsync. Дополнительные сведения об отложенном и немедленном выполнении запросов см. в разделе Выполнение запроса.

Примечание. Метод Contains выполняется в базе данных, а не в приведенном выше коде c#. Регистр символов запроса учитывается в зависимости от параметров базы данных и сортировки. В SQL Server метод Contains сопоставляется с SQL LIKE, в котором регистр символов не учитывается. В SQLite при параметрах сортировки по умолчанию регистр символов учитывается.

Перейдите к /Movies/Index. Добавьте в URL-адрес строку запроса, например ?searchString=Ghost. Отображаются отфильтрованные фильмы.

Представление Index

Если изменить сигнатуру Index метода с именем idпараметра, id параметр будет соответствовать необязательному {id} заполнителю для заданных по умолчанию маршрутов Program.cs.

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

Измените параметр на id, а все вхождения searchString — на id.

Предыдущий метод Index:

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

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

Обновленный метод Index с параметром 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!.Contains(id));
    }

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

Теперь можно передать заголовок поиска в качестве данных маршрута (сегмент URL-адреса) вместо значения строки запроса.

Представление Index, в URL-адрес которого добавлено слово ghost, возвращает два фильма: Ghostbusters и Ghostbusters 2

Тем не менее пользователи вряд ли будут каждый раз изменять URL-адрес для поиска фильмов. Итак, теперь вам необходимо добавить элементы пользовательского интерфейса для удобства фильтрации фильмов. Если вы изменили сигнатуру метода Index для тестирования передачи параметра ID с привязкой к маршруту, измените ее снова, чтобы она снова принимала параметр 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!.Contains(searchString));
    }

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

Views/Movies/Index.cshtml Откройте файл и добавьте разметку, выделенную <form> ниже:

@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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

Тег HTML <form> использует вспомогательную функцию тега Form, чтобы при отправке формы строка фильтра передавалась в действие Index контроллера movies. Сохраните изменения и протестируйте фильтр.

Представление Index со словом ghost в текстовом поле фильтра по названию

Вопреки ожиданиям, перегрузка [HttpPost] для метода Index отсутствует. Она не нужна, поскольку метод не изменяет состояние приложения и просто выполняет фильтрацию данных.

Можно добавить следующий метод [HttpPost] Index.

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

Параметр notUsed используется для создания перегрузки метода Index. Это мы обсудим далее в этом учебнике.

При добавлении этого метода вызывающий метод действия будет сопоставлять метод [HttpPost] Index, а метод [HttpPost] Index будет выполняться, как показано на рисунке ниже.

Окно браузера с ответом приложения From HttpPost Index: фильтр по слову ghost

Тем не менее при добавлении этой версии [HttpPost] метода Index существует ограничение на общую реализацию. Допустим, вам необходимо добавить в закладки конкретный поиск или отправить друзьям ссылку, по которой они могут просмотреть аналогичный отфильтрованный список фильмов. Обратите внимание, что URL-адрес запроса HTTP POST совпадает с URL-адресом запроса GET (localhost:{ПОРТ}/Movies/Index) — в URL-адресе отсутствуют сведения о поиске. Данные строки поиска отправляются на сервер в виде значения поля формы. Вы можете проверить это с помощью средств разработчика для браузера или инструмента Fiddler. На рисунке ниже показаны средства разработчика для браузера Chrome:

Вкладка

В теле запроса отображается параметр поиска и маркер XSRF. Обратите внимание, что, как описывается в предыдущем руководстве, вспомогательная функция тега Form создает маркер защиты от подделки XSRF. Поскольку мы не изменяем данные, проверять маркер безопасности в методе контроллера не нужно.

Так как параметр поиска находится в теле запроса, а не в URL-адресе, эти сведения о поиске нельзя добавить в закладки или открыть для общего доступа. Исправьте это, указав запрос, который должен находиться HTTP GET в 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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

После отправки поиска URL-адрес содержит строку поискового запроса. Поиск также переносится в метод HttpGet Index, даже если у вас определен метод HttpPost Index.

Окно браузера с фрагментом searchString=ghost в URL-адресе, которое возвращает фильмы Ghostbusters и Ghostbusters 2, с текстом ghost в названии

В следующем примере разметки показано изменение тега form:

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

Добавление поиска по жанру

Добавьте следующий класс MovieGenreViewModel в папку 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; }
}

Модель представления фильмов по жанру будет содержать:

  • Список фильмов.
  • Объект SelectList со списком жанров. В этом списке пользователь может выбрать жанр фильма.
  • Объект MovieGenre, содержащий выбранный жанр.
  • SearchString, содержащий текст, который пользователи вводят в поле поиска.

Замените метод Index в файле MoviesController.cs следующим кодом:

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

Следующий код определяет запрос LINQ, который извлекает все жанры из базы данных.

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

Объект SelectList со списком жанров создается путем проецирования отдельных жанров (это необходимо, чтобы исключить повторяющиеся жанры).

Когда пользователь выполняет поиск элемента, значение поиска сохраняется в поле поиска.

Добавление поиска по жанру в представление индекса

Обновите файл Index.cshtml, находящийся в папке Views/Movies/, следующим образом:

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

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Проверьте лямбда-выражение, которое используется в следующем вспомогательном методе HTML:

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

В предыдущем коде вспомогательный метод HTML DisplayNameFor проверяет свойство Title, указанное в лямбда-выражении, и определяет отображаемое имя. Поскольку лямбда-выражение проверяется, а не вычисляется, в том случае, если model, model.Movies или model.Movies[0] имеют значение null или пусты, не происходит нарушение прав доступа. При вычислении лямбда-выражения (например, @Html.DisplayFor(modelItem => item.Title)) вычисляются значения для свойств модели. model.Movies После ! этого используется оператор, допускающий значение NULL, который используется для объявления, что Movies не равно null.

Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и по обоим этим параметрам:

Окно браузера с результатами https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

В этом разделе вы добавите в метод действия Index возможности поиска, которые позволяют выполнять поиск фильмов по жанру или названию.

Обновите метод, найденный Index внутри Controllers/MoviesController.cs , с помощью следующего кода:

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

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

Следующая строка в методе Index действия создает запрос LINQ для выбора фильмов:

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

Этот запрос только определяется в этой точке и не выполняется для базы данных.

Если параметр searchString содержит строку, запрос фильмов изменяется для фильтрации по значению в строке поиска:

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

Приведенный выше код s => s.Title!.Contains(searchString) представляет собой лямбда-выражение. Лямбда-выражения используются в запросах LINQ на основе методов в качестве аргументов стандартных методов операторов запроса, таких как метод Where или Contains (используется в приведенном выше коде). Запросы LINQ не выполняются, если они определяются или изменяются путем вызова метода, например Where, Contains или OrderBy. Вместо этого выполнение запроса откладывается. Это означает, что вычисление выражения откладывается до тех пор, пока не будет выполнена итерация его реализованного значения или пока не будет вызван метод ToListAsync. Дополнительные сведения об отложенном и немедленном выполнении запросов см. в разделе Выполнение запроса.

Примечание. Метод Contains выполняется в базе данных, а не в приведенном выше коде c#. Регистр символов запроса учитывается в зависимости от параметров базы данных и сортировки. В SQL Server метод Contains сопоставляется с SQL LIKE, в котором регистр символов не учитывается. В SQLite при параметрах сортировки по умолчанию регистр символов учитывается.

Перейдите к /Movies/Index. Добавьте в URL-адрес строку запроса, например ?searchString=Ghost. Отображаются отфильтрованные фильмы.

Представление Index

Если изменить сигнатуру Index метода с именем idпараметра, id параметр будет соответствовать необязательному {id} заполнителю для заданных по умолчанию маршрутов Program.cs.

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

Измените параметр на id, а все вхождения searchString — на id.

Предыдущий метод Index:

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

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

Обновленный метод Index с параметром 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!.Contains(id));
    }

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

Теперь можно передать заголовок поиска в качестве данных маршрута (сегмент URL-адреса) вместо значения строки запроса.

Представление Index, в URL-адрес которого добавлено слово ghost, возвращает два фильма: Ghostbusters и Ghostbusters 2

Тем не менее пользователи вряд ли будут каждый раз изменять URL-адрес для поиска фильмов. Итак, теперь вам необходимо добавить элементы пользовательского интерфейса для удобства фильтрации фильмов. Если вы изменили сигнатуру метода Index для тестирования передачи параметра ID с привязкой к маршруту, измените ее снова, чтобы она снова принимала параметр 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!.Contains(searchString));
    }

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

Views/Movies/Index.cshtml Откройте файл и добавьте разметку, выделенную <form> ниже:

@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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

Тег HTML <form> использует вспомогательную функцию тега Form, чтобы при отправке формы строка фильтра передавалась в действие Index контроллера movies. Сохраните изменения и протестируйте фильтр.

Представление Index со словом ghost в текстовом поле фильтра по названию

Вопреки ожиданиям, перегрузка [HttpPost] для метода Index отсутствует. Она не нужна, поскольку метод не изменяет состояние приложения и просто выполняет фильтрацию данных.

Можно добавить следующий метод [HttpPost] Index.

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

Параметр notUsed используется для создания перегрузки метода Index. Это мы обсудим далее в этом учебнике.

При добавлении этого метода вызывающий метод действия будет сопоставлять метод [HttpPost] Index, а метод [HttpPost] Index будет выполняться, как показано на рисунке ниже.

Окно браузера с ответом приложения From HttpPost Index: фильтр по слову ghost

Тем не менее при добавлении этой версии [HttpPost] метода Index существует ограничение на общую реализацию. Допустим, вам необходимо добавить в закладки конкретный поиск или отправить друзьям ссылку, по которой они могут просмотреть аналогичный отфильтрованный список фильмов. Обратите внимание, что URL-адрес запроса HTTP POST совпадает с URL-адресом запроса GET (localhost:{ПОРТ}/Movies/Index) — в URL-адресе отсутствуют сведения о поиске. Данные строки поиска отправляются на сервер в виде значения поля формы. Вы можете проверить это с помощью средств разработчика для браузера или инструмента Fiddler. На рисунке ниже показаны средства разработчика для браузера Chrome:

Вкладка

В теле запроса отображается параметр поиска и маркер XSRF. Обратите внимание, что, как описывается в предыдущем руководстве, вспомогательная функция тега Form создает маркер защиты от подделки XSRF. Поскольку мы не изменяем данные, проверять маркер безопасности в методе контроллера не нужно.

Так как параметр поиска находится в теле запроса, а не в URL-адресе, эти сведения о поиске нельзя добавить в закладки или открыть для общего доступа. Исправьте это, указав запрос, который должен находиться HTTP GET в 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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

После отправки поиска URL-адрес содержит строку поискового запроса. Поиск также переносится в метод HttpGet Index, даже если у вас определен метод HttpPost Index.

Окно браузера с фрагментом searchString=ghost в URL-адресе, которое возвращает фильмы Ghostbusters и Ghostbusters 2, с текстом ghost в названии

В следующем примере разметки показано изменение тега form:

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

Добавление поиска по жанру

Добавьте следующий класс MovieGenreViewModel в папку 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; }
}

Модель представления фильмов по жанру будет содержать:

  • Список фильмов.
  • Объект SelectList со списком жанров. В этом списке пользователь может выбрать жанр фильма.
  • Объект MovieGenre, содержащий выбранный жанр.
  • SearchString, содержащий текст, который пользователи вводят в поле поиска.

Замените метод Index в файле MoviesController.cs следующим кодом:

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

Следующий код определяет запрос LINQ, который извлекает все жанры из базы данных.

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

Объект SelectList со списком жанров создается путем проецирования отдельных жанров (это необходимо, чтобы исключить повторяющиеся жанры).

Когда пользователь выполняет поиск элемента, значение поиска сохраняется в поле поиска.

Добавление поиска по жанру в представление индекса

Обновите файл Index.cshtml, находящийся в папке Views/Movies/, следующим образом:

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

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Проверьте лямбда-выражение, которое используется в следующем вспомогательном методе HTML:

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

В предыдущем коде вспомогательный метод HTML DisplayNameFor проверяет свойство Title, указанное в лямбда-выражении, и определяет отображаемое имя. Поскольку лямбда-выражение проверяется, а не вычисляется, в том случае, если model, model.Movies или model.Movies[0] имеют значение null или пусты, не происходит нарушение прав доступа. При вычислении лямбда-выражения (например, @Html.DisplayFor(modelItem => item.Title)) вычисляются значения для свойств модели. model.Movies После ! этого используется оператор, допускающий значение NULL, который используется для объявления, что Movies не равно null.

Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и по обоим этим параметрам:

Окно браузера с результатами https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

В этом разделе вы добавите в метод действия Index возможности поиска, которые позволяют выполнять поиск фильмов по жанру или названию.

Обновите метод, найденный Index внутри Controllers/MoviesController.cs , с помощью следующего кода:

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

В первой строке метода действия Index создается запрос LINQ для выбора фильмов:

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

Этот запрос только определяется в этой точке и не выполняется для базы данных.

Если параметр searchString содержит строку, запрос фильмов изменяется для фильтрации по значению в строке поиска:

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

Приведенный выше код s => s.Title!.Contains(searchString) представляет собой лямбда-выражение. Лямбда-выражения используются в запросах LINQ на основе методов в качестве аргументов стандартных методов операторов запроса, таких как метод Where или Contains (используется в приведенном выше коде). Запросы LINQ не выполняются, если они определяются или изменяются путем вызова метода, например Where, Contains или OrderBy. Вместо этого выполнение запроса откладывается. Это означает, что вычисление выражения откладывается до тех пор, пока не будет выполнена итерация его реализованного значения или пока не будет вызван метод ToListAsync. Дополнительные сведения об отложенном и немедленном выполнении запросов см. в разделе Выполнение запроса.

Примечание. Метод Contains выполняется в базе данных, а не в приведенном выше коде c#. Регистр символов запроса учитывается в зависимости от параметров базы данных и сортировки. В SQL Server метод Contains сопоставляется с SQL LIKE, в котором регистр символов не учитывается. В SQLite при параметрах сортировки по умолчанию регистр символов учитывается.

Перейдите к /Movies/Index. Добавьте в URL-адрес строку запроса, например ?searchString=Ghost. Отображаются отфильтрованные фильмы.

Представление Index

Если изменить сигнатуру Index метода с именем idпараметра, id параметр будет соответствовать необязательному {id} заполнителю для заданных по умолчанию маршрутов Program.cs.

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

Измените параметр на id, а все вхождения searchString — на id.

Предыдущий метод Index:

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

Обновленный метод Index с параметром 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());
}

Теперь можно передать заголовок поиска в качестве данных маршрута (сегмент URL-адреса) вместо значения строки запроса.

Представление Index, в URL-адрес которого добавлено слово ghost, возвращает два фильма: Ghostbusters и Ghostbusters 2

Тем не менее пользователи вряд ли будут каждый раз изменять URL-адрес для поиска фильмов. Итак, теперь вам необходимо добавить элементы пользовательского интерфейса для удобства фильтрации фильмов. Если вы изменили сигнатуру метода Index для тестирования передачи параметра ID с привязкой к маршруту, измените ее снова, чтобы она снова принимала параметр 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());
}

Views/Movies/Index.cshtml Откройте файл и добавьте разметку, выделенную <form> ниже:

@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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

Тег HTML <form> использует вспомогательную функцию тега Form, чтобы при отправке формы строка фильтра передавалась в действие Index контроллера movies. Сохраните изменения и протестируйте фильтр.

Представление Index со словом ghost в текстовом поле фильтра по названию

Вопреки ожиданиям, перегрузка [HttpPost] для метода Index отсутствует. Она не нужна, поскольку метод не изменяет состояние приложения и просто выполняет фильтрацию данных.

Можно добавить следующий метод [HttpPost] Index.

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

Параметр notUsed используется для создания перегрузки метода Index. Это мы обсудим далее в этом учебнике.

При добавлении этого метода вызывающий метод действия будет сопоставлять метод [HttpPost] Index, а метод [HttpPost] Index будет выполняться, как показано на рисунке ниже.

Окно браузера с ответом приложения From HttpPost Index: фильтр по слову ghost

Тем не менее при добавлении этой версии [HttpPost] метода Index существует ограничение на общую реализацию. Допустим, вам необходимо добавить в закладки конкретный поиск или отправить друзьям ссылку, по которой они могут просмотреть аналогичный отфильтрованный список фильмов. Обратите внимание, что URL-адрес запроса HTTP POST совпадает с URL-адресом запроса GET (localhost:{ПОРТ}/Movies/Index) — в URL-адресе отсутствуют сведения о поиске. Данные строки поиска отправляются на сервер в виде значения поля формы. Вы можете проверить это с помощью средств разработчика для браузера или инструмента Fiddler. На рисунке ниже показаны средства разработчика для браузера Chrome:

Вкладка

В теле запроса отображается параметр поиска и маркер XSRF. Обратите внимание, что, как описывается в предыдущем руководстве, вспомогательная функция тега Form создает маркер защиты от подделки XSRF. Поскольку мы не изменяем данные, проверять маркер безопасности в методе контроллера не нужно.

Так как параметр поиска находится в теле запроса, а не в URL-адресе, эти сведения о поиске нельзя добавить в закладки или открыть для общего доступа. Исправьте это, указав запрос, который должен находиться HTTP GET в 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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

После отправки поиска URL-адрес содержит строку поискового запроса. Поиск также переносится в метод HttpGet Index, даже если у вас определен метод HttpPost Index.

Окно браузера с фрагментом searchString=ghost в URL-адресе, которое возвращает фильмы Ghostbusters и Ghostbusters 2, с текстом ghost в названии

В следующем примере разметки показано изменение тега form:

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

Добавление поиска по жанру

Добавьте следующий класс MovieGenreViewModel в папку 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; }
    }
}

Модель представления фильмов по жанру будет содержать:

  • Список фильмов.
  • Объект SelectList со списком жанров. В этом списке пользователь может выбрать жанр фильма.
  • Объект MovieGenre, содержащий выбранный жанр.
  • SearchString, содержащий текст, который пользователи вводят в поле поиска.

Замените метод Index в файле MoviesController.cs следующим кодом:

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

Следующий код определяет запрос LINQ, который извлекает все жанры из базы данных.

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

Объект SelectList со списком жанров создается путем проецирования отдельных жанров (это необходимо, чтобы исключить повторяющиеся жанры).

Когда пользователь выполняет поиск элемента, значение поиска сохраняется в поле поиска.

Добавление поиска по жанру в представление индекса

Обновите файл Index.cshtml, находящийся в папке Views/Movies/, следующим образом:

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

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Проверьте лямбда-выражение, которое используется в следующем вспомогательном методе HTML:

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

В предыдущем коде вспомогательный метод HTML DisplayNameFor проверяет свойство Title, указанное в лямбда-выражении, и определяет отображаемое имя. Поскольку лямбда-выражение проверяется, а не вычисляется, в том случае, если model, model.Movies или model.Movies[0] имеют значение null или пусты, не происходит нарушение прав доступа. При вычислении лямбда-выражения (например, @Html.DisplayFor(modelItem => item.Title)) вычисляются значения для свойств модели.

Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и по обоим этим параметрам:

Окно браузера с результатами https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2

В этом разделе вы добавите в метод действия Index возможности поиска, которые позволяют выполнять поиск фильмов по жанру или названию.

Обновите метод, найденный Index внутри Controllers/MoviesController.cs , с помощью следующего кода:

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

В первой строке метода действия Index создается запрос LINQ для выбора фильмов:

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

Этот запрос только определяется в этой точке и не выполняется для базы данных.

Если параметр searchString содержит строку, запрос фильмов изменяется для фильтрации по значению в строке поиска:

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

Приведенный выше код s => s.Title.Contains() представляет собой лямбда-выражение. Лямбда-выражения используются в запросах LINQ на основе методов в качестве аргументов стандартных методов операторов запроса, таких как метод Where или Contains (используется в приведенном выше коде). Запросы LINQ не выполняются, если они определяются или изменяются путем вызова метода, например Where, Contains или OrderBy. Вместо этого выполнение запроса откладывается. Это означает, что вычисление выражения откладывается до тех пор, пока не будет выполнена итерация его реализованного значения или пока не будет вызван метод ToListAsync. Дополнительные сведения об отложенном и немедленном выполнении запросов см. в разделе Выполнение запроса.

Примечание. Метод Contains выполняется в базе данных, а не в приведенном выше коде c#. Регистр символов запроса учитывается в зависимости от параметров базы данных и сортировки. В SQL Server метод Contains сопоставляется с SQL LIKE, в котором регистр символов не учитывается. В SQLite при параметрах сортировки по умолчанию регистр символов учитывается.

Перейдите к /Movies/Index. Добавьте в URL-адрес строку запроса, например ?searchString=Ghost. Отображаются отфильтрованные фильмы.

Представление Index

Если изменить сигнатуру Index метода с именем idпараметра, id параметр будет соответствовать необязательному {id} заполнителю для заданных по умолчанию маршрутов Startup.cs.

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

Измените параметр на id, а все вхождения searchString — на id.

Предыдущий метод Index:

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

Обновленный метод Index с параметром 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());
}

Теперь можно передать заголовок поиска в качестве данных маршрута (сегмент URL-адреса) вместо значения строки запроса.

Представление Index, в URL-адрес которого добавлено слово ghost, возвращает два фильма: Ghostbusters и Ghostbusters 2

Тем не менее пользователи вряд ли будут каждый раз изменять URL-адрес для поиска фильмов. Итак, теперь вам необходимо добавить элементы пользовательского интерфейса для удобства фильтрации фильмов. Если вы изменили сигнатуру метода Index для тестирования передачи параметра ID с привязкой к маршруту, измените ее снова, чтобы она снова принимала параметр 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());
}

Views/Movies/Index.cshtml Откройте файл и добавьте разметку, выделенную <form> ниже:

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

<h2>Index</h2>

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

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

<table class="table">
    <thead>

Тег HTML <form> использует вспомогательную функцию тега Form, чтобы при отправке формы строка фильтра передавалась в действие Index контроллера movies. Сохраните изменения и протестируйте фильтр.

Представление Index со словом ghost в текстовом поле фильтра по названию

Вопреки ожиданиям, перегрузка [HttpPost] для метода Index отсутствует. Она не нужна, поскольку метод не изменяет состояние приложения и просто выполняет фильтрацию данных.

Можно добавить следующий метод [HttpPost] Index.

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

Параметр notUsed используется для создания перегрузки метода Index. Это мы обсудим далее в этом учебнике.

При добавлении этого метода вызывающий метод действия будет сопоставлять метод [HttpPost] Index, а метод [HttpPost] Index будет выполняться, как показано на рисунке ниже.

Окно браузера с ответом приложения From HttpPost Index: фильтр по слову ghost

Тем не менее при добавлении этой версии [HttpPost] метода Index существует ограничение на общую реализацию. Допустим, вам необходимо добавить в закладки конкретный поиск или отправить друзьям ссылку, по которой они могут просмотреть аналогичный отфильтрованный список фильмов. Обратите внимание, что URL-адрес запроса HTTP POST совпадает с URL-адресом запроса GET (localhost:{ПОРТ}/Movies/Index) — в URL-адресе отсутствуют сведения о поиске. Данные строки поиска отправляются на сервер в виде значения поля формы. Вы можете проверить это с помощью средств разработчика для браузера или инструмента Fiddler. На рисунке ниже показаны средства разработчика для браузера Chrome:

Вкладка

В теле запроса отображается параметр поиска и маркер XSRF. Обратите внимание, что, как описывается в предыдущем руководстве, вспомогательная функция тега Form создает маркер защиты от подделки XSRF. Поскольку мы не изменяем данные, проверять маркер безопасности в методе контроллера не нужно.

Так как параметр поиска находится в теле запроса, а не в URL-адресе, эти сведения о поиске нельзя добавить в закладки или открыть для общего доступа. Исправьте это, указав запрос, который должен находиться HTTP GET в 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>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

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

После отправки поиска URL-адрес содержит строку поискового запроса. Поиск также переносится в метод HttpGet Index, даже если у вас определен метод HttpPost Index.

Окно браузера с фрагментом searchString=ghost в URL-адресе, которое возвращает фильмы Ghostbusters и Ghostbusters 2, с текстом ghost в названии

В следующем примере разметки показано изменение тега form:

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

Добавление поиска по жанру

Добавьте следующий класс MovieGenreViewModel в папку 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; }
    }
}

Модель представления фильмов по жанру будет содержать:

  • Список фильмов.
  • Объект SelectList со списком жанров. В этом списке пользователь может выбрать жанр фильма.
  • Объект MovieGenre, содержащий выбранный жанр.
  • SearchString, содержащий текст, который пользователи вводят в поле поиска.

Замените метод Index в файле MoviesController.cs следующим кодом:

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

Следующий код определяет запрос LINQ, который извлекает все жанры из базы данных.

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

Объект SelectList со списком жанров создается путем проецирования отдельных жанров (это необходимо, чтобы исключить повторяющиеся жанры).

Когда пользователь выполняет поиск элемента, значение поиска сохраняется в поле поиска.

Добавление поиска по жанру в представление индекса

Обновите файл Index.cshtml, находящийся в папке Views/Movies/, следующим образом:

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

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Проверьте лямбда-выражение, которое используется в следующем вспомогательном методе HTML:

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

В предыдущем коде вспомогательный метод HTML DisplayNameFor проверяет свойство Title, указанное в лямбда-выражении, и определяет отображаемое имя. Поскольку лямбда-выражение проверяется, а не вычисляется, в том случае, если model, model.Movies или model.Movies[0] имеют значение null или пусты, не происходит нарушение прав доступа. При вычислении лямбда-выражения (например, @Html.DisplayFor(modelItem => item.Title)) вычисляются значения для свойств модели.

Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и по обоим этим параметрам:

Окно браузера с результатами https://localhost:5001/Movies?MovieGenre=Comedy& SearchString=2