譯文,個人原創,轉載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(中)),不對的地方歡迎指出與交流。
章節出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時仔細分辨,唯望莫誤人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 41 ASP.NET MVC
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(上)
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(下)
------------------------------------
從客戶端提交數據
直到現在,只使用來自客戶端的 HTTP GET 請求從服務器檢索HTML代碼。從客戶端發送表單數據怎么辦?
為了提交表單數據,為控制器 SubmitData 創建視圖 CreateMenu。該視圖包含一個HTML表單元素,用於定義應將哪些數據發送到服務器。 form方法被聲明為 HTTP POST 請求。定義輸入字段的 input 元素都具有與Menu類型的屬性對應的名稱(代碼文件 MVCSampleApp/Views/SubmitData/CreateMenu.cshtml):
@{
ViewBag.Title ="Create Menu";
}
<h2>Create Menu</h2>
<form action="/SubmitData/CreateMenu" method="post">
<fieldset>
<legend>Menu</legend>
<div>Id:</div>
<input name="id" />
<div>Text:</div>
<input name="text" />
<div>Price:</div>
<input name="price" />
<div>Category:</div>
<input name="category" />
<div></div>
<button type="submit">Submit</button>
</fieldset>
</form>
圖41.7顯示了瀏覽器中打開的頁面。

圖41.7
在 SubmitData 控制器中,創建兩個 CreateMenu 操作方法:一個用於 HTTP GET 請求,另一個用於 HTTP POST 請求。因為C#允許不同的方法有相同的名稱,只需要參數號或類型不同。當然,這個要求與操作方法是一致。 Action方法也需要與HTTP請求方法不同。默認請求方法是GET,當你應用屬性 HttpPost 時,請求方法是POST。要讀取HTTP POST數據,可以從 Request 對象使用信息。但是,定義有參數的 CreateMenu 方法要簡單得多。參數與表單字段的名稱相匹配(代碼文件MVCSampleApp/Controllers/SubmitDataController.cs):
public IActionResult Index() => View();
public IActionResult CreateMenu() => View(); [HttpPost] public IActionResult CreateMenu(int id, string text, double price, string category) { var m = new Menu { Id = id, Text = text, Price = price }; ViewBag.Info = $"menu created: {m.Text}, Price: {m.Price}, category: {m.Category}"; return View("Index"); }
要顯示結果,可以顯示ViewBag.Info的值(代碼文件MVCSampleApp/Views/SubmitData/Index.cshtml):
@ViewBag.Info
模型綁定器
不僅可以在action方法使用多個參數,還還可以使用包含與傳入字段名稱匹配的屬性的類型(代碼文件 MVCSampleApp/Controllers/SubmitDataController.cs):
[HttpPost]
public IActionResult CreateMenu2(Menu m) { ViewBag.Info = $"menu created: {m.Text}, Price: {m.Price}, category: {m.Category}"; return View("Index"); }
用戶使用表單提交數據時,將調用一個 CreateMenu 方法,顯示帶有提交的菜單數據的Index視圖,如圖41.8所示。

圖41.8
模型綁定器負責從 HTTP POST 請求傳輸數據。模型綁定器實現接口 IModelBinder。默認情況下 FormCollectionModelBinder 類用於將輸入字段綁定到模型。這個binder支持基本類型,模型類(例如 Menu 類型)和實現 ICollection<T> , IList<T> 和 IDictionary<TKey, TValue> 的集合。
如果不是所有參數類型的屬性都要填充模型綁定器,則可以使用 Bind 屬性。使用此屬性,可以指定應包含在綁定中的屬性名稱列表。
還可以使用沒有參數的操作方法將輸入數據傳遞到模型,如下一個代碼段所示。創建一個新的Menu類實例,並將此實例傳遞給 Controller 基類的 TryUpdateModelAsync 方法。如果更新后的模型在更新后不處於有效狀態,TryUpdateModelAsync 將返回false:
[HttpPost]
public async Task<IActionResult> CreateMenu3Result() { var m = new Menu(); bool updated = await TryUpdateModelAsync<Menu>(m); if (updated) { ViewBag.Info = $"menu created: {m.Text}, Price: {m.Price}, category: {m.Category}"; return View("Index"); } else { return View("Error"); } }
注釋和驗證
可以向模型類型添加一些注釋,這些注釋用於更新數據的驗證。命名空間 System.ComponentModel.DataAnnotations 包含可用於客戶端上指定的信息數據並可用於驗證的特性類型。(譯者注:為了區分 Attribute 和 property,將 Attribute 譯為“特性”,property 譯為“屬性”,二者雖然含有屬性的意思,其實還是有些區別的,感興趣的讀者可以查閱相關資料,此處不作詳細介紹。)
這些添加的特性可以在Menu類作更改(代碼文件MVCSampleApp/Models/Menu.cs):
public class Menu
{
public int Id { get; set; } [Required, StringLength(50)] public string Text { get; set; } [Display(Name="Price"), DisplayFormat(DataFormatString="{0:C}")] public double Price { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } [StringLength(10)] public string Category { get; set; } }
用於驗證的常用的特性類型有,CompareAttribute 用於比較不同的屬性,CreditCardAttribute用於驗證有效的信用卡號,EmailAddressAttribute 用於驗證電子郵件地址,EnumDataTypeAttribute用於將輸入與枚舉值進行比較,PhoneAttribute用於驗證電話號碼。
還可以使用其他特性來獲取顯示和錯誤消息的值,例如 DataTypeAttribute 和 DisplayFormatAttribute。
要使用驗證屬性,以下所示的操作方法中使用 ModelState.IsValid 可用於驗證模型的狀態(代碼文件MVCSampleApp/Controllers/SubmitDataController.cs):
[HttpPost]
public IActionResult CreateMenu4(Menu m) { if (ModelState.IsValid) { ViewBag.Info = $"menu created: {m.Text}, Price: {m.Price}, category: {m.Category}"; } else { ViewBag.Info ="not valid"; } return View("Index"); }
如果使用工具生成的模型類,您可能認為很難向屬性添加特性。由於工具生成的類被定義為部分類,可以通過添加屬性和方法,實現額外的接口,以及實現由工具生成的類使用的部分方法來擴展類。如果無法更改類型的源代碼,則無法向現有屬性和方法添加特性,但對於這種情況有幫助!假設Menu類是由工具生成的部分類。然后,不同名稱的新類(例如,MenuMetadata)可以定義與實體類相同的屬性,並添加注釋,如下所示:
public class MenuMetadata
{
public int Id { get; set; } [Required, StringLength(25)] public string Text { get; set; } [Display(Name="Price"), DisplayFormat(DataFormatString="{0:C}")] public double Price { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } [StringLength(10)] public string Category { get; set; } }
MenuMetadata類必須鏈接到Menu類。使用工具生成的部分類,可以在同一命名空間中創建另一個部分類型,以將MetadataType特性添加到創建連接的類型定義中:
[MetadataType(typeof(MenuMetadata))]
public partial class Menu { }
HTML Helper方法還可以利用注釋向客戶端添加信息。
使用HTML Helpers
HTML Helpers 是創建HTML代碼的助手。在視圖中Razor語法可以直接使用它們。
Html 是視圖基類 RazorPage 的屬性,屬於 IHtmlHelper 類型。 HTML Helper 方法被實現為擴展方法來擴展 IHtmlHelper 接口。
類 InputExtensions 定義了 HTML 輔助方法來創建復選框、密碼控件、單選按鈕和文本框控件。 Action 和 RenderAction 輔助方法由 ChildActionExtensions 類定義。顯示的輔助方法由 DisplayExtensions 類定義。 HTML表單的輔助方法由類FormExtensions定義。
以下部分介紹使用HTML Helpers的一些示例。
使用簡單助手
以下代碼段使用HTML助手方法 BeginForm,Label和CheckBox。 BeginForm啟動一個表單元素。還有一個EndForm用於結束表單元素。該示例使用從BeginForm方法返回的MvcForm實現的IDisposable接口(來釋放資源)。釋放 MvcForm 時,會調用EndForm。這樣,BeginForm方法可以被一個using語句包圍,以便在關閉的大括號中結束表單。方法 DisplayName 直接返回參數中的內容,方法CheckBox是一個type 特性設置為checkbox的 input 元素(代碼文件MVCSampleApp/Views/HelperMethods/SimpleHelper.cshtml):
@using (Html.BeginForm()) {
@Html.DisplayName("Check this (or not)")
@Html.CheckBox("check1")
}
下一個代碼段顯示生成的HTML代碼。 CheckBox方法創建兩個具有相同名稱的輸入元素,一個設置為 hidden 。這種行為有一個重要的原因:如果復選框的值為false,瀏覽器不會將表單內容的此信息傳遞給服務器。只將所選的復選框的值傳遞到服務器。此HTML特性會自動綁定動作方法的參數,從而產生問題。一個簡單的解決方案是通過CheckBox助手方法執行的。該方法創建相同名稱並設置為false的隱藏輸入元素。如果未選中該復選框,則隱藏的輸入元素將傳遞到服務器,並且可以綁定false值。如果選中此復選框,則會向服務器發送兩個具有相同名稱的輸入元素。第一個輸入元素設置為true;第二個設置為false。使用自動綁定,僅選擇第一個輸入元素進行綁定:
<form action="/HelperMethods/SimpleHelper" method="post">
Check this (or not)
<input id="FileName_check1" name="check1" type="checkbox" value="true" />
<input name="check1" type="hidden" value="false" />
</form>
使用模型數據
可以對模型數據使用輔助方法。示例創建一個 Menu 對象。這個類型在本章前面在 Models 目錄中已聲明,並將一個樣例菜單作為模型傳遞給視圖(代碼文件MVCSampleApp/Controllers/HTMLHelpersController.cs):
public IActionResult HelperWithMenu() => View(GetSampleMenu());
private Menu GetSampleMenu() =>
new Menu { Id = 1, Text ="Schweinsbraten mit Knödel und Sauerkraut", Price = 6.9, Date = new DateTime(2016, 10, 5), Category ="Main" };
視圖的模型定義為 Menu 類型。 HTML Helper 的DisplayName 從參數返回文本,如上一個示例所示。 Display方法使用一個表達式作為參數,其中屬性名稱可以以字符串格式傳遞。這種方式下,屬性嘗試查找具有此名稱的屬性,並訪問屬性訪問器以返回屬性的值(代碼文件MVCSampleApp/Views/HTMLHelpers/HelperWithMenu.cshtml):
@model MVCSampleApp.Models.Menu
@{
ViewBag.Title ="HelperWithMenu";
}
<h2>Helper with Menu</h2> @Html.DisplayName("Text:") @Html.Display("Text") <br /> @Html.DisplayName("Category:") @Html.Display("Category")
生成的HTML代碼,可以視為調用DisplayName和Display方法的輸出:
Text:
Schweinsbraten mit Knödel und Sauerkraut
<br /> Category: Main
注意 助手方法還提供強類型變量來訪問模型的成員。有關詳細信息,請參閱“使用強類型助手”部分。
定義HTML屬性
大多數HTML Helper方法都有重載,可以傳遞任何HTML屬性。例如,以下TextBox方法創建一個類型為 text 的輸入元素。第一個參數定義名稱,第二個參數定義使用文本框設置的值。 TextBox方法的第三個參數是對象類型,它允許傳遞一個匿名類型,其中每個屬性都被更改為HTML元素的特性。這里輸入元素的結果是required 特性設置為required,maxlength特性設置為15,class特性設置為CSSDemo。因為類是一個C#關鍵字,它不能直接設置為屬性。但是它以@為前綴以生成CSS樣式的類屬性:
@Html.TextBox("text1","input text here",
new { required="required", maxlength=15, @class="CSSDemo" });
生成的HTML輸出如下所示:
<input class="Test" id="FileName_text1" maxlength="15" name="text1" required="required" type="text" value="input text here" />
創建列表
為了顯示列表,存在類似 DropDownList和ListBox的輔助方法。這些方法創建HTML選擇元素。
在控制器中,首先創建一個包含鍵和值的字典。然后,字典將使用自定義擴展方法 ToSelectListItems 轉換為 SelectListItem 的列表。 DropDownList 和 ListBox 方法使用 SelectListItem 集合(代碼文件MVCSampleApp/Controllers/HTMLHelpersController.cs):
public IActionResult HelperList()
{
var cars = new Dictionary<int, string>(); cars.Add(1,"Red Bull Racing"); cars.Add(2,"McLaren"); cars.Add(3,"Mercedes"); cars.Add(4,"Ferrari"); return View(cars.ToSelectListItems(4)); }
自定義擴展方法 ToSelectListItems 在類SelectListItemsExtensions中定義,它擴展了來自cars集合的 IDictionary<int, string> 類型。在實現中,字典中的每個項目返回一個新的 SelectListItem 對象(代碼文件MVCSampleApp/Extensions/SelectListItemsExtensions.cs):
public static class SelectListItemsExtensions
{
public static IEnumerable<SelectListItem> ToSelectListItems( this IDictionary<int, string> dict, int selectedId) { return dict.Select(item => new SelectListItem { Selected = item.Key == selectedId, Text = item.Value, Value = item.Key.ToString() }); } }
在視圖中幫助方法 DropDownList 直接訪問從控制器返回的模型(代碼文件MVCSampleApp/Views/HTMLHelpers/HelperList.cshtml):
@{
ViewBag.Title ="Helper List"; } @model IEnumerable<SelectListItem> <h2>Helper2</h2> @Html.DropDownList("carslist", Model)
生成的HTML創建一個 select 元素,其中包含從 SelectListItem 創建的選項子元素,並定義從控制器返回的所選項:
<select id="FileName_carslist" name="carslist"> <option value="1">Red Bull Racing</option> <option value="2">McLaren</option> <option value="3">Mercedes</option> <option selected="selected" value="4">Ferrari</option> </select>
使用強類型助手
HTML Helper方法提供強類型方法來訪問從控制器傳遞的模型。這些方法都以名稱For后綴。例如,可以使用TextBoxFor方法代替TextBox方法。
以下示例再次使用控制器返回單個實體(代碼文件MVCSampleApp/Controllers/HTMLHelpersController.cs):
public IActionResult StronglyTypedMenu() => View(GetSampleMenu());
視圖使用 Menu 類型作為模型,因此方法 DisplayNameFor 和 DisplayFor 可以直接訪問 Menu 屬性。默認情況下,DisplayNameFor返回屬性的名稱(在這個例子中,它是Text屬性),DisplayFor 返回屬性的值(代碼文件 MVCSampleApp/Views/HTMLHelpers/StronglyTypedMenu.cshtml):
@model MVCSampleApp.Models.Menu
@Html.DisplayNameFor(m => m.Text)
<br /> @Html.DisplayFor(m => m.Text)
同樣,可以使用返回一個輸入元素的 Html.TextBoxFor(m => m.Text) 可以設置模型的Text屬性。此方法還使用添加到 Menu 類型的Text屬性的注釋。 Text屬性添加了 Required 和 MaxStringLength 特性,這就是從TextBoxFor方法返回 data-val-length、data-val-length-max和data-val-required屬性的原因:
<input data-val="true" data-val-length="The field Text must be a string with a maximum length of 50." data-val-length-max="50" data-val-required="The Text field is required." id="FileName_Text" name="Text" type="text" value="Schweinsbraten mit Knödel und Sauerkraut" />
使用編輯器擴展
EditorExtensions 類中的輔助方法為類型的所有屬性提供了一個編輯器,而不是為每個屬性使用至少一個幫助方法。
使用與之前相同的 Menu 模型,方法 Html.EditorFor(m => m) 用於構建編輯菜單的完整用戶界面(UI)。該方法調用的結果如圖41.9所示。

圖41.9
可以使用Html.EditorForModel 而不是使用 Html.EditorFor(m => m) 。方法 EditorForModel 使用視圖的模型,不需要明確指定它。 EditorFor在使用其他數據源(例如,模型提供的屬性)方面具有更大的靈活性,EditorForModel 需要添加的參數更少。
實現模板
從HTML Helpers 擴展結果的一個好方法是使用模板。模板是通過HTML Helper方法隱式或顯式使用的簡單視圖。模板存儲在特殊文件夾中。顯示模板存儲在視圖文件夾(例如Views/HelperMethods)或共享文件夾(Shared/DisplayTemplates)中的DisplayTemplates文件夾中。所有視圖都可使用共享文件夾,特定視圖文件夾僅由此文件夾中的視圖使用。編輯器模板存儲在文件夾 EditorTemplates 中。
現在看看一個例子。Menu類型的 Date屬性的注釋DataType的值為 DataType.Date。默認情況下,指定該特性時 DateTime類型不顯示為日期和時間,而是只顯示短日期格式(代碼文件MVCSampleApp/Models/Menu.cs):
public class Menu
{
public int Id { get; set; } [Required, StringLength(50)] public string Text { get; set; } [Display(Name="Price"), DisplayFormat(DataFormatString="{0:c}")] public double Price { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } [StringLength(10)] public string Category { get; set; } }
現在創建日期的模板。使用該模板,將使用長日期字符串格式D返回模型,該日期字符串格式D嵌入在CSS類 markRed 的 div 標記中(代碼文件MVCSampleApp/Views/HTMLHelpers/DisplayTemplates/Date.cshtml):
<div class="markRed">
@string.Format("{0:D}", Model)
</div> The markRed CSS class is defined within the style sheet to set the color red (code file MVCSampleApp/wwwroot/styles/Site.css): .markRed { color: #f00; }
現在,可以使用 DisplayForModel 等 HTML 輔助程序的顯示來使用定義的模板。該模型是Menu類型,因此 DisplayForModel 方法顯示 Menu 類型的所有屬性。對於日期,它找到了模板Date.cshtml,因此該模板用於以CSS樣式顯示長日期格式的日期(代碼文件 MVCSampleApp/Views/HTMLHelpers/Display.cshtml):
@model MVCSampleApp.Models.Menu
@{
ViewBag.Title ="Display";
}
<h2>@ViewBag.Title</h2>
如果單個類型在同一視圖中具有不同的表示,則可以為模板文件使用其他名稱。然后可以使用 UIHint 特性指定模板名稱,或者可以使用輔助方法的模板參數指定模板。
了解標簽助手
ASP.NET MVC 6提供了一種新的技術,可以用來代替HTML Helpers:Tag Helpers(標簽助手)。有了標簽助手,不用編寫混合HTML的C#代碼,而是使用在服務器上解析的HTML特性和元素。現在許多JavaScript庫都使用自己的屬性(例如Angular)擴展HTML,因此使用服務器端技術來自定義HTML特性非常方便。許多ASP.NET MVC標簽助手有前綴asp-,所以可以很容易地看到在服務器上解決的特性。這些特性不會發送到客戶端,而是在服務器上解析生成HTML代碼。
激活標簽助手
為了使用ASP.NET MVC標簽助手,需要通過調用addTagHelper激活標簽。第一個參數定義要使用的類型(a *打開裝配體的所有標簽助手),第二個參數定義了標簽助手的程序集。使用removeTagHelper,標簽助手將會再次被停用。停用標記助手可能很重要,例如,不會與腳本庫發生命名沖突。使用內置的標簽助手與asp-前綴不太可能造成沖突,但與其他標簽助手可以具有相同的名稱作為其他標簽助手或用於腳本庫的HTML屬性,卻很可能發生沖突。
要使標簽助手可用於所有視圖,將addTagHelper語句添加到共享文件_ViewImports.cshtml(代碼文件 MVCSampleApp/Views/_ViewImports.cshtml)中:
@addTagHelper *, Microsoft.AspNet.Mvc.TagHelpers
使用Anchor Tag Helpers
讓我們從擴展錨點元素的標簽助手開始。標簽助手的示例控制器是TagHelpersController。 Index動作方法返回一個視圖,用於顯示錨標簽助手(代碼文件MVCSampleApp/Controllers/TagHelpersController.cs):
public class TagHelpersController : Controller
{
public IActionResult Index() => View(); // etc. }
錨標記助手定義了asp-controller和asp-action特性。使用這些特性,控制器和動作方法可用於構建錨元素的URL。第二個和第三個例子不需要控制器,因為視圖來自相同的控制器(代碼文件MVCSampleApp/Views/TagHelpers/Index.cshtml):
<a asp-controller="Home" asp-action="Index">Home</a> <br /> <a asp-action="LabelHelper">Label Tag Helper</a> <br /> <a asp-action="InputTypeHelper">Input Type Tag Helper</a>
以下代碼段顯示生成的HTML代碼。 asp-controller和asp-action特性為 a 元素生成 href 特性。第一個示例訪問Home控制器中的Index操作方法,因為它們都是路由定義的默認值,所以在結果中需要一個 "/" 的 href。指定 asp-Action LabelHelper時,href指向 /TagHelpers/LabelHelper時,因為操作方法LabelHelper在當前控制器中:
<a href="/">Home</a> <br /> <a href="/TagHelpers/LabelHelper">Label Tag Helper</a> <br /> <a href="/TagHelpers/InputTypeHelper">Input Type Tag Helper</a>
使用標簽助手
在以下代碼片段中,演示了標簽助手的功能,操作方法LabelHelper將一個Menu對象傳遞給視圖(代碼文件MVCSampleApp/Controllers/TagHelpersController.cs):
private Menu GetSampleMenu() =>
new Menu
{
Id = 1, Text ="Schweinsbraten mit Knödel und Sauerkraut", Price = 6.9, Date = new DateTime(2016, 10, 5), Category ="Main" }; }
Menu類有一些應用於影響標簽助手的結果的數據注釋。看看Text屬性的 Display 特性。它將Display特性的Name屬性設置為“Menu”(代碼文件MVCSampleApp/Models/Menu.cs):
public class Menu
{
public int Id { get; set; } [Required, StringLength(50)] [Display(Name ="Menu")] public string Text { get; set; } [Display(Name ="Price"), DisplayFormat(DataFormatString ="{0:C}")] public double Price { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } [StringLength(10)] public string Category { get; set; } }
視圖使用應用於 label 控件的 asp-for 特性。用於該特性的值是視圖模型的屬性。在Visual Studio 2015中可以使用IntelliSense 訪問Text,Price 和 Date 屬性(代碼文件MVCSampleApp/Views/TagHelpers/LabelHelper.cshtml):
@model MVCSampleApp.Models.Menu
@{
ViewBag.Title ="Label Tag Helper";
}
<h2>@ViewBag.Title</h2>
<label asp-for="Text"></label>
<br/>
<label asp-for="Price"></label>
<br />
<label asp-for="Date"></label>
生成的HTML代碼里可以看到 for 特性,該特性引用與屬性名稱相同的元素,內容作為屬性名稱或Display特性的值。還可以使用該特性來本地化值:
<label for="Text">Menu</label> <br/> <label for="Price">Price</label> <br /> <label for="Date">Date</label>
使用輸入標簽輔助程序
HTML 標簽通常與 input 元素相關聯。以下代碼片段可以了解使用標簽助手的輸入元素生成的內容:
<label asp-for="Text"></label> <input asp-for="Text"/> <br/> <label asp-for="Price"></label> <input asp-for="Price" /> <br /> <label asp-for="Date"></label> <input asp-for="Date" />
檢查生成的HTML代碼的結果顯示,輸入類型標簽輔助程序根據屬性的類型創建type特性,並且它們還應用DateType特性。屬性Price的類型為double,即結果是數字輸入類型。因為Date屬性在DataType.Date值應用了DataType,所以輸入類型是日期。除此之外,還可以看到由於注釋而創建的data-val-length,data-val-length-max和data-val-required特性:
<label for="Text">Menu</label>
<input type="text" data-val="true"
data-val-length=
"The field Menu must be a string with a maximum length of 50."
data-val-length-max="50"
data-val-required="The Menu field is required."
id="FileName_Text" name="Text"
value="Schweinsbraten mit Knödel und Sauerkraut" />
<br/>
<label for="Price">Price</label>
<input type="number" data-val="true"
data-val-required="The Price field is required."
id="FileName_Price" name="Price" value="6.9" />
<br />
<label for="Date">Date</label>
<input type="date" data-val="true"
data-val-required="The Date field is required."
id="FileName_Date" name="Date" value="10/5/2016" />
現代瀏覽器中HTML 5輸入控件還有些特殊的樣式,如日期控件。 Microsoft Edge的輸入日期控制如圖41.10所示。

圖41.10
使用帶驗證的表單
為了將數據發送到服務器,輸入字段需要由表單相聯系起來。表單的標記助手使用asp-method 和 asp-controller定義了action特性。對於輸入控件,已經了解過驗證信息由這些控件定義。但是驗證錯誤信息需要顯示,因此驗證消息標簽輔助程序使用 asp-validation-for 擴展了span元素(代碼文件MVCSampleApp/Views/TagHelpers/FormHelper.cs):
<form method="post" asp-method="FormHelper">
<input asp-for="Id" hidden="hidden" />
<hr />
<label asp-for="Text"></label>
<div>
<input asp-for="Text" />
<span asp-validation-for="Text"></span>
</div>
<br />
<label asp-for="Price"></label>
<div>
<input asp-for="Price" />
<span asp-validation-for="Price"></span>
</div>
<br />
<label asp-for="Date"></label>
<div>
<input asp-for="Date" />
<span asp-validation-for="Date"></span>
</div>
<label asp-for="Category"></label>
<div>
<input asp-for="Category" />
<span asp-validation-for="Category"></span>
</div>
<input type="submit" value="Submit" />
</form>
控制器通過檢查 ModelState 來驗證接收的數據是否正確。如果不正確,將再次顯示相同的視圖(代碼文件MVCSampleApp/Controllers/TagHelpersController.cs):
public IActionResult FormHelper() => View(GetSampleMenu());
[HttpPost]
public IActionResult FormHelper(Menu m) { if (!ModelState.IsValid) { return View(m); } return View("ValidationHelperResult", m); }
運行應用程序可以看到如圖41.11所示的錯誤信息。

圖41.11
創建自定義標簽助手
除了使用預定義的標簽助手外,還可以創建自定義標簽助手。在本節中構建的示例自定義標記助手擴展HTML表格元素,以便為列表中的每個項目顯示一行,每個屬性顯示一列。
控制器實現 CustomHelper 方法返回Menu對象列表(代碼文件MVCSampleApp/Controllers/TagHelpersController.cs):
public IActionResult CustomHelper() => View(GetSampleMenus());
private IList<Menu> GetSampleMenus() =>
new List<Menu>() { new Menu { Id = 1, Text ="Schweinsbraten mit Knödel und Sauerkraut", Price = 8.5, Date = new DateTime(2016, 10, 5), Category ="Main" }, new Menu { Id = 2, Text ="Erdäpfelgulasch mit Tofu und Gebäck", Price = 8.5, Date = new DateTime(2016, 10, 6), Category ="Vegetarian" }, new Menu { Id = 3, Text ="Tiroler Bauerngröst'l mit Spiegelei und Krautsalat", Price = 8.5, Date = new DateTime(2016, 10, 7), Category ="Vegetarian" } };
現在進入標記助手。自定義實現需要以下命名空間:
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using System.Collections.Generic; using System.Linq; using System.Reflection;
自定義標記助手派生自基類 TagHelper。TargetElement 特性定義標記助手所擴展的HTML元素。該標記助手擴展了 table 元素;因此字符串“table”被傳遞給元素的構造函數。使用 Attributes 屬性,可以定義分配給標記助手使用的HTML元素的特性列表。標簽助手使用 items 特性。標簽助手可以通過以下語法使用: <table items=“Model”></table> ,其中 Model 是可以迭代的列表。如果要創建一個被多個HTML元素一起使用的標簽助手,只需要應用特性 TargetElement 多次。要將 items 特性的值自動分配給 Items 屬性,將特性 HtmlAttributeName 分配給該屬性(代碼文件MVCSampleApp/Extensions/TableTagHelper.cs):
[TargetElement("table", Attributes = ItemsAttributeName)]
public class TableTagHelper : TagHelper { private const string ItemsAttributeName ="items"; [HtmlAttributeName(ItemsAttributeName)] public IEnumerable<object> Items { get; set; } // etc. }
標記助手的核心是在方法 Process 中。該方法需要創建從輔助程序返回的HTML代碼。使用 Process 方法的參數接收 TagHelperContext 。該上下文包含應用標記助手的HTML元素的屬性和所有子元素。表格元素里已經定義行和列,可以將結果與現有內容合並。但在示例中,這些被忽略了,只是特性被放到結果中。結果需要寫入第二個參數:TagHelperOutput 對象。為了創建HTML代碼,使用 TagBuilder 類型。 TagBuilder幫助創建有特性的HTML元素,並且處理元素的關閉。要向 TagBuilder 添加特性,請使用方法 MergeAttributes 。該方法需要所有特性名稱及其值的字典。這個字典是通過使用LINQ擴展方法 ToDictionary 創建的。Where方法獲取表元素的所有現有屬性(除了items屬性)。 items 特性用於使用標記助手定義項目,但稍后客戶端不需要它:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
TagBuilder table = new TagBuilder("table"); table.GenerateId(context.UniqueId,"id"); var attributes = context.AllAttributes .Where(a => a.Name != ItemsAttributeName).ToDictionary(a => a.Name); table.MergeAttributes(attributes); // etc. }
注意 如果需要在Tag Helper實現中調用異步方法,則可以覆蓋ProcessAsync方法,而不是Process方法。
注意 在第13章“語言集成查詢”中解釋了LINQ。
接下來創建表格的第一行。此行包含一個tr元素作為table元素的子元素,並且每個屬性包含td元素。要獲取所有屬性名稱,可以調用 First 方法來檢索集合的第一個對象。可以使用反射訪問此實例的屬性,調用Type對象上的GetProperties方法,並將屬性的名稱寫入到HTML th 元素的內部文本:
// etc.
var tr = new TagBuilder("tr");
var heading = Items.First(); PropertyInfo[] properties = heading.GetType().GetProperties(); foreach (var prop in properties) { var th = new TagBuilder("th"); th.InnerHtml.Append(prop.Name); th.InnerHtml.AppendHtml(th); } table.InnerHtml.AppendHtml(tr); // etc.
注意 反射在第16章中作了解釋。
Process 方法的最后一部分遍歷集合的所有項目,並為每個項目創建更多行(tr)。對於每個屬性,添加一個td元素,並將該屬性的值寫為內部文本。最后,將創建的 table 元素的內部HTML代碼寫入輸出:
foreach (var item in Items)
{
tr = new TagBuilder("tr"); foreach (var prop in properties) { var td = new TagBuilder("td"); td.InnerHtml.Append(prop.GetValue(item).ToString()); td.InnerHtml.AppendHtml(td); } table.InnerHtml.AppendHtml(tr); } output.Content.Append(table.InnerHtml);
創建標記助手后,創建視圖變得非常簡單。定義模型后,可以通過 addTagHelper 傳遞程序集名稱來引用標記助手。使用 item 特性定義HTML表時,標記助手本身將被實例化(代碼文件MVCSampleApp/Views/TagHelpers/CustomHelper.cshtml):
@model IEnumerable<Menu>
@addTagHelper"*, MVCSampleApp"
<table items="Model" class="sample"></table>
運行應用程序時,看到的表應該看起來像 圖41.12。創建標記助手后,非常易於使用。所有 使用CSS定義的格式化仍像HTML表定義的所有特性在生成的HTML輸出中那樣應用即可。
圖41.12
------未完待續
