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

{

&hellip;&hellip;

&hellip;&hellip;

&hellip;&hellip;

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&rdquo; 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」視圖。

Advertisements

你可能會喜歡