创建ASP.NET Core MVC应用程序(5)-添加查询功能 & 新字段

创建ASP.NET Core MVC应用程序(5)-添加查询功能 & 新字段

添加查询功能

本文将实现通过Name查询用户信息。

首先更新GetAll方法以启用查询:

public async Task<IEnumerable<User>> GetAll(string searchString)
{
    var users = from u in _context.Users
                select u;

    if (!string.IsNullOrEmpty(searchString))
    {
        users = users.Where(u => u.Name.Contains(searchString));
    }

    return await users.ToListAsync();
}

第一行的LINQ查询仅仅在这里作了定义,并没有在这里实际操作数据库。

LINQ查询在被定义或者通过调用类似于WhereContainsOrderBy的方法进行修改的时候不会执行。相反的,查询会延迟执行,比如在ToListAsync方法被调用之后。

Contains方法会在数据库中运行,而不是上面的C#代码,在数据库中,Contains会被映射成不区分大小写的SQLLIKE

由于u => u.Name.Contains(searchString)Lambda表达式在当前我所使用的MySQL Connector/NET版本中运行报错(新版本好像已经修复了该问题,这里懒得更新了),所以这里先改成:

users = users.Where(u => u.Name == searchString);

导航到http://localhost:5000/User,添加?searchString=Zhu查询字符串,将过滤出指定的用户信息(http://localhost:5000/User?searchString=Zhu)。

如果你修改Index方法的签名使得方法包含一个名为id的参数,那么id参数将会匹配Startup.cs文件中设置的默认路由的可选项{id?}

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Rename后的方法:

public async Task<IActionResult> Index(string id)
{
    var users = from u in _context.Users
                select u;
    if (!String.IsNullOrEmpty(id))
    {
        users = users.Where(u => u.Name == id);
    }

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

现在你可以传递这个Name查询条件作为路由数据(URL Segment)来代替查询字符串(http://localhost:5000/User/Index/Zhu)。

然而,我们不能指望用户每次都通过修改URL来进行查询,我们通过添加UI来进行查询。如果你想改变Index方法的签名来测试怎样传递路由绑定ID参数,将searchString参数改回来。

接着,打开Views/User/Index.cshtml文件,添加<form>标记:

<form asp-controller="User" asp-action="Index">
    <p>
        姓名: <input type="text" name="SearchString">
        <input type="submit" value="过滤" />
    </p>
</form>

这里HTML<form>标签使用了Form Tag Helper,所以当你提交这个表单时,过滤字符串会被传递到User控制器的Index方法中。

这里没有使用你所希望的[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方法,最终仍然存在局限性。想象你将一个指定的查询作为书签或者将查询结果作为一个链接发送给你的朋友以便他们在打开时能看到同样的过滤结果时,注意HTTP POST请求的URL和GET请求的URL(http://localhost:5000/User)是一样的-在URL里面没有任何查询信息。查询信息是作为表单数据发送到服务器的。

在请求体中可以看到查询参数和XSRF反伪造标记(通过Form Tag Helper生成),由于查询没有修改数据,所以无需在控制器方法中验证该标记。

为了把查询参数从请求体中移到URL中,必须把请求指定为HTTP GET

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

此时当你再提交时,URL将会包含具体的查询条件。查询将会跳转到HttpGet Index方法,即使存在着HttpPost Index方法。

添加新的字段

我们将通过Entity Framework Code First Migrations工具来添加一个新的字段到模型中,并将新的改变同步到数据库中。

当你使用EF Code First自动创建一个数据库时,Code First会添加一个表到数据库中帮助跟踪数据库的数据结构是否和模型类保持同步。如果不同步,EF会抛出一个异常。

添加身高属性到模型类

public class User
{
    public int ID { get; set; }
    [Display(Name = "姓名")]
    public string Name { get; set; }
    [Display(Name = "邮箱")]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
    [Display(Name = "简介")]
    public string Bio { get; set; }

    // 新添加的属性
    [Display(Name = "身高")]
    public decimal Height { get; set; }

    [Display(Name = "职称")]
    public string Title { get; set; }

    [Display(Name = "部门")]
    public string Dept { get; set; }
}

Build该应用(dotnet build)。可能由于我所使用的MySQL EF Core provider还不成熟,添加DateTime类型的字段时会报错(新版本可能会修复该问题,这里懒得去测试了)。

由于你添加了一个新的字段到User类,所以你需要更新所绑定的白名单,这样新的属性将会包含这内。为CreateEdit方法更新[Bind]特性以包含Height属性。

[Bind("ID,Name,Email,Bio,Height,Title,Dept")]

为了显示新的字段还要更新视图模板。

打开Index.cshtmlCreate.cshtmlEdit.cshtml等文件,添加新的Height字段。

然后需要更新数据库以包含新的字段。如果没有更新,此时运行将报错,因为更新后User模型类和已存在的数据库User表的结构不一致。

有以下几种方案来解决该错误:

  • EF可以基于新的模型类自动删除并重建数据库。开发阶段在测试数据库上做开发还是比较方便的,但是你会丢失数据库中的现有数据。因此该方案不适用于生产环境数据库!

  • 显式修改现有数据库的结构,使得它与模型类相匹配,这个方案可以让你保留数据库的现有数据。你可以通过手动或者数据库脚本来进行变更。

  • 使用Code First Migrations来更新数据库结构。

这里采用第三种方案,运行如下命令:

dotnet ef migrations add Height
dotnet ef database update

migrations add命令告诉Migration框架去检查当前的User模型类和当前的User数据库表结构是否一致。如果不一致,就创建必要的代码来迁移数据库到新的模型类。

基于新添加的“部门”字段进行查询

Models目录下添加UserDeptViewModel类:

using Microsoft.AspNetCore.Mvc.Rendering;

public class UserDeptViewModel
{
    public List<User> users;
    public SelectList depts;
    public string userDept { get; set; }
}

这个视图模型(View Model)将包含:

  • 用户列表users
  • 包含部门列表(depts)的SelectList,将用于视图页面中允许用户去从列表中选择一个部门。
  • userDept,包含用户所选中的部门(dept)。

修改Index方法:

public async Task<IActionResult> Index(string userDept, string searchString)
{
    IQueryable<string> deptQuery = from u in _context.Users
                                    orderby u.Dept
                                    select u.Dept;

    var users = from u in _context.Users
                select u;
    if (!String.IsNullOrEmpty(searchString))
    {
        users = users.Where(u => u.Name == searchString);
    }

    if (!String.IsNullOrEmpty(userDept))
    {
        users = users.Where(u => u.Dept == userDept);
    }

    var userDeptVM = new UserDeptViewModel();
    userDeptVM.depts = new SelectList(await deptQuery.Distinct().ToListAsync());
    userDeptVM.users = await users.ToListAsync();

    return View(userDeptVM);
}

下面的代码通过LINQ查询从数据库获取所有的部门数据。

IQueryable<string> deptQuery = from u in _context.Users
                                    orderby u.Dept
                                    select u.Dept;

depts的SelectList是通过投影不重复的部门(Distinct)创建的。

userDeptVM.depts = new SelectList(await deptQuery.Distinct().ToListAsync());

在Index视图中添加“部门”查询字段

首先删除最顶部的@model:

@model IEnumerable<MyFirstApp.Models.User>

替换成:

@model UserDeptViewModel

<form>标记中添加:

<select asp-for="userDept" asp-items="Model.depts">
    <option value="">所有</option>
</select>

<table>标记中分别删除:

@Html.DisplayNameFor(model => model.Dept)

@foreach (var item in Model) {

替换成:

@Html.DisplayNameFor(model => model.users[0].Dept)
@foreach (var item in Model.users) {

最终的Index.cshtml文件如下:

@model UserDeptViewModel

@{
    ViewData["Title"] = "Index - User List";
}

<h2>�首页 - 用户列表</h2>

<p>
    <a asp-action="Create">�新建</a>
</p>

<form asp-controller="User" asp-action="Index" method="GET">
    <p>

        <select asp-for="userDept" asp-items="Model.depts">
            <option value="">所有</option>
        </select>

        姓名: <input type="text" name="SearchString">
        <input type="submit" value="过滤" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Height)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Dept)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.users[0].Bio)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.users) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Height)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Dept)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Bio)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">编辑</a> |
                <a asp-action="Details" asp-route-id="@item.ID">详情</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">删除</a>
            </td>
        </tr>
}
    </tbody>
</table>

最终运行的页面:

个人博客

我的个人博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容