7 天玩轉 ASP.NET MVC — 第 3 天
0. 前言
我們假定你在開始學習時已經閱讀了前兩天的學習內容。在第 2 天我們完成了關於顯示 Employees 列表的項目。
在第三天,我們將會通過介紹數據訪問層和數據入口將它升級到一個新的層次。
1. 數據訪問層
在真實場景的項目中,如果沒有 Database,那麼這個項目是未完成的。在我們的項目中,我們還沒有談到資料庫。第三天的首個 Lab 將會學習資料庫和資料庫層。
這裡我們將使用 SQL Server 和 Entity Framework 來創建 Database 和 Database 訪問層。
簡單來說,什麼是 Entity Framework?
這是一個 ORM 工具。ORM 代表的是 Object Relational Mapping。即:對象關係映射。
Advertisements
在 RDBMS 領域中,我們所談論的 Tables 表格和 Columns 列的這些方面,在 .NET 領域(面向對象領域)中被稱為 Classes 類,對象和屬性。
當我們思考任何有關數據驅動應用的方式時,都可以得出以下兩種方式:
書寫代碼來和資料庫打交道(被稱為數據訪問層和資料庫邏輯)
書寫代碼來將資料庫數據映射到面向對象中,反之亦然。
ORM 是一個工具,可以自動做如上兩件事。Entity Framework 是微軟的 ORM 工具。
什麼是 Code First 方法?
在 Entity Framework 中,我們可以使用如下三種的任意方法:
Database First 方法。創建一個有表,列和關係的資料庫。Entity Framework 將會生成對應的 Model 類(業務實體)和數據訪問層代碼。
Advertisements
Model First 方法。在這個方法中,Model 類和它們之間的聯繫將會被 Model 設計者在 Visual Studio 中被手動定義。然後 Entity Framework 會自動創建數據訪問層和擁有表、列以及關係的資料庫。
Code First 方法。在這個方法中,手動創建 POCO 類。這些類中的關係將會被代碼所定義。當應用第一次執行時,Entity Framework 將會自動在資料庫伺服器上創建數據訪問層和擁有表、列以及關係的資料庫。
什麼是 POCO 類?
POCO 代表的是「Plain Old CLR Objects」。POCO 類代表的是我們所創建的簡單 .NET 類。在我們之前的例子中, Employee 類是一個簡單的 POCO。
2. Lab 8 — 向項目中添加數據訪問層
第一步:創建資料庫
連接 SQL Server 然後創建一個新的資料庫,命名為「SalesERPDB」。
第二步:創建 ConnectionString
打開 Web.config 文件,然後在 Configuration 區域內添加如下片段:
<connectionStrings>
<add connectionString="Data Source=(local);Initial Catalog=SalesERPDB;Integrated Security=True"
name="SalesERPDAL"
providerName="System.Data.SqlClient"/>
</connectionStrings>
第三步:添加 Entity Framework 引用
右擊項目-> 管理 Nuget 包。搜索 Entity Framework,然後點擊安裝。
第四步:創建數據訪問層
在根目錄下創建一個新文件夾,命名為「DataAccessLayer」,然後在裡面創建一個新的類,命名為「SalesERPDAL」。
在類頂部寫引用聲明如下 using System.Data.Entity;
繼承 DbContext 的類「SalesERPDAL」
public class SalesERPDAL: DbContext
{
}
第五步:為 Employee 類創建主鍵
打開 Employee 類並在類頂部聲明如下:
using System.ComponentModel.DataAnnotations;
在 Employee 類中添加 EmployeeId 屬性,然後將其標註為 Key 屬性。
public class Employee
{
[Key]
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Salary { get; set; }
}
第六步:定義映射
在「SalesERPDAL」類中添加如下聲明語句:
using WebApplication1.Models;
在 SalesERPDAL 類中重寫 OnModelCreating 方法。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<employee>().ToTable("TblEmployee");
base.OnModelCreating(modelBuilder);
}
注意:上述代碼中的片段「TblEmployee」代表的是表名。在運行時講自動被創建。
第七步:在資料庫中創建 Employees 屬性
在「SalesERPDAL」類中創建一個新屬性,命名為 Employee,如下所示:
public DbSet<employee> Employees{get;set;}
DbSet 將會展示所有可以在資料庫中查詢到的 Employees。
第八步:改變業務層代碼,從資料庫中讀取數據
打開 EmployeeBusinessLayer 類,在頂部加上聲明如下:
using WebApplication1.DataAccessLayer;
現在改變 GetEmployees 方法如下:
public List<employee> GetEmployees()
{
SalesERPDAL salesDal = new SalesERPDAL();
return salesDal.Employees.ToList();
}
第九步:執行並測試
按下 F5,並執行應用。
現在的資料庫中,我們沒有任何的 Employees,所以我們看見的是一個空白的 Grid。
查看資料庫,現在我們可以在 TblEmployee 表中看到所有的列。
第十步:插入測試數據
向 TblEmployee 表中插入一些測試數據。
第十一步:執行並測試應用
按下 F5 並再次運行應用。
Lab 8 的 Q&A
什麼是 DbSet?
DbSet 簡單地表示了可以從資料庫中查詢到的實體集合。當我們再次寫一個 Linq 查詢時,DbSet 對象會對查詢進行內存轉換,然後觸發資料庫。
在我們的例子中,「Employee」是一個 DbSet,它承載了所有可以從資料庫中查詢到的 Employee 實體對象。每一次我們嘗試訪問「Employees」時,它都將從「TblEmployee」表中獲取記錄,然後將其轉換為「Employees」對象並返回集合。
資料庫連接串和數據訪問層是如何連接的?
Mapping 通過名稱來實現。在我們的例子中,ConnectionString 名稱和數據訪問層類的名稱是一樣的,即「SalesERPDAL」,因此它們是自動映射的。
我們可以更改 ConnectionString 的名稱嗎?
答案是肯定的。在這個例子中,我們需要在數據訪問層類中定義一個構造函數如下:
public SalesERPDAL():base("NewName")
{
}
3. 組織所有
我們需要做幾個改變,使得所有是有組織和有意義的。
第一步:重命名
「TestController」換名為 「EmployeeController」。
GetView 行為方法改為 Index。
Test 文件夾(Views 文件夾下) 改為 Employee
「MyView」視圖改為「Index」。
第二步:從 EmployeeListViewModel 中刪除 UserName 屬性
第三步:從視圖中刪除 UserName
打開 View/Employee.Index.cshtml 視圖,然後從中刪除 UserName。
簡單來說,就是刪除如下代碼:
Hello @Model.UserName
<hr />
第四步:在 EmployeeController 中更改 Index 行為方法
更改 EmployeeController 中的 Index 行為方法如下:
public ActionResult Index()
{
……
……
……
employeeListViewModel.Employees = empViewModels;
//employeeListViewModel.UserName = "Admin";-->Remove this line -->Change1
return View("Index", employeeListViewModel);//-->Change View Name -->Change 2
}
現在執行的 URL 將會為:「…/Employee/Index」。
4. Lab 9 — 創建 Data Entry Screen
第一步:創建 Action 方法
在 EmployeeController 中創建一個 Action 方法,命名為「AddNew」,如下:
public ActionResult AddNew()
{
return View("CreateEmployee");
}
第二步:創建 View
在文件夾 View/Employee 下創建一個 View,命名為「CreateEmployee」。代碼如下:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>CreateEmployee</title>
</head>
<body>
<div>
<form action="/Employee/SaveEmployee" method="post">
First Name: <input type="text" id="TxtFName" name="FirstName" value="" /><br />
Last Name: <input type="text" id="TxtLName" name="LastName" value="" /><br />
Salary: <input type="text" id="TxtSalary" name="Salary" value="" /><br />
<input type="submit" name="BtnSave" value="Save Employee" />
<input type="button" name="BtnReset" value="Reset" />
</form>
</div>
</body>
</html>
第三步:在 Index 視圖中創建一個鏈接
打開 Index.cshtml,然後增加一個超鏈接指向 AddNew 行為的URL。
<ahref="/Employee/AddNew">Add New</a>
第四步:執行並測試應用
按下 F5 並執行應用。
Lab 9 的 Q&A
Form 標籤的目的是什麼?
在第一天的系列學習中,我們已經明白了「Web 世界不會遵循事件驅動編程模型。它遵循的是請求響應模型。終端用戶發出請求,然後伺服器給出響應。」Form 標籤是 HTML 中做出響應的其中一種方式。只要標籤里的提交按鈕被點擊,一個請求就將發送給動作屬性中指定的 URL 中。
Form 標籤中的方法屬性是什麼?
它決定了請求的類型。請求也許是如下的其中一種:get、post、put 或者是 delete。
Get:當我們想獲取什麼數據時
Post:當我們想創建什麼數據時
Put:當我們想更新什麼數據時
Delete:當我們想刪除什麼數據時
運用 Form 標籤和通過瀏覽器地址欄或者超鏈接來發出請求,有何區別?
當我們使用 Form 標籤來發送請求時,所有輸入控制項中的值都會伴隨著請求一起被處理。
Checkbox、Radio 按鈕和 Dropdowns 控制項中的值也會被發送嗎?
答案是肯定的。所有輸入控制項(輸入類型為 Text,Radio,Checkbox)以及 Dropdowns(表示的是被選中的元素)都會被發送。
輸入的值如何發送給伺服器?
當請求的類型是 Get、Put 或者 Delete 時,輸入的值會以查詢字元串參數的方式發送。
當請求的類型是 Post 時,輸入的值會以 Post 數據發送。
輸入控制項中的 Name 屬性的目的是什麼?
就像我們之前所談論的,當按鈕被點擊時,輸入控制項中的值將會隨著請求一起被發送。這會使得伺服器在這個時刻接收到多於一個的值。為了在發送值的時候,單獨區別每一個值,就會為它們附加上一個 Key,這個 Key 就是「Name」屬性。
Name 和 Id 屬性的目的是否是相同的?
答案是否定的。就像剛才的問題所說,當請求發生時,「Name」屬性被 HTML 所使用,而「Id」屬性被開發者所使用,為一些 JavaScript 實現一些動態功能。
「input type = submit」 和 「input type = button」有什麼區別?
當我向伺服器發送請求時,Submit 按鈕會被特殊用到。而一個簡單的按鈕是用來處理一些客戶端的行為的。簡單的按鈕不會自己做一些事情。
5. Lab 10 — 在伺服器/Controller 獲取 Posted 數據
第一步:創建 SaveEmployee 行為方法
在 Employee 控制器中創建一個行為方法,命名為 SaveEmployee,代碼如下:
public string SaveEmployee(Employee e)
{
return e.FirstName + "|"+ e.LastName+"|"+e.Salary;
}
第二步:執行並測試
按下 F5 並執行應用。
Lab 10 的 Q&A
在 Action 方法里,Textbox 的值是如何更新 Employee 對象的?
在 ASP.NET MVC 中,存有一個概念,叫做 Model Binder。
無論何時一個包含參數的請求向 Action 方法發送時,Model Binder 都會自動執行。
Model Binder 將會遍歷方法的所有原始參數,然後將它們與發送過來的數據的參數的名稱相對比。(發送過來的數據意味著要麼是 Posted 數據,或者是查詢字元串)。當匹配成功時,會依照發送過來的數據分配給參數。
當 Model Binder 遍歷完每一個類參數中的每一個屬性后,然後和發送過來的數據做對比。當匹配成功后,依照發送過來的數據分配給參數。
當兩個參數是特指的,將會發生什麼?例如第一個是「Employee」,第二個是「FirstName」?
FirstName 將會在初始的變數 FirstName 中更新,也會在 e.FirstName 屬性中更新。
ModelBinder 可以和組合關係一起運用嗎?
答案是肯定的。但是在這個情形下控制項的名稱應該被給出。例如:
Customer 和 Address 類的代碼如下:
public class Customer
{
public string FName{get;set;}
public Address address{get;set;}
}
public class Address
{
public string CityName{get;set;}
public string StateName{get;set;}
}
在這種情形下,HTML 如下:
...
...
...
<input type="text" name="FName">
<input type="text" name="address.CityName">
<input type="text" name="address.StateName">
...
...
...
6. Lab 11 — 重置和取消按鈕
第一步:開始重置和取消按鈕
增加一個重置和取消按鈕,代碼如下:
...
...
...
<input type="submit" name="BtnSubmit” value="Save Employee" />
<input type="button" name="BtnReset" value="Reset" onclick="ResetForm();" />
<input type="submit" name="BtnSubmit" value="Cancel" />
注意:保存和取消按鈕都有相同的「Name」屬性,即「BtnSubmit」。
第二步:定義 ResetForm 方法
在 HTML 頂部區域增加一個 Script 標籤,用於創建一個 JavaScript 方法,命名為 ResetForm。代碼如下:
<script>
function ResetForm() {
document.getElementById('TxtFName').value = "";
document.getElementById('TxtLName').value = "";
document.getElementById('TxtSalary').value = "";
}
</script>
第三步:在 EmployeeController 的 SaveEmployee 行為方法中實現取消點擊事件。
改變 SaveEmployee 行為方法如下:
public ActionResult SaveEmployee(Employee e, string BtnSubmit)
{
switch (BtnSubmit)
{
case "Save Employee":
return Content(e.FirstName + "|" + e.LastName + "|" + e.Salary);
case "Cancel":
return RedirectToAction("Index");
}
return new EmptyResult();
}
第四步:執行應用
按下 F5 並執行應用。通過點擊「Add New」鏈接導航到 AddNew 屏幕。
第五步:測試重置功能
第六步:測試 Save 和 Cancel 功能
Lab 11 的 Q&A
為什麼保存和取消按鈕的名稱是一樣的?
我們知道,一旦提交按鈕被點擊,一個請求就會被發送到伺服器端。並且伴隨著請求,所有輸入控制項的值也一起被發送。
Submit 按鈕也是一個輸入按鈕。因為按鈕的值也會被發送。
當保存按鈕被點擊時,保存按鈕的值,即「Save Employee」將會隨著請求一起被發送。當取消按鈕被點擊時,取消按鈕的值,即「Cancel」將會隨著請求一起被發送。
在 Action 方法中。Model Binder 將會處理這些工作。它將會依照輸入的值(伴隨著請求)更新參數的值。
實現多個提交按鈕的方式是什麼?
這裡有多個方式。我介紹其中三種。
隱藏 Form 元素
第一步:在視圖中創建一個隱藏 Form 元素
<form action="/Employee/CancelSave" id="CancelForm" method="get" style="display:none">
</form>
第二步:改變 Submit 按鈕為一個常規按鈕,並且通過 JavaScript 將上面的 Form 發送
<input type="button" name="BtnSubmit" value="Cancel" onclick="document.getElementById('CancelForm').submit()" />
運用 JavaScript 動態地改變動作 URL
<form action="" method="post" id="EmployeeForm" >
...
...
<input type="submit" name="BtnSubmit" value="Save Employee" onclick="document.getElementById('EmployeeForm').action = '/Employee/SaveEmployee'" />
...
<input type="submit" name="BtnSubmit" value="Cancel" onclick="document.getElementById('EmployeeForm').action = '/Employee/CancelSave'" />
</form>
Ajax
不再運用 Submit 按鈕,取而代之的是簡單的輸入按鈕,然後運用 JQuery 或者其它庫來實現純 Ajxa 請求。
為什麼我們在實現重置功能時,沒使用「input type = reset」?
「input type = reset」控制項不會清除值,它只是將控制項的值改為默認的值。例如:
<input type="text" name="FName" value="Sukesh">
在這個例子中,控制項的默認值是「Sukesh」。
如果我們運用「input type = reset」來實現重置功能,那麼每一次點擊重置按鈕,默認的值「Sukesh」將會被設置到 Textbox 中。
當名稱沒有與類中的屬性名稱匹配時,會怎樣?
這是一個在面試中經常被問到的常規問題。
例如我們有一段 HTML 代碼如下:
First Name: <input type="text" id="TxtFName" name="FName" value="" /><br />
Last Name: <input type="text" id="TxtLName" name="LName" value="" /><br />
Salary: <input type="text" id="TxtSalary" name="Salary" value="" /><br />
現在我們的 Model 類包含的屬性名稱有 FirstName,LastName 和 Salary。因此默認的 Model Binder 將不會在這裡處理。
在這種情形下,我們有三種解決方案:
在 Action 方法內部,運用 Request.Form 語法來檢索發送過來的值,然後手動構造 Model 對象如下:
public ActionResult SaveEmployee()
{
Employee e = new Employee();
e.FirstName = Request.Form["FName"];
e.LastName = Request.Form["LName"];
e.Salary = int.Parse(Request.Form["Salary"])
...
...
}
運用參數名稱,然後手動創建 Model 對象如下:
public ActionResult SaveEmployee(string FName, string LName, int Salary)
{
Employee e = new Employee();
e.FirstName = FName;
e.LastName = LName;
e.Salary = Salary;
...
...
}
創建自定義的 Model Binder 來替換默認的 Model Binder。
第一步:創建自定義的 Model Binder
public class MyEmployeeModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
Employee e = new Employee();
e.FirstName = controllerContext.RequestContext.HttpContext.Request.Form["FName"];
e.LastName = controllerContext.RequestContext.HttpContext.Request.Form["LName"];
e.Salary = int.Parse(controllerContext.RequestContext.HttpContext.Request.Form["Salary"]);
return e;
}
}
第二步:用這個新的 Model Binder 來替換默認的 Model Binder
public ActionResult SaveEmployee([ModelBinder(typeof(MyEmployeeModelBinder))]Employee e, string BtnSubmit)
{
......
}
RedirectToFunction 函數是做什麼的?
它用來產生 RedirectToRouteResult,就像 ViewResult 和 ContentResult一樣(在第一天學習中探討)。RedirectToRouteResult 是 ActionResult 的子類,它代表的是間接的響應。當瀏覽器接到 RedirectToRouteResult,它就會產生新的請求到一個新的行為方法。
註:這裡瀏覽器對新的請求是有責任的,因此 URL 將會更新到一個新的 URL。
什麼是 EmptyResult?
它是 ActionResult 的其中一個子類。當瀏覽器接到的響應是 EmptyResult 時,它將會簡單地呈現一個空白屏幕。它簡單地代表「No Result」。
在我們的例子中,這種情形不會發生。只要確保所有的代碼路徑返回的值。
註:當 Action 方法返回的值是空的,結果等同於 EmptyResult。
7. Lab 12 — 在資料庫中保存記錄並更新 Grid
第一步:在 EmployeeBusinessLayer 中創建 SaveEmployee
public Employee SaveEmployee(Employee e)
{
SalesERPDAL salesDal = new SalesERPDAL();
salesDal.Employees.Add(e);
salesDal.SaveChanges();
return e;
}
第二步:改變 SaveEmployee 行為方法
在 EmployeeController 中,改變 SaveEmployee 行為方法,代碼如下:
public ActionResult SaveEmployee(Employee e, string BtnSubmit)
{
switch (BtnSubmit)
{
case "Save Employee":
EmployeeBusinessLayer empBal = new EmployeeBusinessLayer();
empBal.SaveEmployee(e);
return RedirectToAction("Index");
case "Cancel":
return RedirectToAction("Index");
}
return new EmptyResult();
}
第三步:執行並測試
按下 F5 並執行應用。導航到 Data 入口屏幕並輸入一些合法的值。
8. Lab 13 — 增加伺服器端認證
在 Lab 10 中,我們已經了解了 Model Binder 的基本功能。現在我們來更多地了解下。
Model Binder 通過發送過來的數據來更新 Employee 對象。
但是這個不是 Model Binder 的唯一功能。Model Binder 還更新 ModelState。
它有一個屬性,稱為 IsValid,這個決定了 Model(即 Employee 對象)是否更新成功了。如果伺服器端的認證失敗了,Model 將不會更新。
它承載了認證錯誤。例如:ModelState["FirstName"].Errors 包含了與 First Name 相關的錯誤。
它承載了發送過來的數據值。(Posted 數據或者是查詢字元串數據)
在 ASP.NET MVC 中,我們運用 DataAnnotations 來實現伺服器端的認證。
在了解 Data Annotation 之前,我們先了解一些 Model Binder。
ModelBinder 如何處理初始數據類型?
當 Action 方法包含初始類型參數時,Model Binder 將會把參數的名稱與發送過來的數據進行對比。(發送過來的數據是 Posted 數據或者是查詢字元串)
當匹配成功時,將會依照發送過來的數據分配給參數。
當匹配失敗時,參數將會被分配給默認值。(對於整型的默認值是0,對於字元串的默認值是 null)
當數據類型不匹配的異常被拋出時,這種情況下不會進行分配操作。
Model Binder 如何處理類?
當參數是一個類參數,Model Binder 將會遍歷所有類的所有屬性,並且將每一個屬性名稱與發送過來的數據做對比。
當匹配成功時,如果發送過來的數據是空的。
Null 值將會被分配給屬性。如果不能分配,默認的值將會被設置,並且 ModelState.IsValid 將會被設置為 false。
當 Null 值可以分配給屬性時,這將會被視作不合法的值,因為屬性附上了認證,因此 ModelState.IsValid 將會被設置為 false。
當匹配成功時,發送過來的數據不為空。
當數據類型不匹配時,將不會分配值。或者伺服器端的認證失敗,將分配 Null 值。此時 ModelState.IsValid 將會被設置為 false。如果不能分配 Null 值,默認的值將會被設置。
當匹配不成功時,參數將會被分配為默認值。(對於整型的默認值是0,對於字元串的默認值是 null)。在這種情形下,ModelState.IsValid 將不會受到影響。
現在讓我們了解一下如何向項目中增加認證功能。
第一步:運用 DataAnnotations 裝飾屬性。
在 Model 文件夾下打開 Employee 類,運用 DataAnnotation 來裝飾 FirstName 和 LastName,代碼如下:
public class Employee
{
...
...
[Required(ErrorMessage="Enter First Name")]
public string FirstName { get; set; }
[StringLength(5,ErrorMessage="Last Name length should not be greater than 5")]
public string LastName { get; set; }
...
...
}
第二步:改變 SaveEmployee 行為方法。
打開 EmployeeController,改變 SaveEmloyee 行為方法如下:
public ActionResult SaveEmployee(Employee e, string BtnSubmit)
{
switch (BtnSubmit)
{
case "Save Employee":
if (ModelState.IsValid)
{
EmployeeBusinessLayer empBal = new EmployeeBusinessLayer();
empBal.SaveEmployee(e);
return RedirectToAction("Index");
}
else
{
return View("CreateEmployee ");
}
case "Cancel":
return RedirectToAction("Index");
}
return new EmptyResult();
}
註:正如你所看見的,當 SaveEmployee 按鈕點擊后, ModelState.IsValid 失敗,ViewResult 指向「CreateEmloyee」視圖。