ASP.NET Core Razor Pages in Action 6 使用表单:标记辅助函数
本章涵盖
• 使用标记帮助程序构建表单
• 使用数据注释控制输入类型
• 使用服务器端代码填充选择列表
• 使用表单中的复选框和单选按钮
• 将文件上传到服务器
上一章介绍了模型绑定如何采用输入并将其绑定到 Razor Pages 中的处理程序参数和公共属性。您了解了确保成功绑定的关键是确保绑定源的名称与绑定目标的名称匹配。到目前为止,您已经手动生成了表单控件的名称。这种可能容易出错的方法除了在 Request.Form 集合中寻找已发布的值之外,不会让您感到困惑。
在 Razor Pages 应用程序中,表单是标记帮助程序大放异彩的地方。您已经通过 validation 标签帮助程序看到了它们在客户端和服务器端验证中的角色。本章将探讨如何生成表单以从用户那里收集数据并使用模型绑定系统。您将了解如何使用它们来确保控件名称与绑定目标名称顺利工作。以下标记帮助程序可用于表单构建:
• Form
• Form action
• Label
• Input
• Select
• Option
• Textarea
每个标记帮助程序都以它命名的 HTML 元素为目标,但表单作标记帮助程序除外,该标记帮助程序以 type 属性设置为 submit 或 image 的按钮和 input 元素为目标。
本章首先介绍 form 标签帮助程序,以帮助您了解其角色并对其进行配置。我们将广泛研究 input tag helper 并学习如何根据绑定目标数据类型控制它呈现的输入类型,以及如何在需要时使用数据注释属性来微调内容。
在技术社区支持站点和论坛上,选择列表、复选框和单选按钮似乎会引发大多数与表单相关的问题,因此我们将详细介绍如何使用它们的标记帮助程序。最后,我们将研究使用表单上传文件。
6.1 表单和表单作标签辅助函数
表单标记帮助程序以 HTML 表单元素为目标,具有两个主要职责。它确保请求验证令牌包含在其方法设置为 post 的每个表单的隐藏字段中。它还会根据您提供给表单自定义属性的值生成表单的 action 属性的 URL。
表单作标记帮助程序以按钮和输入为目标,其 type 属性设置为 submit 或 image。它的作用是设置这些元素上的 formaction 属性的 URL,该 URL 指定表单应提交到的位置。因此,它会覆盖表单的 action 属性。
接下来,当我在文本中引用标记帮助程序的自定义属性时,我将省略 asp- 前缀。请记住,所有框架属性都以 asp- 为前缀。表单标签帮助程序和表单作标签帮助程序上可用的生成 URL 的自定义属性与我在第 4 章中详细介绍的锚点标签帮助程序上的自定义属性相同,并且它们以相同的方式根据应用程序的路由配置构造值:
• page
• page-handler
• route-*
• all-route-data
• host
• protocol
• area
如果在表单元素中包含 action 属性以及这些自定义路由属性中的任何一个,则框架将引发错误。表单标签帮助程序使用传递给这些自定义属性的值来生成 action 属性,类似于锚点标签帮助程序从相同的自定义路由属性生成 href 属性的方式。您可以自己通过 action 属性显式设置 URL,也可以使用标签帮助程序属性来配置 URL。但你不能两者兼得。如果您不包含 action 属性或任何自定义路由属性,则表单将提交到当前页面的 URL。对于绝大多数 CRUD 情况,这很可能是您要执行的作,您将在生成表单的同一页面的 OnPost 处理程序中处理提交。当处理页面是不同的页面或在命名处理程序中处理表单时,您通常会使用 tag helper 属性来设置表单的作。例如,当我们在第 3 章中介绍命名处理程序时,您已经看到了您可能希望设置页面处理程序名称的位置:
<form method="post" asp-page-handler="Search">
表单标记帮助程序支持一个额外的自定义属性:防伪。这需要一个 Boolean 值来控制是否呈现请求验证令牌,因此它为您提供了一种有选择地逐个表单禁用请求验证的方法。如果省略该属性,则该值默认为 true。回想一下我们在第 3 章中介绍的命名处理程序的例子,清单 3.32 中的搜索表单可以编写为使用表单作标签帮助程序,而不是通过表单标签帮助程序设置提交 URL。下面的清单演示了如何使用 page-handler 属性在渲染的按钮中生成 formaction 属性。
清单 6.1 表单动作标签助手
<form method="post">
<p>Search</p>
<input name="searchTerm" />
<button asp-page-handler="Search">Search</button> ❶
</form>
❶ page-handler 属性被应用于按钮,导致 formaction 属性被渲染到浏览器。
6.2 输入和标签标签帮助程序
在上一章中,您从将表单字段控件的名称映射到 Form 和 Query 集合中的键这一可能容易出错的做法,发展到绑定到由于应用了 BindProperty 属性而被显式标记为绑定目标的公共属性。但您仍然有出错的余地,因为控件的 name 属性的值必须与目标 PageModel 属性的 name 匹配。回想一下城市的 Create 页面中表单的 HTML,在该页面中,您将字符串 “cityName” 分配给 name 属性:
<input class="form-control" type="text" name="cityName" />
如前所述,您不能依赖字符串匹配。对其中一个或另一个的更改只会导致运行时出错。由于属性可在 Razor 页面中访问,因此可以直接在 Razor 代码中使用 nameof 表达式,以确保为表单控件的 name 属性生成的值与属性名称匹配:
<input name="@nameof(Model.CityName)" type="text" />
但是,使用 input tag helper 可以以更少的仪式实现类似的效果,并提供更多的收益。它有一个 for 属性,该属性采用 PageModel 属性的名称,并有效地将该属性绑定到标记帮助程序,从而在呈现的 HTML 中为表单控件生成 name 和 id 属性。input 标记帮助程序还将呈现 type 属性,根据 bound 属性的数据类型将其值设置为合适的值,因此您也不需要将其包含在标记中。标记帮助程序还呈现一个 value 属性,该属性被设置为分配给该属性的值。以下简单代码是生成完全连接 input 元素所需的全部代码:
<input asp-for="CityName" />
上面一行的渲染输出是
<input type="text" id="CityName" name="CityName" value="" />
通过这种方式,你可以在表单控件和公共 PageModel 属性之间,或者绑定源和它的目标之间实现一种双向绑定(图 6.1)。
图 6.1 使用标签帮助程序实现绑定源与其目标之间的双向绑定。
Create City 页面中的现有 label 元素可以替换为 tag 帮助程序。当前 HTML for 属性需要以 asp- 为前缀才能激活标记帮助程序。就像 input 标签帮助程序一样,传递给 for 属性的值应该是 model 属性的名称:
<label asp-for="CityName"></label>
在此示例中,标签标签帮助程序为空。它没有内容。当像这样使用时,属性名称将在标签中呈现:
<label for="CityName">CityName</label>
你可以通过将文本添加到标签的内容或使用 data annotation 属性来覆盖它,我们稍后将介绍该属性。
因此,总而言之,在表单中使用标记帮助程序的好处是,您可以在服务器代码中的属性和呈现给浏览器的表单控件之间建立双向绑定关系。如果在表单中更新了该值,则在提交时,该更新将自动应用于服务器。同样,如果更改服务器上的值,则呈现的表单控件将自动反映这些更改。
6.2.1 了解输入类型
现在,您已经了解了在表单中使用标记帮助程序的好处,是时候考虑确保呈现正确类型的输入的方法了。首先,我们将了解 .NET 和 HTML 类型之间的默认映射。HTML 5 在现有选项(包括 checkbox、radio、file 等)的基础上,添加了对不同类型数据(数字、日期时间-本地时间、周和范围)的新输入类型集合的支持。某些 HTML 5 输入类型在浏览器上仅享有有限的支持,但在可以的情况下,您应该在使用提供类似功能的第三方库(例如,日期选择器)之前使用它们。
通常,您可以依靠 Razor Pages 仅根据绑定到标记帮助程序的属性数据类型生成正确的类型。表 6.1 提供了有关输入标记帮助程序为指定 .NET 数据类型的 HTML type 属性生成哪个值的详细信息。
表 6.1 .NET 数据类型与输入标记帮助程序生成的 type 属性之间的映射
要对此进行测试,请将名为 PropertyManager 的新文件夹添加到 Pages 文件夹。这将包含用于管理网站上可用的出租物业详细信息的页面。然后添加名为 Create.cshtml 的新 Razor 页面。将以下公共属性添加到 PageModel 中,表示有关允许用户使用一系列 .NET 类型的租赁属性的信息。
清单 6.2 Property Manager 的 CreateModel
public class CreateModel : PageModel
{
[BindProperty]
public string Name { get; set; }
[BindProperty]
public int MaxNumberOfGuests { get; set; }
[BindProperty]
public decimal DayRate { get; set; }
[BindProperty]
public bool SmokingPermitted { get; set; }
[BindProperty]
public DateTime AvailableFrom { get; set; }
}
接下来,将 Create.cshtml 文件更改为包含表单,对每个 PageModel 属性使用标记帮助程序,使代码如下所示。
清单 6.3 Create Property 页面的表单,使用标签助手
@page
@model CityBreaks.Pages.PropertyManager.CreateModel
@{
ViewData["Title"] = "Create Property";
}
<form method="post">
<div class="mb-3">
<label class="form-label" asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="MaxNumberOfGuests"></label>
<input class="form-control" asp-for="MaxNumberOfGuests" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="DayRate"></label>
<input class="form-control" asp-for="DayRate" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="AvailableFrom"></label>
<input class="form-control" asp-for="AvailableFrom" />
</div>
<div class="mb-3">
<label asp-for="SmokingPermitted"></label>
<input asp-for="SmokingPermitted" />
</div>
<div class="mb-3">
<button class="btn btn-primary">Submit</button>
</div>
</form>
除了用于表示的 Bootstrap 类之外,该表单还由一系列标签和输入标记帮助程序组成,这些标记表示 PageModel 中的每个属性。当您导航到 /property-manager/create 时,您应该会看到类似于图 6.2 的内容。
图 6.2 “Create Property”(创建属性)页面的呈现表单
label 标记帮助程序已呈现属性的名称,并且此值也已分配给呈现的标签的 for 属性。其中一些标签值不是特别用户友好。我们稍后会看看你可以管理它的方法。
输入是根据关联属性的数据类型生成的,如表 6.1 中所述。请注意,即使您尚未设置值,也已为多个输入分配了值。关联的属性不可为空,因此它们被视为必需属性,并且已为其分配了默认值。现在让我们看看表单的渲染源代码。
清单 6.4 表单的渲染源
<form method="post">
<div class="mb-3">
<label class="form-label" for="Name">Name</label>
<input class="form-control" type="text" id="Name"
➥ name="Name" value="" />
</div>
<div class="mb-3">
<label class="form-label"
➥ for="MaxNumberOfGuests">MaxNumberOfGuests</label>
<input class="form-control" type="number" data-val="true"
data-val-required="The MaxNumberOfGuests field is required."
id="MaxNumberOfGuests" name="MaxNumberOfGuests" value="0" />
</div>
<div class="mb-3">
<label class="form-label" for="DayRate">DayRate</label>
<input class="form-control" type="text" data-val="true"
data-val-number="The field DayRate must be a number."
➥ data-val-required="The DayRate field is required."
➥ id="DayRate" name="DayRate" value="0.00" />
</div>
<div class="mb-3">
<label class="form-label" for="AvailableFrom">AvailableFrom</label>
<input class="form-control" type="datetime-local" data-val="true"
➥ data-val-required="The AvailableFrom field is required."
➥ id="AvailableFrom"
name="AvailableFrom" value="0001-01-01T00:00:00.000" />
</div>
<div class="mb-3">
<label for="SmokingPermitted">SmokingPermitted</label>
<input type="checkbox" data-val="true"
➥ data-val-required="The SmokingPermitted field is required."
➥ id="SmokingPermitted" name="SmokingPermitted" value="true" />
</div>
<div class="mb-3">
<button class="btn btn-primary">Submit</button>
</div>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ...F4" />
<input name="SmokingPermitted" type="hidden" value="false" />
</form>
所有不可为 null 的属性都包括您在上一章中遇到的其他客户端验证属性:data-val 和 data-val-required。每个属性的 data-val 属性都设置为 true。该 data-val-required 属性分配了一条错误消息,该消息已自定义为关联属性。
忽略我在第 13 章中介绍的请求验证令牌,另一个值得关注的点是已经为 SmokingPermitted 属性生成了两个输入。第一个是您希望看到的复选框,根据表 6.1 中描述的映射,为 Boolean 值呈现。第二个是包含在结束 form 标签之前的隐藏输入:
<input name="SmokingPermitted" type="hidden" value="false" />
通常,如果未选中复选框,则在提交表单时,不会向服务器传递关联属性的值。隐藏字段将确保表单提交中将包含名称-值对,无论是否选中该复选框。如果选中该复选框,则发布的值将为 true,false。否则,它将为 false。基于此,模型绑定器将能够推断出要分配给 PageModel 属性的正确值。此行为实际上是 MVC 的一个功能,其中选择要在控制器上执行的特定作可以归结为作方法采用的参数,这些参数是从已发布的值的集合中确定的。
如果您不希望呈现隐藏字段,解决方法是避免使用标记帮助程序来呈现布尔属性的复选框。在这些情况下,请改用纯 HTML。
6.2.2 使用数据注释属性控制表示
数据注释属性驻留在 System.ComponentModel.DataAnnotations 命名空间中,它提供了一种向类型添加额外信息或元数据的方法。.NET 中的各种组件框架可以使用这些附加信息来影响其行为。数据注释属性广泛用于 Razor Pages 中的表单,以影响验证(如您所见)和演示。本节介绍如何使用属性来影响作为表单的一部分生成的 HTML。在管理 UI 时,有两个属性特别值得关注:DataTypeAttribute 和 DisplayAttribute。
使用 DataTypeAttribute 影响呈现的输入类型
DataTypeAttribute 使您能够指定比 .NET 类型系统提供的更具体的类型的名称,并将该类型与属性相关联。输入标记帮助程序将使用您指定的元数据来覆盖为输入标记帮助程序的 type 属性生成的值,从而产生对指定类型唯一的 UI 行为。例如,.NET 中没有密码类型,但您可以通过数据注释属性指定字符串应被视为密码类型。框架的大部分内容都会忽略此元数据,但 input 标记帮助程序将通过将呈现的控件上的 type 属性设置为 password 来响应它。浏览器将呈现一个类型设置为密码的输入,作为一个表单控件,作为安全措施,该控件会隐藏默认情况下输入的字符。
DataType 属性有两个构造函数。一个采用 DataType 枚举值,另一个采用字符串。枚举值范围很广,但只有一个子集会影响 HTML 输入上生成的类型。表 6.2 中详细介绍了它们。
表 6.2 DataType 枚举和输入类型之间的映射
当您将多个属性应用于一个属性(例如,BindProperty 属性和 DataType)时,您可以在它们自己的一组方括号中单独应用它们,也可以使用一组括号并用逗号分隔每个属性。清单 6.5 中的两个示例都是有效的。
清单 6.5 将多个 attribute 应用于单个属性的不同选项
[BindProperty]
[DataType(DataType.Time)]
public DateTime ArrivalTime { get; set; }
[BindProperty, DataType(DataType.Time)]
public DateTime DepartureTime { get; set; }
大多数主流浏览器都支持 week 和 month 输入,使用户能够指定一年中的一周或一个月。这些输入类型没有匹配的 DataType 枚举值。相反,你可以使用 DataType 属性的构造函数,该构造函数接受一个字符串,将 input 类型设置为这些选项以及表 6.2 中未涵盖的任何其他选项:
[DataType("week")]
public DateTime ArrivalWeek { get; set; }
除了 DataType 枚举之外,还有名称类似的独立属性:
• EmailAddress
• Phone
• Url
虽然这些也会影响分配给 rendered type 属性的值,但它们继承自 ValidationAttribute 并参与验证,如上一章所示。System.ComponentModel.DataAnnotations 并不是可以找到这些类型的属性的唯一位置。HiddenInput 是属于 MVC 框架的另一个独立属性;可以在 Microsoft.AspNetCore.Mvc 命名空间下找到它。当应用于 PageModel 属性时,关联的输入将呈现,并将其 type 属性设置为 hidden。
6.2.3 格式化渲染的日期或时间值
在查看刚刚为添加新属性而创建的表单时,您可能已经注意到,为 DateTime PageModel 属性生成的 HTML 5 日期时间本地输入类型支持将值设置为毫秒(图 6.3)。但是,向用户公开此功能几乎没有意义。
图 6.3 日期时间值的默认绑定会激活在日期时间本地控件中设置毫秒的功能。
默认情况下,如果将值分配给绑定的 DateTime 属性,则其毫秒部分将公开给浏览器控件,从而激活设置这部分值的功能。您可以通过将格式字符串应用于 DateTime 值来控制这一点。使用 DisplayFormat 属性可以将格式字符串应用于属性。该格式适用于使用该属性的任何地方(假设该位置是框架识别并应用数据注释提示的位置)。格式字符串是通过属性的 DataFormatString 属性指定的,另一个属性 ApplyFormatInEditMode 必须设置为 true,才能在可编辑设置(如表单输入)中应用格式字符串:
[BindProperty, DisplayFormat(DataFormatString = "{0:yyyy-MM-ddTHH:mm}",
ApplyFormatInEditMode = true)]
public DateTime AvailableFrom { get; set; } = DateTime.Now;
或者,您可以使用 input 标签帮助程序的 format 属性来控制各个 input 控件的格式:
<input asp-for="AvailableFrom" asp-format="{0:yyyy-MM-ddTHH:mm}" />
应用了这些示例中所示的格式字符串后,只有时间的小时和分钟元素可以在浏览器中设置(图 6.4)。
图 6.4 秒和毫秒部分的时间不再可设置。
支持的日期和时间格式
基于日期和时间的输入类型主要支持根据 RFC 3339 (https://datatracker.ietf.org/doc/html/rfc3339) 中指定的规则设置格式的值。简而言之,它们要求将年份表示为四位数,并将日期和时间组件从最不具体(即年份)到最具体的顺序排序。时间部分应与日期之间用大写的 T 分隔。在 .NET 中,以下格式字符串表示这一点:
yyyy-MM-ddTHH:mm:ss.fff
秒 (ss) 和毫秒 (fff) 对于大多数浏览器来说是可选的,前提是你包含 time 元素。
周输入类型用于根据 ISO 8601 周编号选择一年中的一周。周输入支持的格式为 yyyy-Www,其中 -W 是文本,ww 表示一年中的一周。默认模型绑定器不包括对此格式的支持,因为 .NET 不支持将其作为有效的日期/时间格式字符串。如果需要使用 week 输入类型,则必须自己以正确的格式生成值。然后,您可以自己解析 Request.Form 中的值,也可以实现自定义模型 Binder。这是一个高级主题,本书不会涉及,但您可以参考我的博客文章,该文章提供了一个特定于使用周输入类型的示例:http://mng.bz/M5eW。
6.2.4 使用 DisplayAttribute 控制标签
前面,我注意到表单中呈现的许多标签值(图 6.1)缺乏用户友好性。标签标记帮助程序只是获取 C# 属性的名称,并按原样呈现它 - 包括 Pascal 大小写。如果你将 label 标签 helper 的内容留空,就会发生这种情况,就像我们在此示例表单中所做的那样。覆盖此行为的一种方法是在 label 标签的每个实例中提供您的内容:
<label asp-for="DayRate">Day Rate</label>
或者,您也可以将 DisplayAttribute 应用于属性,以通过将首选标签文本分配给其 Name 属性来控制输出:
[BindProperty]
[Display(Name="Day Rate")]
public decimal DayRate { get; set; }
此方法的主要好处是可以集中配置,因此无需在 Razor 页面中四处寻找进行更改。下面的清单显示了应用了数据类型和显示属性的 Create Property 页的修订后的 PageModel 属性。图 6.5 说明了框架如何将属性值应用于呈现的标签。
清单 6.6 使用合适的属性装饰的公共 PageModel 属性
[BindProperty]
public string Name { get; set; }
[BindProperty]
[Display(Name = "Maximum Number Of Guests")]
public int MaxNumberOfGuests { get; set; }
[BindProperty]
[Display(Name ="Day Rate")]
public decimal DayRate { get; set; }
[BindProperty]
[Display(Name = "Smoking Permitted")]
public bool SmokingPermitted { get; set; }
[BindProperty]
[DataType(DataType.Date)]
[Display(Name ="Available From")]
public DateTime AvailableFrom { get; set; }
图 6.5 数据注释属性用于管理标签和输入表示。
这样就完成了我们对基本 input 和 label 标签帮助程序的了解。在接下来的部分中,我们将重点介绍用于向用户提供一系列固定选项的 select 标签助手,然后继续讨论复选框和单选按钮输入类型的特殊性质。
6.3 select 标签助手
在我们的应用程序中,每个属性都必须位于特定城市,因此当用户添加新属性时,他们应该能够指定该属性所在的城市。您可以为此提供文本输入,但允许用户输入他们想要的任何内容的问题在于,您可能会遇到拼写错误和变化。例如,他们是输入 New York 还是 New York City?该解决方案提供了一组预先确定的选项,用户可以从中进行选择。从 UI 的角度来看,最经济和可用的方法是在 HTML select 元素中为可能会增长很多的列表提供选项。
select 标签帮助程序以 HTML select 元素为目标。它有两个自定义属性:现在熟悉的 for 属性(用于将表示所选值的 PageModel 属性绑定到 select 元素)和 items 属性(通过该属性分配选项)。选项是从 SelectListItem 对象、SelectList 对象或枚举的集合中填充的。选项也可以通过 option tag helper 单独设置。
6.3.1 创建选项
HTML option 元素表示 select 元素中的单个选项。至少,它由浏览器中显示的文本组成。为 select 标记帮助程序创建选项的最简单方法是将某种集合(如上一章中使用的城市数组)传递给 SelectList 的构造函数。要了解其工作原理,您将向属性创建表单中添加一个新元素,使用户能够指定属性所在的城市。调整 PropertyManager 文件夹中的 CreateModel 类以包括两个额外的高亮显示属性,确保 SelectedCity 属性使用 BindProperty 属性进行装饰。
列表 6.7 添加属性来表示选项和所选值
public class CreateModel : PageModel
{
[BindProperty]
public string Name { get; set; }
[BindProperty]
public int MaxNumberOfGuests { get; set; }
[BindProperty]
public decimal DayRate { get; set; }
[BindProperty]
public bool SmokingPermitted { get; set; }
[BindProperty]
public DateTime AvailableFrom { get; set; }
[BindProperty] ❶
[Display(Name="City")] ❶
public string SelectedCity { get; set; } ❶
public SelectList Cities { get; set; } ❷
public void OnGet() ❸
{ ❸
var cities = new[] { "London", "Berlin", "Paris", ❸
➥ "Rome", "New York" }; ❸
Cities = new SelectList(cities); ❸
} ❸
}
❶ SelectedCity 属性表示所选值。
❷ Cities 属性表示用户可以从中进行选择的选项。
❸ 添加了一个 OnGet 处理程序,其中包含创建城市名称数组并传递给 SelectList 构造函数的代码。
您还应该添加一个 using 指令以包括 Microsoft.AspNetCore.Mvc。Rendering namespace,这是 SelectList 类型所在的位置。现在,对位于 PropertyManager 文件夹中的 Create.cshtml.cs 文件的标记部分进行下一个清单中详述的更改。更改将包括一个 select 标签帮助程序,该帮助程序提供一系列城市供用户选择,当提交表单时,所选城市的名称将呈现到页面。
清单 6.8 修改表单以包含一个 select 标签助手
@if (Request.HasFormContentType) ❶
{ ❶
<p>You selected @Model.SelectedCity</p> ❶
} ❶
<form method="post">
<div class="mb-3"> ❷
<label class="form-label" asp-for="SelectedCity"></label> ❷
<select class="form-control" asp-for="SelectedCity" ❷
➥ asp-items="Model.Cities"></select> ❷
</div> ❷
<div class="mb-3">
<label class="form-label" asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="MaxNumberOfGuests"></label>
<input class="form-control" asp-for="MaxNumberOfGuests" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="DayRate"></label>
<input class="form-control" asp-for="DayRate" />
</div>
<div class="mb-3">
<label class="form-label" asp-for="AvailableFrom"></label>
<input class="form-control" asp-for="AvailableFrom" />
</div>
<div class="mb-3">
<label asp-for="SmokingPermitted"></label>
<input asp-for="SmokingPermitted" />
</div>
<button class="btn btn-primary">Submit</button>
</form>
❶ 如果表单已提交,则添加此代码块以呈现所选城市。
❷ 添加标签和用于管理城市选择的 select 标签助手。
导航到 /property-manager/create。您应该看到 London 出现在 select 控件中(图 6.6)。
图 6.6 London 是 select 控件中的默认选项。
请随意将选择保留为 London,或从选项中选择另一个城市,然后提交表单。您应该注意两件事:第一是选定的城市被呈现给浏览器,第二是选项列表为空(图 6.7)。
图 6.7 显示所选选项,但选项列表已消失。
那么选择去哪儿了呢?这些选项当前在 OnGet 处理程序中生成。使用 POST 方法将表单提交到服务器时,不会执行 OnGet 处理程序。由于 OnPost 处理程序中没有用于恢复 city 选项的内容,因此不会为 POST 请求生成任何选项。在下一个示例中,我们将介绍一个简单的策略,当您将更复杂的对象传递给 SelectList 时,可以纠正此问题。
设置 options 的值
到目前为止,您已将一个简单的字符串数组传递到 SelectList 中。SelectList 将数组的每个元素分配给选项的文本,从而为 select 元素生成以下 HTML。
清单 6.9 为 select city 元素生成的 HTML
<select class="form-control" id="SelectedCity" name="SelectedCity">
<option>London</option>
<option>Berlin</option>
<option>Paris</option>
<option>Rome</option>
<option>New York</option>
</select>
当 HTML 选项元素仅分配了文本时,该文本将在提交表单时作为值传递。当您处理数据库中的数据时(从第 8 章开始),您将更经常地希望使用与数据库中的数据项关联的唯一标识符,即其主键值。这些键值对普通用户没有任何意义,因此您需要将键分配给 select 选项的 value 属性,同时仍传入一段文本,使用户能够了解每个选项所代表的内容。使用 SelectList 构造函数的重载,可以将不同的值映射到选项的 value 属性和文本。下一个示例演示如何使用此重载来模拟将每个城市的数字标识符映射到 option 值的过程,同时仍向用户显示城市的名称。
您将生成 City 对象的集合,就像在上一章的复选框示例中所做的那样。您将这些属性分配给 SelectList,并指定哪个属性表示 DataValueField(选项的值),以及应将哪个属性分配给 DataTextField(选项的文本)。由于您对每个城市使用数字键,因此 SelectedCity 属性将从字符串更改为 int。此外,您需要将生成 SelectList 的代码放在一个单独的方法中,并在 OnGet 和 OnPost 方法中调用它,以便在提交表单后重新填充选项。下面的清单显示了 CreateModel 类的更改部分。SelectedCity 之前的所有属性均保持不变。
Listing 6.10 修改后的 Property Manager 的 CreateModel
[BindProperty]
[Display(Name = "City")]
public int SelectedCity { get; set; } ❶
public SelectList Cities { get; set; }
public string Message { get; set; } ❷
public void OnGet()
{
Cities = GetCityOptions(); ❸
}
public void OnPost()
{
Cities = GetCityOptions(); ❸
if (ModelState.IsValid)
{
var city = GetCityOptions().First(o => o.Value == ❹
➥ SelectedCity.ToString()); ❹
Message = $"You selected {city.Text} with value of {SelectedCity}";❹
}
}
private SelectList GetCityOptions() ❺
{ ❺
var cities = new List<City> ❺
{ ❺
new City{ Id = 1, Name = "London"}, ❺
new City{ Id = 2, Name = "Paris" }, ❺
new City{ Id = 3, Name = "New York" }, ❺
new City{ Id = 4, Name = "Rome" }, ❺
new City{ Id = 5, Name = "Dublin" } ❺
}; ❺
return new SelectList(cities, nameof(City.Id), nameof(City.Name)); ❺
} ❺
❶ 将 SelectedCity 从 string 更改为 int。
❷ 添加名为 Message 的字符串属性。
❸ 从新的私有 GetCityOptions 方法分配 Cities。
❹ 从 GetCityOptions 方法生成的数据中获取所选城市的详细信息。
❺ 添加一个用于生成城市的私有方法,以便将逻辑集中在一个地方。
Razor 页面唯一需要的更改是包括 Message 属性,以便你可以看到所选城市的确认。我将它放在页面顶部,在开始的 form 元素之前:
<p>@Model.Message</p>
运行页面并选择一个城市。提交表单后,您会注意到所选城市的详细信息已呈现给浏览器(图 6.8)。
图 6.8 所选城市的详细信息将呈现到浏览器中。
当你查看页面的源代码时,你会看到你选择的选项也应用了 selected 属性(图 6.9)。
图 6.9 所选选项应用了所选属性。
您应该能够从此行为中推断出,您可以通过将 select 标记帮助程序的值分配给绑定到其 for 属性的属性来设置所选项目。当然,您自己没有将值分配给 SelectedCity 属性;模型活页夹为您完成了这项工作。
设置所选值
除了为 bound 属性分配值之外,还可以通过将附加参数值传递给表示所选选项值的 SelectList 构造函数来设置所选值:
var cities = new SelectList(cities, nameof(City.Id), nameof(City.Name), 3);
或者,您可以显式构造 SelectListItem的集合,并将其中一个元素上的 Selected 属性设置为 true。
列表 6.11 构造 SelectListItem的集合
private SelectList GetCityOptions()
{
var cities = new List<SelectListItem>
{
new SelectListItem{ Value = "1", Text = "London"},
new SelectListItem{ Value = "2", Text = "Paris" },
new SelectListItem{ Value = "3", Text = "New York", Selected =
➥ true },
new SelectListItem{ Value = "4", Text = "Rome" },
new SelectListItem{ Value = "5", Text = "Dublin" }
};
return new SelectList(cities);
}
像这样的方法(在你显式设置所选项的地方)只有在你不通过其 for 属性将属性绑定到标记帮助程序时才有效。分配给 bound 属性的值(甚至是其默认值)将覆盖设置所选项的任何其他尝试。
6.3.2 绑定多个值
到目前为止,您已经使用了 select 标记帮助程序,使用户能够选择一系列可用选项之一。在某些情况下,您可能希望允许他们选择多个选项。当您使用 HTML 时,只需向 select 元素添加一个 multiple 属性即可启用此功能:
<select name="cities" multiple>
当这个属性存在时,浏览器通过呈现通常所说的列表框来做出响应——一个列出所有选项的框,如果内容超过控件的高度,则可能有一个滚动条(图 6.10)。在大多数浏览器中,用户可以在选择时按住 Ctrl 键来选择多个选项。
图 6.10 应用了多个属性的 Select
如果属性绑定到集合,则 select 标记帮助程序会自动呈现 Boolean multiple 属性。准确地说,它渲染 multiple=“multiple”,但仅仅存在 multiple 属性就是启用多选所需的全部内容。大多数浏览器会忽略传递给 multiple 属性的任何值。
当用户到达网站时,他们可能还没有决定要访问哪个城市。他们可能想要查看各种选项。您可以通过将 collection 属性绑定到 select 标记帮助程序,为它们提供一种轻松选择多个选项的方法。让我们通过在网站的主页上为用户添加选择表单来探讨这一点。打开 Pages 文件夹中的 Index.cshtml.cs 文件,并将以下公共属性添加到 IndexModel,表示 select 标记帮助程序 (int[] SelectedCities) 的绑定,以及带有消息的选项。
清单 6.12 多选列表的 PageModel 属性
[BindProperty]
[Display(Name = "Cities")]
public int[] SelectedCities { get; set; }
public SelectList Cities { get; set; }
public string Message { get; set; }
现在,从您一直在使用的 Create Property 页面复制相同的私有 GetCityOptions 方法以生成 SelectList,并将其添加到 IndexModel。
清单 6.13 复制 GetCityOptions 方法并将其添加到 IndexModel 中
private SelectList GetCityOptions()
{
var cities = new List<City>
{
new City{ Id = 1, Name = "London"},
new City{ Id = 2, Name = "Paris" },
new City{ Id = 3, Name = "New York" },
new City{ Id = 4, Name = "Rome" },
new City{ Id = 5, Name = "Dublin" }
};
return new SelectList(cities, nameof(City.Id), nameof(City.Name));
}
最后,在 OnGet 方法中分配 Cities。然后添加一个 OnPost 方法,该方法使用 SelectedCities 属性中的绑定值,并将其用作 Cities 集合上的筛选器,以提取所选城市的详细信息。
示例 6.14 向 IndexModel 添加处理程序方法来设置 city 选项
public void OnGet()
{
Cities = GetCityOptions();
}
public void OnPost()
{
Cities = GetCityOptions();
if (ModelState.IsValid)
{
var cityIds = SelectedCities.Select(x => x.ToString()); ❶
var cities = GetCityOptions().Where(o => cityIds.Contains(o.Value)).Select(o=>o.Text); ❷
Message = $"You selected {string.Join(", ", cities)}"; ❸
}
}
❶ 将 int 的集合转换为字符串。
❷ 过滤城市选项以仅选择已选择的城市选项。
❸ 使用字符串。Join 方法从生成的集合中构造消息。
现在,将 Index.cshtml 中的现有内容替换为下一个列表中显示的简单表单,该表单仅包含 select 标记帮助程序和一个按钮。
Listing 6.15 将表单添加到主页
<div class="col-4">
<form method="post">
<div class="mb-3">
<label class="form-label" asp-for="SelectedCities"></label>
<select class="form-control" asp-for="SelectedCities"
➥ asp-items="Model.Cities"></select>
</div>
<div class="mb-3">
<button class="btn btn-primary">Submit</button>
</div>
</form>
</div>
<p>@Model.Message</p>
运行应用程序时,很明显 select 标记帮助程序的输出与前面的示例不同,因为您应该会看到列表框出现。在按住 Ctrl 键的同时选择几个选项,然后提交表单。确认您的选择已包含在 Message 输出中,并且它们在列表框中保持选中状态(图 6.11)。
图 6.11 所选选项保持选中状态,并包含在呈现的消息中。
6.3.3 使用 OptGroup
SelectList 类包括另一个构造函数,该构造函数使您能够指定应该用于表示 HTML optgroup 元素的属性的名称,即 DataGroupField,该元素用于在向用户显示的选项列表中将相关选项组合在一起。
每个城市都属于一个国家。在 select 元素中使用选项组按国家/地区对城市选项进行分组似乎是合理的。当用户在应用程序中创建新属性时,他们可以更轻松地找到该属性所在的城市。因此,让我们向 City 类添加新的公共属性来表示国家/地区名称:
public string CountryName { get; set; }
现在,您将更改 Pages\PropertyManager\Create .cshtml.cs 文件中的 GetCityOptions 方法,以将国家/地区名称分配给所选城市,然后传入 nameof(City.CountryName) 来表示 DataGroupField。
Listing 6.16 向 city 选项添加一个选项组
private SelectList GetCityOptions()
{
var cities = new List<City>
{
new City{ Id = 1, Name = "Barcelona" , CountryName = "Spain" },
new City{ Id = 2, Name = "Cadiz" , CountryName = "Spain" },
new City{ Id = 3, Name = "London", CountryName = "United Kingdom" },
new City{ Id = 4, Name = "Madrid" , CountryName = "Spain" },
new City{ Id = 5, Name = "Rome", CountryName = "Italy" },
new City{ Id = 6, Name = "Venice", CountryName = "Italy" },
new City{ Id = 7, Name = "York" , CountryName = "United Kingdom" },
};
return new SelectList(cities, nameof(City.Id), nameof(City.Name),
➥ null, nameof(City.CountryName));
}
当您运行该页面时,您应该会看到按国家/地区分组的数据,并将国家/地区名称设置为分组标签(图 6.12)。大多数情况下,您将以面向对象的方式处理数据。您已经有一个 Country 类,因此可以通过向 City 类添加 Country 属性(在下面的清单中以粗体显示)来使用组合来表示城市和国家之间的关系。
图 6.12 城市按国家分组。
清单 6.17 将 CountryName 属性替换为 Country 属性
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public Country Country { get; set; } ❶
}
❶ 将字符串属性替换为 Country 属性。
下一个清单显示了 GetCityOptions 方法的修订版本,其中国家/地区名称是通过 Country 属性设置的。
Listing 6.18 使用组合为每个城市设置国家
private SelectList GetCityOptions()
{
var cities = new List<City>
{
new City{ Id = 1, Name = "London", Country = new Country{
➥ CountryName = "United Kingdom"} },
new City{ Id = 2, Name = "York" , Country = new Country{
➥ CountryName = "United Kingdom"} },
new City{ Id = 3, Name = "Venice", Country = new Country{
➥ CountryName = "Italy"} },
new City{ Id = 4, Name = "Rome", Country = new Country{
➥ CountryName = "Italy" } },
new City{ Id = 5, Name = "Madrid" , Country = new Country{
➥ CountryName = "Spain" } },
new City{ Id = 5, Name = "Barcelona" , Country = new Country{
➥ CountryName = "Spain" } },
new City{ Id = 5, Name = "Cadiz" , Country = new Country{
➥ CountryName = "Spain" } }
};
return new SelectList(cities, nameof(City.Id), nameof(City.Name),
➥ null, "Country.CountryName"); ❶
}
❶ 在处理复杂对象时,您可以使用文本字符串来表示 DataGroupField。
这一次,您不能使用 nameof 运算符来表示 DataGroupField 名称。您必须将完整的属性名称作为字符串传入;否则,将找不到它,从而导致在运行时出现 NullReferenceException。
6.3.4 绑定枚举
在大多数实际应用程序中,您填充选择列表的选项将来自数据库,尤其是在它们可能更改的情况下。当选项的范围自然受到限制时,您可以决定由枚举代替它们。例如,合理描述他们的工作状态的方式有限:全职就业、兼职就业、自雇、找工作、退休或接受教育。因此,您可以创建一个名为 WorkStatus 的枚举,其中包含表示所有可能选项的成员。
在您的应用程序中,应用于可以保留的属性的评级是枚举的良好候选项。评分范围从 1 星到 5 星不等,您还必须处理尚未评级的房产。因此,您将创建一个包含涵盖所有选项的成员的枚举。将新的 C# 类文件添加到名为 Rating.cs 的 Models 文件夹中,并将该类更改为枚举。
Listing 6.19 Rating 枚举
public enum Rating
{
Unrated, OneStar, TwoStar, ThreeStar, FourStar, FiveStar
}
下一步是将属性添加到 PropertyManager 文件夹中的 CreateModel:
[BindProperty]
public Rating Rating { get; set; }
您希望能够在 Razor 页面中引用此类型,因此需要使其可以使用 CityBreaks.Models 命名空间。如果您回想一下第 3 章,您会记得您可以通过向 ViewImports 文件添加合适的 using 指令来全局实现这一点,因此打开 Pages 文件夹中的那个指令,并通过在下面的列表中添加粗体代码来做到这一点。
清单 6.20 向 ViewImports 添加 using 指令
@using CityBreaks
@using CityBreaks.Models
@namespace CityBreaks.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
提供了 Html.GetEnumSelectList 方法,因此您可以轻松地将枚举绑定为选择列表的数据源。您将在将添加到“创建属性”表单的 select 标记帮助程序的 asp-items 属性中使用该方法。在 select city 列表之后添加以下代码块。
清单 6.21 使用 Html.GetEnumSelectList 方法绑定枚举
<div class="mb-3">
<label asp-for="Rating"></label>
<select class="form-control" asp-for="Rating" asp-items="Html.GetEnumSelectList<Rating>()"></select>
</div>
运行页面。枚举值应出现在选择列表中(图 6.13)。
图 6.13 通过 Html.GetEnumSelectList 方法绑定到 select list 的枚举
在此示例中,选择了 Unrated 选项。您可以检查页面的源代码以确认这一事实。这是因为其选项值 (零) 是 Rating 的默认值。如果不希望预先选择该值,可以将 PageModel 属性设置为可为 null:
[BindProperty]
public Rating? Rating { get; set; }
回想一下上一章,如果你使属性可为空,但你仍然希望使关联的表单控件成为必填字段,你将需要添加一个 Required 属性。
呈现给用户的实际值可以稍作整理。如果您以前使用过枚举,则可能会想将 Description 属性应用于您希望更加用户友好的每个成员。但是,这不会影响 select 标记帮助程序。就像其他与表单相关的标记帮助程序一样,select 标记帮助程序响应 DisplayAttribute。下面的清单演示了如何将其应用于您希望向用户显示原始属性名称以外的内容的每个成员。
清单 6.22 使用 DisplayAttribute 更改显示的值
public enum Rating
{
Unrated,
[Display(Name="1 Star")]
OneStar,
[Display(Name = "2 Star")]
TwoStar,
[Display(Name = "3 Star")]
ThreeStar,
[Display(Name = "4 Star")]
FourStar,
[Display(Name = "5 Star")]
FiveStar
}
渲染的结果如图 6.14 所示。
图 6.14 显示名称会自动应用于选项文本。
现在我们已经介绍了使用选择列表时出现的最常见问题,我们可以继续查看复选框和单选按钮控件。虽然 input 标签帮助程序以他们为目标,但他们行为的各个方面是独一无二的,值得关注。
6.4 复选框和无线电控制
我们已经介绍了在 Razor Pages 表单中使用复选框的一些方面,但值得重复这些知识,同时我们将探索使用类似的控件:单选按钮。复选框旨在使用户能够从预定义选项(如选择列表)中进行选择零个或多个。复选框可以单独表示布尔选项,当这是要求时,您应该将 PageModel 属性绑定到它们,该属性可以是布尔值或字符串,其值可以转换为布尔值(例如,“true”/“false”)。还可以通过共享相同的名称将复选框组合在一起,以使用户能够进行多项选择,例如您刚刚使用的列表框。在上一章中,你查看了模型绑定到简单集合时所做的。您是选择使用列表框还是一组复选框来促进多选,与任何事情一样,都是一个演示文稿设计决策。
单选控件表示互斥的选项。它们主要用于允许用户从多个选项中选择一个,并且只能选择一个。因此,它们几乎总是被归为一组。当选择组中的一个单选按钮时,将自动取消选择所有其他单选按钮。您不能取消选择单个无线电,这就是为什么它们不能真正单独使用的原因。取消选择一个无线电的唯一方法是选择同一组中的另一个无线电。以强制选择的形式使用一个无线电控件可能是有意义的,例如在继续之前表明您同意条款和条件。
在下一个示例中,您将使用单选按钮而不是 select 元素来选择属性评级枚举值之一。唯一需要的更改是表单本身。将 select 元素替换为下一个清单中的代码。
Listing 6.23 将一个 select 元素替换为一组 radio 控件
<p>Rating</p>
<ul class="list-group border-0">
@foreach(var option in Html.GetEnumSelectList<Rating>()) ❶
{
<li class="list-group-item p-0 border-0">
<input asp-for="Rating" type="radio"
➥ id="rating-@option.Value" value="@option.Value"> ❷
<label for="rating-@option.Value">
➥ @option.Text</label> ❸
</li>
}
</ul>
❶ 使用 Html.GetEnumSelectList 方法将枚举转换为 IEnumerable
❷ 将单选按钮绑定到 Rating 属性,但显式设置 type、id 和 value。
❸ 添加标签,并将其 for 属性设置为相应单选按钮的 id。
Html.GetEnumSelectList 方法可用于 Razor 页面中的任何位置,以将枚举转换为 IEnumerable<SelectListItem>
。您可以迭代这些并为每个 API 呈现一个 Input。您可以使用输入标记帮助程序的 for 属性将单选按钮控制组绑定到 Rating 属性,但必须使用此输入标记帮助程序显式设置类型。没有从 .NET 类型到无线电控件的映射。当您在标签帮助程序上显式设置 HTML 属性时,标签帮助程序将遵循您的分配。您对 id 属性执行了相同的作,该属性通常由 tag helper 生成。您需要自己设置该值。否则,所有输入都将具有相同的自动生成的 ID “Rating”,并且您不能在同一文档中拥有具有相同 ID 值的多个元素。因此,通过将评级的数值连接到 “rating-”,生成 “rating-0”、“rating-1” 等,从而构建了唯一值。图 6.15 显示了单选按钮的最终渲染集合。
图 6.15 基于枚举渲染的无线电
我在此处使用了 Html.GetEnumSelectList 来生成适合在 Razor 页面中使用的类型的集合,因为返回类型 IEnumerable <SelectListItem>
可以更轻松地将枚举值绑定到无线电。数据可以由任何类型表示,这些类型公开了可以映射到输入值的属性,以及某种类型的描述性标签。
6.5 上传文件
在大多数情况下,您将使用表单将用户的数据作为简单字符串捕获,尽管在服务器上,它们可能会转换为数字、DateTime 或布尔值。表单还可用于将二进制数据发送到服务器,从而可以上传文件。要将模型绑定与成功上传文件相结合,您必须满足三个基本要求:
- 表单必须使用 POST 方法。
- 表单必须将 enctype 属性设置为 multipart/form-data。
- 如果要启用多个文件上传,则上传的文件必须映射到 IFormFile 类型的实例或它们的集合。
IFormFile 接口位于 Microsoft.AspNetCore.Http 命名空间中,表示通过 HTTP 发送的文件。除了一些提供对文件相关信息(其 FileName、Length、ContentType 等)的访问的属性外,IFormFile 接口还指定了几种将内容复制到 Stream 的方法,以便您可以保存它。
在 Razor Pages 应用程序中,可以将文件以二进制形式本地保存到数据库,也可以将其保存到文件系统。过去曾爆发过宗教战争,争论哪种方法更好!通过一些搜索,您可以在互联网上找到这些历史战场的遗迹。战斗往往肆虐的山丘与储存和加工成本有关。由于现在两者都相对便宜,因此现在更可能基于可访问性和安全性来决定存储位置。如果您希望能够出于任何原因快速访问上传的文件,那么文件系统是有意义的。如果您存储的文档是敏感的,则可能需要利用数据库系统提供的内置安全功能。
文件上传的危险
允许将文件上传到您的应用程序存在危险。至少,文件上传为恶意用户提供了一种将恶意软件上传到您的 Web 服务器的方法。您应该花一些时间查看开放 Web 应用程序安全项目 (OWASP;http://mng.bz/aJx7)了解您可能会面临的潜在问题以及您应该实施的缓解措施来保护自己。
默认情况下,早期版本的 ASP.NET 运行时将 HTTP 请求的最大大小限制为 4 MB,以帮助防止因 Web 服务器因过多的数据而无法在内存中处理而导致拒绝服务 (DOS) 攻击。在 ASP.NET Core 中,Kestrel Web 服务器的请求长度限制为 30 MB。您可以通过将 RequestSizeLimit 属性应用于 PageMode 类来实现下限,并提供一个以字节为单位的值,该值表示请求的最大允许大小。例如,以下将对总请求强制实施 1 MB 的限制:
[RequestSizeLimit(1048576)]
public class CreateModel : PageModel
如果总大小超过该值,框架将返回 400 Bad Request 状态代码。
在本章的最后一个示例中,您将上传一个文件并将其保存到文件系统中。您将介绍一个用于创建城市的页面,该表单将包含一个文件上传控件,用于存储每个城市的标志性图像。这将使您有机会复习这两章中关于表单的其他知识。您将应用验证以确保每个城市都有一个名称和一个图像,并验证上传文件扩展名以确保仅接受和处理 JPG 文件。您将为此目的编写自定义验证器,因为您可能希望重用验证逻辑。有一个可用的扩展验证器,但它根据字符串而不是文件上传进行验证。
上传文件后,您会将其重命名为城市的名称。这样,您可以确保每个城市的图像都是唯一的名称,并且在出现命名冲突时不会意外覆盖另一个城市的图像。如果上传的文件包含恶意文件名,它还可以缓解覆盖服务器上关键文件的情况。成功处理上传后,您将使用 PRG (post-redirect-get) 模式重定向到将显示图像的另一个页面,并使用 TempData 存储城市名称和图像名称。
让我们从自定义验证器开始。将 C# 类文件添加到现有的 ValidationAttributes 文件夹中,并将其命名为 UploadFileExtensionsAttribute.cs。以下是验证器的完整代码,包括 using 指令。
清单 6.24 UploadFileExtensions 自定义验证器
using System.ComponentModel.DataAnnotations;
namespace CityBreaks.ValidationAttributes
{
public class UploadFileExtensionsAttribute : ValidationAttribute
{
private IEnumerable<string> allowedExtensions; ❶
public string Extensions { get; set; } ❷
protected override ValidationResult IsValid
➥ (object value, ValidationContext validationContext)
{
allowedExtensions = Extensions? ❸
.Split(new char[] { ',' }, ❸
➥ StringSplitOptions.RemoveEmptyEntries) ❸
.Select(x => x.ToLowerInvariant()); ❸
if (value is IFormFile file &&
➥ allowedExtensions.Any())
{
var extension = ❹
➥ Path.GetExtension(file.FileName.ToLowerInvariant());❹
if (!allowedExtensions.Contains(extension)) ❹❺
{ ❺
return new ValidationResult(ErrorMessage ❹
?? $"The file extension must be ❹
➥ {Extensions}"); ❹
} ❹
}
return ValidationResult.Success; ❺
}
}
}
❶ 创建一个私有字段来容纳允许的扩展。
❷ 创建一个 public property,以便该属性的用户可以设置允许的扩展。
❸ 将用户提供的扩展分配给 private 字段,将它们从逗号分隔的字符串转换为小写值的集合。
❹ 使用模式匹配来确保此属性已应用到的属性是 IFormFile。如果是,并且用户提供了扩展名,则检查文件名是否与其中任何一个匹配。否则,将返回一条错误消息。
❺ 在所有其他情况下,验证成功。
这不是生产就绪的验证器。例如,它没有考虑到用户不知道是否在允许的扩展名列表中包含前导点。但是,它足以进行演示。它与前面的验证器非常相似,因为它提供了一个 public 属性,使用户能够传入接受的文件扩展名列表。假设特性已应用到的属性是 IFormFile 类型。在这种情况下,将执行验证逻辑,如果未提供文件扩展名,则验证将失败。
现在您需要添加一个用于创建城市的页面。将新文件夹添加到名为 CityManager 的 Pages 文件夹,然后在其中添加一个名为 Create 的新 Razor 页面。打开 Create.cshtml.cs 文件,并将 using 指令更改为以下内容。
示例 6.25 城市 CreateModel 类的 Using 指令
using CityBreaks.ValidationAttributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
然后更改 CreateModel 类本身的内容。
Listing 6.26 City CreateModel 类
public class CreateModel : PageModel
{
private readonly IWebHostEnvironment _environment; ❶
public CreateModel(IWebHostEnvironment environment) ❶
{ ❶
_environment = environment; ❶
} ❶
[BindProperty] ❷
[Required] ❷
public string Name { get; set; } ❷
[BindProperty] ❷
[Required] ❷
[UploadFileExtensions(Extensions = ".jpg")] ❷
public IFormFile Upload { get; set; } ❷
[TempData] ❸
public string Photo { get; set; } ❸
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
TempData["Name"] = Name; ❹
Photo = $"{Name.ToLower().Replace(" ", "-")} ❺
➥ {Path.GetExtension(Upload.FileName)}"; ❺
var filePath = Path.Combine( ❺
➥ _environment.WebRootPath, "images", "cities", Photo); ❺
using var stream = System.IO.File. ❺
➥ Create(filePath); ❺
await Upload.CopyToAsync(stream); ❺
return RedirectToPage( ❻
➥ "/CityManager/Index"); ❻
}
return Page();
}
}
❶ 将 IWebHostEnvironment 服务注入 CreateModel,以便您可以使用它来查找 wwwroot 文件夹。
❷ 为城市名称和图片上传添加绑定目标。
❸ 添加 TempData 属性以表示图像文件名。
❹ 将提交的城市名称分配给 TempData。
❺ 重命名上传的文件,并将其保存到 image 文件夹中名为 cities 的文件夹中。
❻ 重定向用户。
IWebHostEnvironment 服务提供有关应用程序正在运行的 Web 托管环境的信息。将其注入 CreateModel 以使用其 WebRootPath 属性查找 wwwroot 文件夹,然后为城市名称和文件上传添加绑定目标。两者都是必需的 — 使用 Required 属性进行修饰 — 并且 Upload 属性也使用新的 validation 属性进行修饰。您选择只接受 JPG 文件。您还添加了另一个使用 TempData 属性修饰的属性。您将使用它来存储上传图像的文件名,以便在重定向用户时可以使用它。
如果验证成功,您将获取提交的城市名称并将其添加到 TempData:
TempData["Name"] = Name;
但是,为什么不直接将 TempData 特性添加到现有的 Name 属性中,就像对 Photo 属性所做的那样呢?使用 TempData 属性,您可以设置 TempData 和从 TempData 获取值。模型绑定和 TempData 彼此冲突,因为它们都提供了一种机制,用于使用 HTTP 请求中的值填充页面属性。一个必须在另一个之后执行,默认行为是 TempData 填充在模型绑定之后进行。因此,模型绑定分配给属性的任何值都将被 TempData 中的任何内容覆盖,这通常为零。因此,您将手动进行分配。
您相当简单的重命名逻辑会从城市名称中删除空格和连字符,然后使用小写版本作为文件名。然后,文件将保存在 cities 文件夹中,该文件夹位于 wwwroot\images 中,位于您在上一章中创建的 Flags 文件夹旁边。在将文件保存到该文件夹之前,您需要手动创建该文件夹。保存文件后,将用户重定向到尚不存在的页面。
在创建该页面之前,您需要将表单添加到 Create.cshtml 文件。清单 6.27 显示了上传表单的代码。您现在应该已经熟悉其中的大部分内容。唯一需要注意的一点是在 form 标记帮助程序上添加了 enctype 属性,指定了表单数据应该如何编码。这被设置为 multipart/ form-data,这是成功上传表单的三个要求之一。如果省略此项,则编码将默认为 application/x-www-form-urlencoded,从而仅将文件名发布到服务器。您已将 accept 属性应用于文件输入。其值设置为应限制上传的文件扩展名。支持此属性的用户代理将对用户可以从中选择的可用文件应用过滤器,以防止上传错误的类型。这可以被视为客户端验证的另一种方法,但您不应依赖它。
列表 6.27 创建城市表单
@page
@model CityBreaks.Pages.CityManager.CreateModel
@{
}
<h4>Create City</h4>
<div class="row">
<div class="col-md-4">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Upload" class="control-label"></label><br />
<input asp-for="Upload" accept=".jpg" />
<span asp-validation-for="Upload" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section scripts{
<partial name="_ValidationScriptsPartial"/>
}
现在,您将添加成功提交表单重定向到的页面。将新的 Razor 页面添加到名为 Index.cshtml 的 CityManager 文件夹,并将页面内容更改为以下内容。
Listing 6.28 确认图片上传
@page
@model CityBreaks.Pages.CityManager.IndexModel
@{
}
@if(TempData["Name"] != null){
<div class="col-6">
<h3>@TempData["Name"]</h3>
<img src="/images/cities/@TempData["Photo"]" class="img-fluid" alt="@TempData["Name"]"/>
</div>
}
同样,这里没有什么新东西。检查 TempData 是否包含某些内容,然后将值呈现给浏览器。然后,将图像文件名分配给 img 元素的 src 的一部分,并使用 Bootstrap img-fluid CSS 类使图像响应以适应其容器。
运行应用程序,导航到 /city-manager/create,然后输入城市名称。找到该城市的图片(我使用了 Unsplash—https://unsplash.com/—用于免费用于商业和非商业用途的图片),并上传它。如果一切正确,您应该看到城市的名称和图像呈现到浏览器中,以及保存到 Images\cities 文件夹中的图像(图 6.16)。
图 6.16 上传到浏览器的图片文件
在过去的几章中,您已经完成了大量表单方面的工作,这将对您创建自己的应用程序有所帮助。之前,您了解了如何使用 Razor 语法生成动态内容,以及如何通过路由显示页面。因此,您已经在应用程序的表示层方面介绍了很多内容。
在接下来的几章中,您将开始了解应用程序的逻辑组件:其服务。下一章将介绍什么是服务以及如何在 ASP.NET Core 应用程序中管理服务。除此之外,您将开始查看 ASP.NET Core 提供的用于管理数据和用户的服务。
总结
表单和表单作标记帮助程序会生成用于表单提交的 URL。
表单标记帮助程序会生成一个包含请求验证令牌的隐藏输入。
输入标记帮助程序会根据绑定到它们的模型属性的数据类型生成不同的类型属性。它们还会生成用于客户端验证的 data-val 属性。
您可以在模型属性上使用 DataType 属性来生成更具体的类型属性。
默认情况下,label 标签帮助程序呈现原始属性名称。您可以使用 Display 属性来呈现对用户更友好的内容。
select 标签帮助程序以 HTML select 元素为目标。它通过其 items 属性呈现绑定到它的选项。
选项可以作为 SelectListItem的集合或 SelectList 提供。
您可以通过为 select 标记帮助程序上绑定到 for 属性的属性分配值来设置所选项。
您可以通过将 select 标记帮助程序绑定到集合属性来启用多个选择。
SelectList 支持使用选项组。
可以使用 Html.GetEnumSelectList 方法将枚举绑定到 select 标记帮助程序。
当您使用输入标记帮助程序生成复选框时,将 Boolean 属性绑定到该复选框会生成一个隐藏字段,其值设置为 false。
无线电控件必须显式指定其类型。
文件上传需要 multipart/form-data 编码才能正常工作。
必须发布上传的文件,并且必须将其绑定到 IFormFile 类型。