創建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查詢在被定義或者通過調用類似於Where
、Contains
、OrderBy
的方法進行修改的時候不會執行。相反的,查詢會延遲執行,比如在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
類,所以你需要更新所綁定的白名單,這樣新的屬性將會包含這內。為Create
和Edit
方法更新[Bind]
特性以包含Height
屬性。
[Bind("ID,Name,Email,Bio,Height,Title,Dept")]
為了顯示新的字段還要更新視圖模板。
打開Index.cshtml、Create.cshtml、Edit.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>
最終運行的頁面: