第 7 部分,将搜索添加到 ASP.NET Core MVC 应用

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文.NET 9 版本。

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

作者:Rick Anderson

在本部分中,将向 Index 操作方法添加搜索功能,以实现按“类型”或“名称”搜索电影 。

使用以下代码更新 Controllers/MoviesController.cs 中的 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

上面的 s => s.Title!.ToUpper().Contains(searchString.ToUpper()) 代码是 Lambda 表达式。 Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法的参数,如 Where 方法或 Contains(上述代码中所使用的)。 在对 LINQ 查询进行定义或通过调用方法(如 WhereContainsOrderBy)进行修改后,此查询不会被执行。 相反,会延迟执行查询。 这意味着表达式的计算会延迟,直到真正循环访问其实现的值或者调用 ToListAsync 方法为止。 有关延迟执行查询的详细信息,请参阅Query Execution(查询执行)。

注意

Contains 方法在数据库中运行,而不是在 C# 代码中运行。 查询是否区分大小写取决于数据库和排序规则。 在 SQL Server 上,Contains 映射到 SQL LIKE,这是不区分大小写的。 具有默认排序规则的 SQLite 是区分大小写和不区分大小写的组合,具体取决于查询。 有关进行不区分大小写的 SQLite 查询的信息,请参阅以下内容:

导航到 /Movies/Index。 将查询字符串(如 ?searchString=Ghost)追加到 URL。 筛选的电影将显示出来。

索引视图

如果将 Index 方法的签名更改为具有名为 id 的参数,id 参数将匹配 Program.cs 中设置的默认路由的可选 {id} 占位符。

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

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

更新后带 id 参数的 Index 方法:

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

现可将搜索标题作为路由数据( URL 段)而非查询字符串值进行传递。

索引视图,显示添加到 URL 的“ghost”一词以及返回的两部电影(Ghostbusters 和 Ghostbusters 2)的电影列表

但是,不能指望用户在每次要搜索电影时都修改 URL。 因此需要添加 UI 元素来帮助他们筛选电影。 若已更改 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

此 HTML <form> 标记使用表单标记帮助程序,因此提交表单时,筛选器字符串会发布到电影控制器的 Index 操作。 保存更改,然后测试筛选器。

显示标题筛选器文本框中键入了 ghost 一词的索引视图

如你所料,不存在 Index 方法的 [HttpPost] 重载。 无需重载,因为该方法不更改应用的状态,仅筛选数据。

可添加以下 [HttpPost] Index 方法。

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

notUsed 参数用于创建 Index 方法的重载。 本教程稍后将对此进行探讨。

如果添加此方法,则操作调用程序将与 [HttpPost] Index 方法匹配,且将运行 [HttpPost] Index 方法,如下图所示。

显示“来自 HttpPost 索引: 筛选 ghost”应用程序响应的浏览器窗口

但是,即使添加 Index 方法的 [HttpPost] 版本,其实现方式也受到限制。 假设你想要将特定搜索加入书签,或向朋友发送一个链接,让他们单击链接即可查看筛选出的相同电影列表。 请注意 HTTP POST 请求的 URL 与 GET 请求的 URL (localhost:{PORT}/Movies/Index) 相同,URL 中没有任何搜索信息。 搜索字符串信息作为表单域值发送给服务器。 可使用浏览器开发人员工具或出色的 Fiddler 工具对其进行验证。

下图显示了 Chrome 浏览器开发人员工具,其中选择了“网络”和“标头”选项卡:

Chrome 浏览器开发人员工具的“网络”和“标头”选项卡,其中显示了 searchString 值为 ghost 的请求正文

选择了“网络”和“有效负载”选项卡以查看表单数据:

显示表单数据的 Chrome 浏览器开发人员工具的“网络”和“有效负载”选项卡

在请求正文中,可看到搜索参数和 XSRF 标记。 请注意,正如之前教程所述,表单标记帮助程序会生成一个 XSRF 防伪令牌。 不会修改数据,因此无需验证控制器方法中的标记。

搜索参数位于请求正文而非 URL 中,因此无法捕获该搜索信息进行书签设定或与他人共享。 修复方法是指定:请求应为 Views/Movies/Index.cshtml 文件中 form 标记内的 HTTP GET

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

现在提交搜索后,URL 将包含搜索查询字符串。 即使具备 HttpPost Index 方法,搜索也将转到 HttpGet Index 操作方法。

URL 中显示 searchString=ghost 且返回了 Ghostbusters 和 Ghostbusters 2 的浏览器窗口包含 ghost 一词

添加按流派搜索

将以下 MovieGenreViewModel 类添加到“模型”文件夹:

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包含用户在搜索文本框中输入的文本。

MoviesController.cs 中的 Index 方法替换为以下代码:

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

以下代码是一种 LINQ 查询,可从数据库中检索所有流派。

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

通过投影不同的流派创建 SelectList(我们不希望选择列表中的流派重复)。

当用户搜索某个项目时,搜索值会保留在搜索框中。

向“索引”视图添加“按流派搜索”

按如下所示更新 Views/Movies/ 中的 Index.cshtml

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

检查以下 HTML 帮助程序中使用的 Lambda 表达式:

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

在上述代码中,DisplayNameFor HTML 帮助程序检查 Lambda 表达式中引用的 Title 属性来确定显示名称。 由于只检查但未计算 Lambda 表达式,因此当 modelmodel.Movies[0]model.Moviesnull 或空时,你不会收到访问冲突。 对 Lambda 表达式求值时(例如,@Html.DisplayFor(modelItem => item.Title)),将求得该模型的属性值。 model.Movies 后面的 !null 包容运算符,该运算符用于声明 Movies 不为 null。

通过按流派搜索、按电影标题搜索以及按流派和电影标题搜索来测试应用:

浏览器窗口,显示了 https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 的结果

在本部分中,将向 Index 操作方法添加搜索功能,以实现按“类型”或“名称”搜索电影 。

使用以下代码更新 Controllers/MoviesController.cs 中的 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

上面的 s => s.Title!.ToUpper().Contains(searchString.ToUpper()) 代码是 Lambda 表达式。 Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法的参数,如 Where 方法或 Contains(上述代码中所使用的)。 在对 LINQ 查询进行定义或通过调用方法(如 WhereContainsOrderBy)进行修改后,此查询不会被执行。 相反,会延迟执行查询。 这意味着表达式的计算会延迟,直到真正循环访问其实现的值或者调用 ToListAsync 方法为止。 有关延迟执行查询的详细信息,请参阅Query Execution(查询执行)。

注意

Contains 方法在数据库中运行,而不是在 C# 代码中运行。 查询是否区分大小写取决于数据库和排序规则。 在 SQL Server 上,Contains 映射到 SQL LIKE,这是不区分大小写的。 具有默认排序规则的 SQLite 是区分大小写和不区分大小写的组合,具体取决于查询。 有关进行不区分大小写的 SQLite 查询的信息,请参阅以下内容:

导航到 /Movies/Index。 将查询字符串(如 ?searchString=Ghost)追加到 URL。 筛选的电影将显示出来。

索引视图

如果将 Index 方法的签名更改为具有名为 id 的参数,id 参数将匹配 Program.cs 中设置的默认路由的可选 {id} 占位符。

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

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

更新后带 id 参数的 Index 方法:

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

现可将搜索标题作为路由数据( URL 段)而非查询字符串值进行传递。

索引视图,显示添加到 URL 的“ghost”一词以及返回的两部电影(Ghostbusters 和 Ghostbusters 2)的电影列表

但是,不能指望用户在每次要搜索电影时都修改 URL。 因此需要添加 UI 元素来帮助他们筛选电影。 若已更改 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

此 HTML <form> 标记使用表单标记帮助程序,因此提交表单时,筛选器字符串会发布到电影控制器的 Index 操作。 保存更改,然后测试筛选器。

显示标题筛选器文本框中键入了 ghost 一词的索引视图

如你所料,不存在 Index 方法的 [HttpPost] 重载。 无需重载,因为该方法不更改应用的状态,仅筛选数据。

可添加以下 [HttpPost] Index 方法。

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

notUsed 参数用于创建 Index 方法的重载。 本教程稍后将对此进行探讨。

如果添加此方法,则操作调用程序将与 [HttpPost] Index 方法匹配,且将运行 [HttpPost] Index 方法,如下图所示。

显示“来自 HttpPost 索引: 筛选 ghost”应用程序响应的浏览器窗口

但是,即使添加 Index 方法的 [HttpPost] 版本,其实现方式也受到限制。 假设你想要将特定搜索加入书签,或向朋友发送一个链接,让他们单击链接即可查看筛选出的相同电影列表。 请注意 HTTP POST 请求的 URL 与 GET 请求的 URL (localhost:{PORT}/Movies/Index) 相同,URL 中没有任何搜索信息。 搜索字符串信息作为表单域值发送给服务器。 可使用浏览器开发人员工具或出色的 Fiddler 工具对其进行验证。 下图展示了 Chrome 浏览器开发人员工具:

Microsoft Edge 开发人员工具的“网络”选项卡,显示了 ghost 的 searchString 值的请求正文

在请求正文中,可看到搜索参数和 XSRF 标记。 请注意,正如之前教程所述,表单标记帮助程序会生成一个 XSRF 防伪令牌。 不会修改数据,因此无需验证控制器方法中的标记。

搜索参数位于请求正文而非 URL 中,因此无法捕获该搜索信息进行书签设定或与他人共享。 修复方法是指定:请求应为 Views/Movies/Index.cshtml 文件中的 HTTP GET

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

现在提交搜索后,URL 将包含搜索查询字符串。 即使具备 HttpPost Index 方法,搜索也将转到 HttpGet Index 操作方法。

URL 中显示 searchString=ghost 且返回了 Ghostbusters 和 Ghostbusters 2 的浏览器窗口包含 ghost 一词

以下标记显示对 form 标记的更改:

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

添加按流派搜索

将以下 MovieGenreViewModel 类添加到“模型”文件夹:

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包含用户在搜索文本框中输入的文本。

MoviesController.cs 中的 Index 方法替换为以下代码:

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

以下代码是一种 LINQ 查询,可从数据库中检索所有流派。

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

通过投影不同的流派创建 SelectList(我们不希望选择列表中的流派重复)。

当用户搜索某个项目时,搜索值会保留在搜索框中。

向“索引”视图添加“按流派搜索”

按如下所示更新 Views/Movies/ 中的 Index.cshtml

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

检查以下 HTML 帮助程序中使用的 Lambda 表达式:

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

在上述代码中,DisplayNameFor HTML 帮助程序检查 Lambda 表达式中引用的 Title 属性来确定显示名称。 由于只检查但未计算 Lambda 表达式,因此当 modelmodel.Movies[0]model.Moviesnull 或空时,你不会收到访问冲突。 对 Lambda 表达式求值时(例如,@Html.DisplayFor(modelItem => item.Title)),将求得该模型的属性值。 model.Movies 后面的 !null 包容运算符,该运算符用于声明 Movies 不为 null。

通过按流派搜索、按电影标题搜索以及按流派和电影标题搜索来测试应用:

浏览器窗口,显示了 https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 的结果

在本部分中,将向 Index 操作方法添加搜索功能,以实现按“类型”或“名称”搜索电影 。

使用以下代码更新 Controllers/MoviesController.cs 中的 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

上面的 s => s.Title!.ToUpper().Contains(searchString.ToUpper()) 代码是 Lambda 表达式。 Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法的参数,如 Where 方法或 Contains(上述代码中所使用的)。 在对 LINQ 查询进行定义或通过调用方法(如 WhereContainsOrderBy)进行修改后,此查询不会被执行。 相反,会延迟执行查询。 这意味着表达式的计算会延迟,直到真正循环访问其实现的值或者调用 ToListAsync 方法为止。 有关延迟执行查询的详细信息,请参阅Query Execution(查询执行)。

注意

Contains 方法在数据库中运行,而不是在 C# 代码中运行。 查询是否区分大小写取决于数据库和排序规则。 在 SQL Server 上,Contains 映射到 SQL LIKE,这是不区分大小写的。 具有默认排序规则的 SQLite 是区分大小写和不区分大小写的组合,具体取决于查询。 有关进行不区分大小写的 SQLite 查询的信息,请参阅以下内容:

导航到 /Movies/Index。 将查询字符串(如 ?searchString=Ghost)追加到 URL。 筛选的电影将显示出来。

索引视图

如果将 Index 方法的签名更改为具有名为 id 的参数,id 参数将匹配 Program.cs 中设置的默认路由的可选 {id} 占位符。

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

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

更新后带 id 参数的 Index 方法:

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

现可将搜索标题作为路由数据( URL 段)而非查询字符串值进行传递。

索引视图,显示添加到 URL 的“ghost”一词以及返回的两部电影(Ghostbusters 和 Ghostbusters 2)的电影列表

但是,不能指望用户在每次要搜索电影时都修改 URL。 因此需要添加 UI 元素来帮助他们筛选电影。 若已更改 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!.ToUpper().Contains(searchString.ToUpper()));
    }

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

此 HTML <form> 标记使用表单标记帮助程序,因此提交表单时,筛选器字符串会发布到电影控制器的 Index 操作。 保存更改,然后测试筛选器。

显示标题筛选器文本框中键入了 ghost 一词的索引视图

如你所料,不存在 Index 方法的 [HttpPost] 重载。 无需重载,因为该方法不更改应用的状态,仅筛选数据。

可添加以下 [HttpPost] Index 方法。

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

notUsed 参数用于创建 Index 方法的重载。 本教程稍后将对此进行探讨。

如果添加此方法,则操作调用程序将与 [HttpPost] Index 方法匹配,且将运行 [HttpPost] Index 方法,如下图所示。

显示“来自 HttpPost 索引: 筛选 ghost”应用程序响应的浏览器窗口

但是,即使添加 Index 方法的 [HttpPost] 版本,其实现方式也受到限制。 假设你想要将特定搜索加入书签,或向朋友发送一个链接,让他们单击链接即可查看筛选出的相同电影列表。 请注意 HTTP POST 请求的 URL 与 GET 请求的 URL (localhost:{PORT}/Movies/Index) 相同,URL 中没有任何搜索信息。 搜索字符串信息作为表单域值发送给服务器。 可使用浏览器开发人员工具或出色的 Fiddler 工具对其进行验证。 下图展示了 Chrome 浏览器开发人员工具:

Microsoft Edge 开发人员工具的“网络”选项卡,显示了 ghost 的 searchString 值的请求正文

在请求正文中,可看到搜索参数和 XSRF 标记。 请注意,正如之前教程所述,表单标记帮助程序会生成一个 XSRF 防伪令牌。 不会修改数据,因此无需验证控制器方法中的标记。

搜索参数位于请求正文而非 URL 中,因此无法捕获该搜索信息进行书签设定或与他人共享。 修复方法是指定:请求应为 Views/Movies/Index.cshtml 文件中的 HTTP GET

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

现在提交搜索后,URL 将包含搜索查询字符串。 即使具备 HttpPost Index 方法,搜索也将转到 HttpGet Index 操作方法。

URL 中显示 searchString=ghost 且返回了 Ghostbusters 和 Ghostbusters 2 的浏览器窗口包含 ghost 一词

以下标记显示对 form 标记的更改:

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

添加按流派搜索

将以下 MovieGenreViewModel 类添加到“模型”文件夹:

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包含用户在搜索文本框中输入的文本。

MoviesController.cs 中的 Index 方法替换为以下代码:

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

以下代码是一种 LINQ 查询,可从数据库中检索所有流派。

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

通过投影不同的流派创建 SelectList(我们不希望选择列表中的流派重复)。

当用户搜索某个项目时,搜索值会保留在搜索框中。

向“索引”视图添加“按流派搜索”

按如下所示更新 Views/Movies/ 中的 Index.cshtml

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

检查以下 HTML 帮助程序中使用的 Lambda 表达式:

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

在上述代码中,DisplayNameFor HTML 帮助程序检查 Lambda 表达式中引用的 Title 属性来确定显示名称。 由于只检查但未计算 Lambda 表达式,因此当 modelmodel.Movies[0]model.Moviesnull 或空时,你不会收到访问冲突。 对 Lambda 表达式求值时(例如,@Html.DisplayFor(modelItem => item.Title)),将求得该模型的属性值。 model.Movies 后面的 !null 包容运算符,该运算符用于声明 Movies 不为 null。

通过按流派搜索、按电影标题搜索以及按流派和电影标题搜索来测试应用:

浏览器窗口,显示了 https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 的结果

在本部分中,将向 Index 操作方法添加搜索功能,以实现按“类型”或“名称”搜索电影 。

使用以下代码更新 Controllers/MoviesController.cs 中的 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 操作方法的第一行创建了 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) 代码是 Lambda 表达式。 Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法的参数,如 Where 方法或 Contains(上述代码中所使用的)。 在对 LINQ 查询进行定义或通过调用方法(如 WhereContainsOrderBy)进行修改后,此查询不会被执行。 相反,会延迟执行查询。 这意味着表达式的计算会延迟,直到真正循环访问其实现的值或者调用 ToListAsync 方法为止。 有关延迟执行查询的详细信息,请参阅Query Execution(查询执行)。

注意:Contains 方法在数据库上运行,而不是在上面显示的 C# 代码中运行。 查询是否区分大小写取决于数据库和排序规则。 在 SQL Server 上,Contains 映射到 SQL LIKE,这是不区分大小写的。 在 SQLite 中,由于使用了默认排序规则,因此需要区分大小写。

导航到 /Movies/Index。 将查询字符串(如 ?searchString=Ghost)追加到 URL。 筛选的电影将显示出来。

索引视图

如果将 Index 方法的签名更改为具有名为 id 的参数,id 参数将匹配 Program.cs 中设置的默认路由的可选 {id} 占位符。

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

更新后带 id 参数的 Index 方法:

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 段)而非查询字符串值进行传递。

索引视图,显示添加到 URL 的“ghost”一词以及返回的两部电影(Ghostbusters 和 Ghostbusters 2)的电影列表

但是,不能指望用户在每次要搜索电影时都修改 URL。 因此需要添加 UI 元素来帮助他们筛选电影。 若已更改 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>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

此 HTML <form> 标记使用表单标记帮助程序,因此提交表单时,筛选器字符串会发布到电影控制器的 Index 操作。 保存更改,然后测试筛选器。

显示标题筛选器文本框中键入了 ghost 一词的索引视图

如你所料,不存在 Index 方法的 [HttpPost] 重载。 无需重载,因为该方法不更改应用的状态,仅筛选数据。

可添加以下 [HttpPost] Index 方法。

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

notUsed 参数用于创建 Index 方法的重载。 本教程稍后将对此进行探讨。

如果添加此方法,则操作调用程序将与 [HttpPost] Index 方法匹配,且将运行 [HttpPost] Index 方法,如下图所示。

显示“来自 HttpPost 索引: 筛选 ghost”应用程序响应的浏览器窗口

但是,即使添加 Index 方法的 [HttpPost] 版本,其实现方式也受到限制。 假设你想要将特定搜索加入书签,或向朋友发送一个链接,让他们单击链接即可查看筛选出的相同电影列表。 请注意 HTTP POST 请求的 URL 与 GET 请求的 URL (localhost:{PORT}/Movies/Index) 相同,URL 中没有任何搜索信息。 搜索字符串信息作为表单域值发送给服务器。 可使用浏览器开发人员工具或出色的 Fiddler 工具对其进行验证。 下图展示了 Chrome 浏览器开发人员工具:

Microsoft Edge 开发人员工具的“网络”选项卡,显示了 ghost 的 searchString 值的请求正文

在请求正文中,可看到搜索参数和 XSRF 标记。 请注意,正如之前教程所述,表单标记帮助程序会生成一个 XSRF 防伪令牌。 不会修改数据,因此无需验证控制器方法中的标记。

搜索参数位于请求正文而非 URL 中,因此无法捕获该搜索信息进行书签设定或与他人共享。 修复方法是指定:请求应为 Views/Movies/Index.cshtml 文件中的 HTTP GET

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

现在提交搜索后,URL 将包含搜索查询字符串。 即使具备 HttpPost Index 方法,搜索也将转到 HttpGet Index 操作方法。

URL 中显示 searchString=ghost 且返回了 Ghostbusters 和 Ghostbusters 2 的浏览器窗口包含 ghost 一词

以下标记显示对 form 标记的更改:

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

添加按流派搜索

将以下 MovieGenreViewModel 类添加到“模型”文件夹:

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包含用户在搜索文本框中输入的文本。

MoviesController.cs 中的 Index 方法替换为以下代码:

// 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(我们不希望选择列表中的流派重复)。

当用户搜索某个项目时,搜索值会保留在搜索框中。

向“索引”视图添加“按流派搜索”

按如下所示更新 Views/Movies/ 中的 Index.cshtml

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

检查以下 HTML 帮助程序中使用的 Lambda 表达式:

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

在上述代码中,DisplayNameFor HTML 帮助程序检查 Lambda 表达式中引用的 Title 属性来确定显示名称。 由于只检查但未计算 Lambda 表达式,因此当 modelmodel.Movies[0]model.Moviesnull 或空时,你不会收到访问冲突。 对 Lambda 表达式求值时(例如,@Html.DisplayFor(modelItem => item.Title)),将求得该模型的属性值。

通过按流派搜索、按电影标题搜索以及按流派和电影标题搜索来测试应用:

浏览器窗口,显示了 https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 的结果

在本部分中,将向 Index 操作方法添加搜索功能,以实现按“类型”或“名称”搜索电影 。

使用以下代码更新 Controllers/MoviesController.cs 中的 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 操作方法的第一行创建了 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() 代码是 Lambda 表达式。 Lambda 在基于方法的 LINQ 查询中用作标准查询运算符方法的参数,如 Where 方法或 Contains(上述代码中所使用的)。 在对 LINQ 查询进行定义或通过调用方法(如 WhereContainsOrderBy)进行修改后,此查询不会被执行。 相反,会延迟执行查询。 这意味着表达式的计算会延迟,直到真正循环访问其实现的值或者调用 ToListAsync 方法为止。 有关延迟执行查询的详细信息,请参阅Query Execution(查询执行)。

注意:Contains 方法在数据库上运行,而不是在上面显示的 C# 代码中运行。 查询是否区分大小写取决于数据库和排序规则。 在 SQL Server 上,Contains 映射到 SQL LIKE,这是不区分大小写的。 在 SQLite 中,由于使用了默认排序规则,因此需要区分大小写。

导航到 /Movies/Index。 将查询字符串(如 ?searchString=Ghost)追加到 URL。 筛选的电影将显示出来。

索引视图

如果将 Index 方法的签名更改为具有名为 id 的参数,id 参数将匹配 Startup.cs 中设置的默认路由的可选 {id} 占位符。

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

更新后带 id 参数的 Index 方法:

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 段)而非查询字符串值进行传递。

索引视图,显示添加到 URL 的“ghost”一词以及返回的两部电影(Ghostbusters 和 Ghostbusters 2)的电影列表

但是,不能指望用户在每次要搜索电影时都修改 URL。 因此需要添加 UI 元素来帮助他们筛选电影。 若已更改 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>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>

此 HTML <form> 标记使用表单标记帮助程序,因此提交表单时,筛选器字符串会发布到电影控制器的 Index 操作。 保存更改,然后测试筛选器。

显示标题筛选器文本框中键入了 ghost 一词的索引视图

如你所料,不存在 Index 方法的 [HttpPost] 重载。 无需重载,因为该方法不更改应用的状态,仅筛选数据。

可添加以下 [HttpPost] Index 方法。

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

notUsed 参数用于创建 Index 方法的重载。 本教程稍后将对此进行探讨。

如果添加此方法,则操作调用程序将与 [HttpPost] Index 方法匹配,且将运行 [HttpPost] Index 方法,如下图所示。

显示“来自 HttpPost 索引: 筛选 ghost”应用程序响应的浏览器窗口

但是,即使添加 Index 方法的 [HttpPost] 版本,其实现方式也受到限制。 假设你想要将特定搜索加入书签,或向朋友发送一个链接,让他们单击链接即可查看筛选出的相同电影列表。 请注意 HTTP POST 请求的 URL 与 GET 请求的 URL (localhost:{PORT}/Movies/Index) 相同,URL 中没有任何搜索信息。 搜索字符串信息作为表单域值发送给服务器。 可使用浏览器开发人员工具或出色的 Fiddler 工具对其进行验证。 下图展示了 Chrome 浏览器开发人员工具:

Microsoft Edge 开发人员工具的“网络”选项卡,显示了 ghost 的 searchString 值的请求正文

在请求正文中,可看到搜索参数和 XSRF 标记。 请注意,正如之前教程所述,表单标记帮助程序会生成一个 XSRF 防伪令牌。 不会修改数据,因此无需验证控制器方法中的标记。

搜索参数位于请求正文而非 URL 中,因此无法捕获该搜索信息进行书签设定或与他人共享。 修复方法是指定:请求应为 Views/Movies/Index.cshtml 文件中的 HTTP GET

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

现在提交搜索后,URL 将包含搜索查询字符串。 即使具备 HttpPost Index 方法,搜索也将转到 HttpGet Index 操作方法。

URL 中显示 searchString=ghost 且返回了 Ghostbusters 和 Ghostbusters 2 的浏览器窗口包含 ghost 一词

以下标记显示对 form 标记的更改:

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

添加按流派搜索

将以下 MovieGenreViewModel 类添加到“模型”文件夹:

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包含用户在搜索文本框中输入的文本。

MoviesController.cs 中的 Index 方法替换为以下代码:

// 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(我们不希望选择列表中的流派重复)。

当用户搜索某个项目时,搜索值会保留在搜索框中。

向“索引”视图添加“按流派搜索”

按如下所示更新 Views/Movies/ 中的 Index.cshtml

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

检查以下 HTML 帮助程序中使用的 Lambda 表达式:

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

在上述代码中,DisplayNameFor HTML 帮助程序检查 Lambda 表达式中引用的 Title 属性来确定显示名称。 由于只检查但未计算 Lambda 表达式,因此当 modelmodel.Movies[0]model.Moviesnull 或空时,你不会收到访问冲突。 对 Lambda 表达式求值时(例如,@Html.DisplayFor(modelItem => item.Title)),将求得该模型的属性值。

通过按流派搜索、按电影标题搜索以及按流派和电影标题搜索来测试应用:

浏览器窗口,显示了 https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 的结果