C# 6 与 .NET Core 1.0 高级编程 - 41 ASP.NET MVC(中)
译文,个人原创,转载请注明出处(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
------未完待续