目錄
Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一個課程中,我們已經學習了如何使用 EF 對 Student 實體進行增、刪、改、查處理。這次的課程我們將對學生的 Index 頁面加入排序、過濾以及分頁的功能。還要創建一個頁面完成簡單的分組。
下面的截圖展示了完成之后的頁面,列的標題作為鏈接支持用戶通過點擊完成排序,點擊標題可以在升序和降序之間進行切換。
3-1 在 Students 的 Index 頁面增加列標題鏈接
為 Index 頁面增加排序的功能,我們需要修改 Student 控制器的 Index 方法,還需要為 Student 視圖增加代碼。
3-1-1 為 Index 方法增加排序功能
打開 Controllers\StudentController.cs,將 Index 方法替換為如下的代碼。
public ViewResult Index(string sortOrder)
{
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
var students = from s in db.Students
select s;
switch (sortOrder)
{
case "Name desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "Date desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(students.ToList());
}
這段代碼從 URL 中接收名為 sortOrder 的參數,這個參數由 ASP.NET MVC 作為參數傳遞給 Action 方法。這個參數可以是 “Name” 或者 “Date”, 可能還有一個空格隔開的 desc 來指定降序。
當第一次請求 Index 的時候,沒有參數,學生使用 LastName 的升序順序顯示。這是通過 switch 的 default 代碼段指定的,當用戶點擊一個列的標題鏈接的時候,合適的 sortOrder 值需要通過查詢字符串傳遞進來。
兩個 ViewBag 變量用來為視圖提供合適的查詢字符串鏈接值。
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
這里使用了條件語句,第一個用來指定當 sortOrder 參數為 null 或者空串的時候, ViewBag.NameSortParm 應用被設置為 Name desc,其他情況下,應該被設置為空串。
這里有四種可能,依賴於當前的排序情況:
- 如果當前的排序規則為 LastName 升序,那么,LastName 鏈接應該設置為降序,Enrollment Date 鏈接必須被設置為按日期升序。
- 如果當前的排序規則為 LastName 降序,那么,LastName 鏈接應該設置為升序,排序串應該為空串,日期為升序。
- 如果當前排序的規則為 Date 升序,那么,鏈接應該為 LastName 升序和日期升序。
- 如果當前的排序規則為 Date 降序,那么,鏈接應該為 LastName 升序和日期降序。
方法中使用 LINQ to Entities 來指定排序,在 switch 之前,代碼首先創建一個 IQueryable 變量,在 switch 語句中修改這個查詢表達式,最后調用 ToList 方法。在創建和修改查詢表達式 IQueryable 的時候,並沒有將查詢發送到數據庫中執行,查詢直到將 IQueryable 對象駝工調用類似 ToList 方法轉換到集合對象的時候才會執行,因此,代碼中查詢直到最后的 return View 才會被執行。
3-2-2 為 Index 視圖增加列標題鏈接
在 Views\Student\Index.cshtml,使用如下的代碼替換標題行中的 <tr> 和 <th> 元素。
<tr>
<th></th>
<th>
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm })
</th>
<th>
First Name
</th>
<th>
@Html.ActionLink("Enrollment Date", "Index", new { sortOrder=ViewBag.DateSortParm })
</th>
</tr>
這段代碼使用 ViewBag 屬性來設置超級鏈接中包含適當的查詢字符串。
運行頁面,點擊列標題,來驗證排序是否正常。
3-2 為 Index 頁面增加搜索框
為 Index 頁面增加過濾功能,需要增加一個文本框和一個提交按鈕,然后,對 Index 方法進行一些修改,文本框允許你輸入一個搜索字符串,用來在 FirstName 和 LastName 中進行搜索。
3-2-1 為 Index 方法增加過濾功能
打開 Controllers\StudentController.cs 文件,使用下面的代碼替換 Index 方法。
public ViewResult Index(string sortOrder, string searchString)
{
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
var students = from s in db.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
switch (sortOrder)
{
case "Name desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "Date desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(students.ToList());
}
現在,為 Index 方法增加了一個參數 searchString ,LINQ 語句中也增加了一個 where 子句,用來選擇在 FirstName 或者 LastName 中包含過濾字符串的學生。搜索串來自文本框的輸入,后面需要你在視圖中加入它。增加的 where 條件子句僅僅在提供了搜索串的情況下才會被處理。
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
注意:在傳遞一個空串的時候,.NET 實現的 Contains 方法將會返回所有的數據行,但是 EF Provider for SQL Server Compact 4.0 對於空串不返回任何行。因此,代碼中增加了一個 if 判斷語句,以確保對於所有的 SQL Server 都有一致的處理結果。另外,.NET 實現的 Contains 默認進行區分大小寫的字符串比較,因此,通過調用 ToUpper 方法顯式轉換字符串為大寫,
以確保在轉換到使用資源庫模式的時候不需要修改代碼。那個時候將會返回一個 IEnumerable 集合而不是 IQueryable 對象 ( 在調用 IEnumerable 集合上的 Contains 方法的時候,使用 .NET 實現的方法,在調用 IQueryable 對象上的 Contains 方法的時候,使用數據庫 Provider 提供的實現 )。
3-2-2 在 Student 視圖上加入搜索框
在視圖 Views\Student\Index.cshtml 上,table 開始標記之前,增加一個標題,一個文本框,以及一個 Search 按鈕。
@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString")
<input type="submit" value="Search" /></p>
}
運行程序,輸入一個搜索串,然后點擊 Search 按鈕來查看過濾的效果。
3-3 在 Student 的 Index 視圖上增加分頁
為了支持分頁,你需要通過 NuGet 包管理器安裝 PagedList ,然后,需要在 Index 方法中增加一些代碼,在視圖中增加分頁的鏈接,下面的截圖展示了分頁的鏈接。
3-3-1 安裝 PagedList 包
NuGet 中的 PagedList 包將會增加一種類型:PagedList,當將查詢結果傳入到 PagedList 中后,它提供的一系列屬性和方法使得排序更加簡單。
在 Visual Studio 中,確信選中了當前的項目,而不是解決方案。在 Tools 菜單中,選擇 Library Package Manager,然后選擇 Add Library Package Reference。
在 Add Library Package Reference 對話框中,點擊左邊的 Online 窗格,然后在搜索框中輸入 pagedlist ,在看到 PagedList 包之后,點擊 Install。
3-3-2 為 Index 方法增加分頁功能
打開 Controllers\StudentController.cs,在代碼的前面為 PagedList 命名空間增加 using 語句.
using PagedList;
將 Index 方法替換成如下的代碼。
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
{
ViewBag.CurrentSort = sortOrder;
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;
var students = from s in db.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
switch (sortOrder)
{
case "Name desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "Date desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
int pageNumber = (page ?? 1);
return View(students.ToPagedList(pageNumber, pageSize));
}
方法又增加了一個 page 參數,方法的簽名如下所示。
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
當第一次顯式這個頁面的時候,或者用戶沒有點擊分頁鏈接的時候,page 參數將會是 null。如果分頁鏈接被點擊了,page 參數將會包含需要顯示的頁碼。
ViewBag 中的 CurrentSort 屬性用來提供當前的排序順序,它必須被包含到當前的分頁鏈接中,以便在分頁處理過程中保持當前的排序規則。
ViewBag.CurrentSort = sortOrder;
其它的 ViewBag 屬性為視圖提供當前的過濾串,因為這個過濾串在頁面被重新顯示的時候,必須重新回到文本框中,另外,這個串也必須包含在分頁鏈接中,以便在分頁過程中,保持過濾效果。最后,如果在分頁的過程中修改了過濾串,那么頁碼將會回到第一頁,因為新的過濾規則返回了不同的數據,很可能原來的頁碼在這時候已經不再存在了。
if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;
在方法的最后,查詢學生的表達式被轉換為 PagedList ,而不再是通常的 List,這樣傳遞到視圖中的就是支持分頁的集合,代碼如下:
int pageSize = 3;
int pageNumber = (page ?? 1);
return View(students.ToPagedList(pageNumber, pageSize));
ToPagedList 方法需要一個頁碼值,兩個問號用來為可空的頁碼提供一個默認值,表達式 ( page ?? 1 ) 意味着如果 page 有值得話返回這個值,如果是 null 的話,返回 1。
3-3-3 為視圖增加分頁鏈接
在 Views\Student\Index.cshtml中,使用下面的代碼替換原有代碼。
@model PagedList.IPagedList<ContosoUniversity.Models.Student>
@{
ViewBag.Title = "Students";
}
<h2>Students</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
<input type="submit" value="Search" /></p>
}
<table>
<tr>
<th></th>
<th>
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
</th>
<th>
First Name
</th>
<th>
@Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.StudentID }) |
@Html.ActionLink("Details", "Details", new { id=item.StudentID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.StudentID })
</td>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
</tr>
}
</table>
<div>
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber)
of @Model.PageCount
@if (Model.HasPreviousPage)
{
@Html.ActionLink("<<", "Index", new { page = 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink("< Prev", "Index", new { page = Model.PageNumber - 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:<<
@Html.Raw(" ");
@:< Prev
}
@if (Model.HasNextPage)
{
@Html.ActionLink("Next >", "Index", new { page = Model.PageNumber + 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink(">>", "Index", new { page = Model.PageCount, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:Next >
@Html.Raw(" ")
@:>>
}
</div>
視圖最前面的 @model 語句指定現在傳遞到視圖的不再是 List 而是 PagedList 。
文本框使用當前的搜索串進行初始化,以便在分頁的時候不會丟失搜索串。
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
列的標題鏈接使用查詢串來傳遞當前的搜索串,以便傳遞給控制器當前的搜索和排序。
@Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
在當前頁面的最后,通過一行來顯示分頁的導航 UI。
Page [current page number] of [total number of pages] << < Prev Next > >>
<< 符號連接到第一頁, < Prev 鏈接到上一頁,等等。如果用戶當前就在第一頁,那么,鏈接到第一頁的鏈接就會被禁用,類似地,如果用戶當前在最后一頁,導航到最后一頁就會被禁用,每一個分頁鏈接傳遞頁碼以及當前的排序串和搜索串到控制器,這使得可以在分頁的同時維護排序和過濾規則。
如果沒有頁可以顯示,將會顯示 “Page 0 of 0 “,在這種情況下,頁面數字就會大於頁數,因為 Model.PageNumber 是 1,但是 Model.PageCount 為 0。
運行頁面。
在不同的排序規則下,點擊分頁鏈接,確認分頁在正常工作。然后輸入一個過濾串,再次點擊分頁的鏈接,確認在排序和過濾的同時,分頁可以正常工作。
3-4 創建 About 頁面顯示學生的統計情況
在 Contoso 大學網站的 About 頁面,我們希望能夠顯示每個注冊日有多少學生注冊。這需要進行分組,然后在每個組上進行簡單地計算,需要完成下列工作:
- 創建用於傳遞數據到視圖的 ViewModel
- 修改 Home 控制器中的 About 方法
- 修改 About 視圖
3-4-1 創建 ViewModel
創建 ViewModels 文件夾,在文件夾中,創建 EnrollmentDateGroup.cs 類文件,將代碼替換為如下代碼:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.ViewModels
{
public class EnrollmentDateGroup
{
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
3-4-2 修改 Home 控制器
增加如下的 using 語句。
using ContosoUniversity.DAL;
using ContosoUniversity.Models;
using ContosoUniversity.ViewModels;
增加一個數據庫上下文變量。
private SchoolContext db = new SchoolContext();
使用如下的代碼替換 About 方法。
public ActionResult About()
{
var data = from student in db.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(data);
}
LINQ 語句通過注冊日期對學生進行分組,計算每一組中的實體數量,最后將查詢結果保存為 EnrollmentDateGroup
對象。
3-4-3 增加 Dispose 方法
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
3-4-4 修改 About 視圖
打開 Views\Home\About.cshtml ,替換為如下代碼。
@model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup>
@{
ViewBag.Title = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@String.Format("{0:d}", item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
運行頁面,每個注冊日注冊學生的數量顯示在表格中。
現在,你已經看到了如何創建數據模型,以及實現基本的增、刪、改、查處理,排序、過濾、分頁和分組功能。下一次,我們將會擴展數據模型開始更加高級的內容。