ASP.NET Core Razor Pages in Action 3 使用 Razor Pages

ASP.NET Core Razor Pages in Action 3 使用 Razor Pages

本章涵盖

• 使用 Razor 模板生成 HTML
• 学习 Razor 语法
• 使用布局、局部和标记帮助程序
• 将 PageModel 理解为控制器和视图模型
• 使用处理程序方法和 IActionResult

此时,您应该对 Razor Pages 应用程序的工作部分有很好的了解,包括如何创建一个应用程序、生成文件的角色以及如何通过请求管道配置应用程序的行为。现在,你已准备好深入了解如何使用 Razor Pages 应用程序中的主要参与者:Razor 页面本身。

在学习本章时,您将学习如何使用 Razor 语法生成动态 HTML 并协调布局和部分文件,以减少代码重复并提高重用率。您已经简要介绍了布局和部分,但要提醒您,布局充当多个页面的一种主模板,而部分文件由可插入主机页面或布局的 UI 片段组成。

您还将了解 PageModel 类,这是 Razor Pages 的一项基本功能,它既充当 MVC 控制器又充当视图模型,或者充当特定于特定视图或页面的数据容器。您将探索如何使用 PageModel 的视图模型方面以强类型方式向 Razor 页面公开数据,这将提高您作为开发人员的效率。这样做还使您能够有效地使用标记帮助程序,或使服务器端代码能够参与 HTML 生成过程的组件。您将了解 ASP.NET Core 中提供的一些不同类型的标签帮助程序以及如何使用它们。

最后,您将看到 PageModel 对象如何充当页面控制器,处理页面请求并决定使用哪个模型和视图。处理程序方法在请求处理中起着重要作用,您将了解其使用背后的重要约定以及它们通常使用的返回类型 (IActionResult)。

3.1 使用 Razor 语法

所有 Web 开发框架都需要能够动态生成 HTML。它们几乎完全依赖于一种称为 Template View 的设计模式。此模式涉及使用由嵌入在 HTML 中的服务器端代码组成的标记或占位符,这些代码解析为对处理和呈现动态内容的调用。

动态内容可以采用多种形式。它通常采用从数据存储(例如数据库)中提取的数据的形式,但如您所见,它也可以只是一些计算的结果,例如一天中的时间。除了内容本身之外,您还需要嵌入服务器端代码来控制内容的呈现。例如,如果动态内容是一个集合(如列表),则需要在代码中迭代它以显示每个项目。或者,您可能只需要在某些条件下显示数据,例如,如果用户有权查看数据。因此,除了使您能够将数据实例嵌入到页面中之外,模板语法还必须使您能够包含控制页面内处理的语句块。

许多框架(如旧版本的 ASP.NET、PHP、Ruby on Rails 和 Java Server Pages)使用类似 HTML 的标记作为标记,让模板处理器知道 HTML 和服务器端代码之间的转换位置。Razor 语法使用 @ 符号作为过渡标记,并具有几个简单的规则。第一条规则是 Razor 语法仅适用于扩展名为 .cshtml 的 Razor 文件的内容。这是 cs 代表 C Sharp 和 html 代表超文本标记语言的混合体。以下部分介绍了更多规则,这些部分研究了使用 Razor 语法的特定方案,首先介绍如何向 Razor 页面添加指令和代码块。

3.1.1 指令和代码块

首先,让我们再看一下上一章中用于计算一天中的时间的代码,了解如何使用 Razor 语法在 Razor 页面中包含服务器端 C# 代码。清单 3.1 中的示例演示了 Razor 语法的三个方面:如何在页面中包含指令,如何包含 C# 代码的独立块,以及如何在呈现的 HTML 中包含 C# 表达式的结果或变量的值。

清单 3.1 在 Welcome 页面中回顾 Razor 语法

@page                                                               ❶
@model WebApplication1.Pages.WelcomeModel                           ❶
@{                                                                  ❷
    ViewData["Title"] = "Welcome!";                                 ❷

    var partOfDay = "morning";                                      ❷
    if(DateTime.Now.Hour > 12){                                     ❷
        partOfDay = "afternoon";                                    ❷
    }                                                               ❷
    if(DateTime.Now.Hour > 18){                                     ❷
        partOfDay = "evening";                                      ❷
    }                                                               ❷
}                                                                   ❷
<h1>Welcome</h1>
<p>It is @partOfDay on @DateTime.Now.ToString("dddd, dd MMMM")</p>  ❸

❶ 指令
❷ C# 代码块
❸ 作为输出的一部分呈现的 C# 内联表达式

此示例中 @ 符号的第一个实例演示了如何将指令添加到页面。指令是 C# 表达式,以 @ 符号开头,后跟保留字(例如,page 或 model),并启用页面内的功能或更改内容的解析方式。支持多个指令。page 指令将此文件表示表示可导航页面,如果它表示要浏览的页面,则它必须出现在 CSHTML 文件的顶行中。model 指令指定充当此页面模型的数据类型,默认情况下,该数据类型是页面附带的 PageModel 类。PageModel 是本章后面要关注的重点。

下一个最常用的指令可能是 using 指令,它将命名空间引入范围,因此可以在不使用其完全限定名称的情况下引用它们的类型。下一个清单说明了用于简化在 System.IO 中使用静态 Path 类的 using static 指令,否则该指令将与 Razor 页面的 Path 属性冲突。

清单 3.2 使用 Razor 语法添加 using 指令

@page
@model WebApplication1.Pages.WelcomeModel
@using static System.IO.Path                       ❶
@{
    var extension = GetExtension("somefile.ext");  ❷
}

❶ using static 指令使静态 Path 类可用,而无需指定类名。请注意,using 指令的末尾没有分号,就像 C# 代码文件中那样。分号在 Razor 文件中是可选的。
❷ 调用静态 Path.GetExtension 方法,而无需包含类名。

Razor 页面支持许多指令。有些是特定于页面的,例如 page 和 model 指令,但其他一些,包括 using 指令,可以通过将它们包含在 ViewImports 文件中来应用于多个页面。

ViewImports 文件是一种名为 _ViewImports.cshtml 的特殊类型的文件,它提供了一种机制,用于集中适用于 CSHTML 文件的指令,因此无需像在前面的示例中对 System.IO.Path 所做的那样,将它们单独添加到 Razor 页面。默认的 ViewImports 文件包括三个指令:

• 引用项目命名空间的 using 指令(在我们的示例中为 WebApplication1)
• 一个命名空间指令,用于为受 ViewImports (WebApplication1.Pages) 影响的所有页面设置命名空间
• 用于管理标签帮助程序的 addTagHelper 指令

标记帮助程序是与标记中的标记一起使用以自动生成 HTML 的组件。本章稍后将更详细地介绍它们。

ViewImports 文件中的指令会影响位于同一文件夹及其子文件夹中的所有 .cshtml 文件。Razor Pages 应用程序可以支持的 ViewImports 文件数没有限制。您可以将其他 ViewImports 文件放在子文件夹中,以添加到顶级 ViewImports 文件的指令或覆盖其设置。某些指令,例如用于管理标签帮助程序的指令、using 指令和 inject 指令(用于使服务(在第 7 章中介绍)对页面可用)是累加的,而其他指令会随着您靠近页面而相互覆盖。因此,例如,如果为该子文件夹中的 ViewImports 文件中的 namespace 指令分配了不同的值,则 Pages 文件夹中的 ViewImports 中指定的命名空间将被覆盖该子文件夹中的页面。

清单 3.1 中第二个突出显示的项目是一个代码块。代码块以 @ 符号开头,后跟左大括号,然后是右大括号:

@{
    ... C# code goes here
}

放置在代码块中的任何内容都是纯 C# 代码,必须遵循 C# 语法规则。您可以在 Razor 页面中包含多个代码块,但应将它们保持在最低限度,仅将它们限制为用于管理演示文稿的逻辑。Razor 页面中的代码块过多通常表明 UI 中可能有应用程序逻辑,应避免这种情况,因为当它混合在 HTML 中时,很难测试。例如,计算一天中时间的逻辑不应位于 Razor 页面中。它应该位于 PageModel 类中,该类可以单独测试,或者如果算法可能在多个位置使用,则应将其放在自己的类中。在本章后面,您将把算法移动到 PageModel 类中。

Razor 还支持另一种类型的代码块:函数块。通过添加 functions 指令,然后左大括号和右大括号来创建 functions 块:

@functions{
    ... C# code goes here
}

同样,functions 块中的代码是纯 C#。您可以将计算一天中的部分时间的算法重构为 functions 块,如下所示。

清单 3.3 在 functions 块中声明方法

@functions{
    string GetPartOfDay(DateTime dt)
    {
        var partOfDay = "morning";
        if (dt.Hour > 12)
        {
            partOfDay = "afternoon";
        }
        if (dt.Hour > 18)
        {
            partOfDay = "evening";
        }
        return partOfDay; 
   }
}
<p>It is @GetPartOfDay(DateTime.Now)</p>

您还可以将此方法添加到标准代码块中。标准代码块和功能块之间的区别在于,功能块支持公共成员的声明,而标准代码块不支持公共成员。但是,通常建议尽量减少功能块的使用,原因与完全减少代码块的原因相同。它们鼓励将应用程序代码与 HTML 混合,使其难以重用、隔离和测试。

在 Razor 页面文件中适当使用功能块将包括管理表示逻辑的小例程,并且仅适用于放置它们的页面。它们对于您当前的目的也很有用,即简化 Razor 语法的学习,而无需在文件之间切换。

3.1.2 使用表达式呈现 HTML

Razor 的主要用途是将动态内容呈现为 HTML。您已经了解了如何将变量或表达式的值呈现给浏览器,方法是将它们内联放置在要输出值的 HTML 中,并在它们前面加上 @ 符号:

<p>It is @partOfDay on @DateTime.Now.ToString("dddd, dd MMMM")</p>

此示例中的表达式称为 隐式表达式。在 Razor 文件中经常使用的另一种表达式类型是显式表达式,其中表达式本身位于括号内,并以 @ 符号为前缀。通常在表达式中有空格或表达式包含尖括号(即 < 和 >)的情况下使用显式表达式,例如在泛型方法中。如果不将此类表达式放在括号内,则尖括号将被视为 HTML。下面是一个涉及使用三元运算符的表达式示例。表达式中包含空格,因此它必须作为显式表达式包含在 Razor 文件中:

<p>It is @(DateTime.Now.Hour > 12 ? "PM" : "AM")</p>

此示例将向浏览器呈现 “PM” 或 “AM”,具体取决于执行表达式的时间。

3.1.3 Razor 中的控制方块

服务器端代码主要用于 Razor 文件中,以控制演示输出。因此,您使用的大多数 Razor 语法都将由控制块组成,例如,页面中的选择和迭代语句(如 if-else、foreach 等),这些语句应用处理逻辑来有条件地呈现输出或循环访问项集合。这些控制块与您之前看到的代码块的不同之处在于,它们嵌入在要呈现的 HTML 内容中,而不是与大括号内的标记隔离。

Razor 支持使用 C# 选择和迭代语句,方法是在打开块的关键字前面加上 @ 符号。下面的清单演示了如何将其应用于 if-else 语句。

列表 3.4 Razor 中的选择语句支持

@if(DateTime.Now.Hour <= 12)
{
    <p>It is morning</p>
}
else if (DateTime.Now.Hour <= 18) 
{ 
    <p>It is afternoon</p>
}
else
{
    <p>It is evening</p>
}

在此示例中,if-else 语句的工作原理是仅根据正在测试的条件(在本例中为执行页面的时间)呈现其中一个段落。请注意,在 else 关键字之前不需要 @ 符号。事实上,如果您尝试这样做,将导致错误。

清单 3.5 说明了使用 switch 语句作为清单 3.4 中 if-else 块的替代方案。同样,@ 符号仅在 opening switch 关键字之前是必需的。

列表 3.5 Razor 中的 Switch 语句示例

@switch (DateTime.Now.Hour)
{
    case int _ when DateTime.Now.Hour <= 12:
        <p>It is morning</p>
        break;
    case int _ when DateTime.Now.Hour <= 18:
        <p>It is afternoon</p>
        break;
    default:
        <p>It is evening</p>
        break;
}

在迭代集合进行渲染时,您经常会发现自己需要在 Razor 页面中使用迭代语句。假设您正在创建一个度假套餐网站,并且您需要呈现可能目的地的列表。以下清单中的代码演示了如何使用 foreach 语句将城市名称数组的成员呈现为无序列表。

列表 3.6 Razor 中的 foreach 语句示例

@functions{
    public class City
    {
        public string Name { get; set; }
        public string Country { get; set; }
    }
    List<City> cities = new List<City>{
        new City { Name = "London", Country = "UK" },
        new City { Name = "Paris", Country = "France" },
        new City { Name = "Rome", Country = "Italy" } ,
        new City { Name = "Berlin", Country = "Germany" },
        new City { Name = "Washington DC", Country = "USA" }
    };
}

<ul>
    @foreach (var city in cities)
    {
        <li>@city.Name</li>
    }
</ul>

3.1.4 渲染文本字符串

到目前为止,所有示例都显示了 Razor 在 HTML 和 C# 代码之间的转换。@ 符号后面的任何内容都被视为 C# 代码,直到遇到 HTML 标记。有时你可能需要渲染文本字符串而不是 HTML。有两种方法可以告诉 Razor 值是文本字符串,而不是 C# 代码。第一种方法是在每行中文本字符串的第一个实例前面加上 @:。

清单 3.7 在 Razor 中渲染文本字符串

@foreach (var city in cities)
{
    if (city.Country == "UK")
    {
        @:Country:  @city.Country, Name: @city.Name
    }
}

或者,您也可以使用 标记将内容括起来。如果您正在处理多行内容,这尤其有效,如下面的清单所示。

清单 3.8 使用 text 标签渲染多行文字字符串

@foreach (var city in cities)
{
    if (city.Country == "UK")
    {
        <text>Country:  @city.Country<br />
        Name: @city.Name</text>
    }
}

文本标记不会作为输出的一部分呈现;仅呈现其内容。此外,使用文本标签会导致从出现在它们之前或之后的输出中删除空格。

3.1.5 渲染文本 HTML

Razor 的默认行为是按字面呈现 Razor 页面中的任何标记,但将 HTML 编码应用于解析为字符串的所有表达式的结果。任何非 ASCII 字符以及可能不安全的字符(可能有助于将恶意脚本注入网页的字符),如 <、> 和 “,都被编码为它们的 HTML 等效字符:<、>、&、” 等。 下面的清单显示了一些 HTML 被分配给呈现给浏览器的变量。

列表 3.9 分配给渲染的输出变量的 HTML

@{
    var output = "<p>This is a paragraph.</p>";
}
@output

生成的 HTML 是

<p>This is a paragraph.</p>

图 3.1 演示了它在浏览器中的显示方式。

图 3.1 默认情况下,非 ASCII 和特殊 HTML 字符被编码以供输出。

如果你有一个 HTML 字符串,并且不希望 Razor 对其进行编码,则可以使用 Html.Raw 方法来阻止编码:

@Html.Raw(“<p>This is a paragraph.</p>”)

这适用于以下方案:例如,将 HTML 存储在数据库中以供显示 - 这是大多数内容管理系统等的典型要求。但是,您应该确保在将 HTML 包含在页面之前对其进行清理。否则,您可能会让您的站点受到脚本注入攻击。您将在第 13 章中了解这些漏洞和其他漏洞。

对于以大量使用或专门使用非 ASCII 字符的语言存储和输出内容的网站的站点的开发人员来说,默认情况下应用的激进编码级别可能并不可取。以这段德语为例(翻译过来就是 “Charges for oversized luggage”):

var output = "Gebühren für übergroßes Gepäck";

当它作为 @output 嵌入到标记中时,将编码为以下内容:

Gebühren für übergroßes Gepäck

对于非拉丁语语言(如西里尔语、中文和阿拉伯语),每个字符都编码为其 HTML 等效项,这可能会显著增加生成的源代码中的字符数。虽然输出可以正确呈现,但页面的最终大小和整体网站性能可能会受到不利影响。

可以安排更广泛的字符,因此不会对它们进行编码。您可以通过设置 WebEncoderOptions 在 Program.cs 中执行此作。默认情况下,允许的字符范围(即未编码的字符)仅限于基本拉丁字符集。清单 3.10 演示了如何配置选项以将 Latin-1 Supplement 字符集添加到允许的范围,其中包括重音元音和德语 eszett 字符 (ß)。

清单 3.10 配置 WebEncoderOptions 以添加 Latin-1 补充

builder.Services.Configure<WebEncoderOptions>(options =>
{
    options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Latin1Supplement);
});

请注意,您在此处设置的任何内容都将覆盖默认设置,这就是为什么您需要包含 BasicLatin 范围的原因。如果您不确定应该包含哪些字符集,可以在此处查看:http://www.unicode.org/charts/。或者,您可以只指定 UnicodeRanges.All。

模板化 Razor 委托

模板化 Razor 委托功能使你能够使用委托创建 Razor 模板并将其分配给变量以供重复使用。你可能还记得我们在上一章中对中间件的讨论,委托是一种表示方法签名和返回类型的类型。Razor 模板委托表示为 Func<dynamic、object>(泛型委托)。该方法的主体包含 Razor 的片段,其开始的 HTML 标记以 @ 符号为前缀。input 参数表示数据,并且是动态类型,因此它可以表示任何内容。数据可通过名为 item 的参数在模板中访问。

在列表 3.6 中,我们创建了一个城市列表,然后使用嵌入在 HTML 中的 foreach 语句将其呈现给浏览器,以呈现一个无序列表。在示例 3.11 中,我们将无序列表的渲染提取到一个模板中,该模板构成了委托的主体。

myUnorderedListTemplate 变量定义为 Func<dynamic, object>,与模板化 Razor 委托的定义匹配。在该数据库中,假定 item 参数表示城市的集合。这些被迭代并呈现为无序列表。以下清单显示了如何将列表的生成分配给 Razor 模板委托。

清单 3.11 定义模板化 Razor 委托

@{
    Func<dynamic, object> myUnorderedListTemplate = @<ul>
        @foreach (var city in item)
        {
            <li>@city.Name</li>
        }
    </ul>;
}

定义模板后,您可以将清单 3.6 中生成的数据传递到其中:

@myUnorderedListTemplate(cities)

此示例依赖于动态输入参数,这会导致潜在的错误,这些错误仅在运行时出现,例如,如果您拼写了属性的名称错误,或者尝试访问不存在的成员。您可以使用强类型来限制模板接受的类型,如下面的清单所示,其中 dynamic 参数已替换为 List

清单 3.12 缩小模板化 Razor 委托中数据项的类型

@{
    Func<List<City>, object> myUnorderedListTemplate = @<ul>
        @foreach (var city in item)
        {
            <li>@city.Name</li>
        }
    </ul>;
}

模板化 Razor 委托的缺点之一是它只接受一个表示数据项的参数,尽管对数据类型没有限制。它可以表示复杂类型,因此,如果您的模板设计为使用多个城市列表,则可以创建一个包含模板所需的一切的包装器类型。

还有一个替代方法,可用于定义可以采用任意数量的参数的模板。要利用此功能,请在返回 void(或 Task,如果需要异步处理)的代码或函数块中创建一个方法,并且只在方法正文中包含 HTML 标记。

列表 3.13 在 Razor 页面中声明模板的替代方法

@{ 
   void MyUnorderedListTemplate(List<City> cities, string style)    ❶
    {
        <ul>                                                        ❷
        @foreach(var city in cities)
        {
            <li class="@(city.Name == "London" ? style : null)">@city.Name</li>
        }
        </ul>
    }
}
@{ MyUnorderedListTemplate(cities, "active"); }                     ❸

❶ 允许在返回 void 或 Task 的方法中使用标记。
❷ 开始标签没有 @ 符号的前缀。
❸ 由于该方法返回 void 或 Task,因此必须在代码块中调用它。

请注意,与模板委托不同,该方法可以指定任意数量的参数,并且前导 HTML 标记不以 @ 符号为前缀。此方法采用两个参数 — 第二个参数表示应有条件地应用于列表项的 CSS 类的名称。如果希望能够使用模板委托实现类似的功能,则需要创建一个包装 List<City> 和 String 的新类型。

无论是使用此方法还是模板委托,这些帮助程序都只能在同一 Razor 页面中重复使用。如果您想在多个页面中重用 HTML 代码段,则有更灵活的替代方案,包括部分页面、标签帮助程序和视图组件。我们将在本章后面更详细地介绍部分页面和标签帮助程序。视图组件在第 14 章中介绍。

Razor 中的注释

Razor 页面文件支持页面标记区域中的标准 HTML 注释和代码块中的 C# 注释。它们还支持 Razor 注释,这些注释以 @ 开头,以 @ 结尾。与 HTML 注释中的内容不同,Razor 注释之间的任何内容都不会呈现到浏览器。清单 3.14 中的代码显示了 HTML 注释中的 C# foreach 语句。呈现页面时,将处理 Razor 代码,生成的项列表在源代码中显示为注释。

列表 3.14 导致内容被渲染的 HTML 注释

<!--<ul>    
    @foreach(var city in cities)
    {
        <li>@city.Name</li>
    }
</ul>-->

以下清单在 Razor 注释中提供了相同的 foreach 语句。源代码中不包含任何内容。

列表 3.15 从源代码中排除 Razor 注释中的内容

@*<ul>    
    @foreach(var city in cities)
    {
        <li>@city.Name</li>
    }
</ul>*@

这样就完成了对 Razor 语法的了解,该语法演示了如何在 HTML 中嵌入服务器端代码以形成单个页面的模板。在以下部分中,您将了解如何使用布局页面和部件创建可跨多个页面重复使用的代码模板。

3.2 布局页面

这是一个罕见的网站,不会在多个页面之间共享通用内容。在本书的后面部分,您将构建一个提供度假套餐的 Web 应用程序。此类站点的轮廓草图很可能类似于图 3.2。

图 3.2 此示例中的页眉、导航、Deal of the Day 小部件和页脚旨在显示在所有页面上。主内容区域表示特定于每个页面的内容。

在示例草图中,页眉、导航、每日交易 Widget 和页脚旨在显示在网站的所有页面上。其中一些元素的实际内容可能因页面而异,但基本结构将适用于所有页面。主内容区域表示特定于页面的内容,该内容可能是 Contact Us (联系我们) 页面上的联系表单和邮政地址,也可能是特定位置提供的休息时间的详细信息。下面的清单显示了如何使用基本的 Bootstrap 样式将图像转换为 HTML。

清单 3.16 包含将要重复内容的网页的基本轮廓

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" 
    ➥ />
</head>
<body>
    <div class="container">
        <header class="card alert-success border-5 p-3 mt-3">Header</header>
        <nav class="card alert-primary border-5 p-3 mt-2">Navigation</nav>
        <div class="row mt-2">
            <div class="col-3">
                <div class="card alert-warning p-5 border-5">
                    Deal Of The Day Widget 1
                </div>
            </div>
            <div class="col-9 card border-5">
                Main Content
            </div>
        </div>
        <footer class="card border-5 p-3 mt-2">Footer</footer>
    </div>
</body>
</html>

如果您创建了多个页面,并且每个页面分别包含这些通用内容,则维护它的负担可能会变得难以忍受。每次向网站添加新页面时,都必须更新所有现有页面中的导航。理想情况下,您希望将这些重复内容集中在一个位置,以便于维护。这种方法被称为 DRY(不要重复自己)。DRY 是软件开发的基本原则之一。

布局页面支持使用 DRY 方法管理常见页面内容。它们充当引用它的所有内容页面的父模板或主模板。在将新页面添加到站点导航时,您已经简要地查看了示例应用程序中的布局页面。布局是扩展名为 .cshtml 的常规 Razor 文件,但使其充当布局的原因是它包含对 RenderBody 方法的调用,其中呈现特定于页面的内容,如下面的列表所示。

列表 3.17 一个 Razor 布局页面,包括对 RenderBody 的调用

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" 
     />
</head>
<body>
    <div class="container">
        <header class="card alert-success border-5 p-3 mt-3">Header</header>
        <nav class="card alert-primary border-5 p-3 mt-2">Navigation</nav>
        <div class="row mt-2">
            <div class="col-3">
                <div class="card alert-warning p-5 border-5">
                    Deal Of The Day Widget 1
                </div>
            </div>
            <div class="col-9 card border-5">
                @RenderBody()                   ❶
            </div>
        </div>
        <footer class="card border-5 p-3 mt-2">Footer</footer>
    </div>
</body>
</html>

❶ 这是将内容页面的输出注入布局的点。

RenderBody 方法调用是布局页面的唯一要求。布局页面中的其他所有内容都包含在引用它的所有人气页面中,这使得管理它变得非常容易。您对布局所做的任何更改都会立即应用于引用该布局的所有页面。

3.2.1 指定 Layout 属性

特定页面的布局是通过页面的 Layout 属性以编程方式分配的。传递给 Layout 属性的值是一个字符串,表示不带扩展名的布局文件的名称或布局文件的路径。以下清单中的任何一种方法都可以使用。

示例 3.18 通过 Layout 属性设置布局页面

@{
    Layout = “_Layout”;
    Layout = “/Pages/Shared/_Layout.cshtml”;
}

使用第一种方法按名称设置布局时,框架会在多个预定义位置搜索具有匹配名称和预配置扩展名(默认为 .cshtml)的文件。首先搜索包含调用页面的文件夹,如果适用,将搜索层次结构中直到根 Pages 文件夹的所有文件夹。最后,再搜索两个位置:\Pages\Shared 和 \Views\Shared。后者是使用 MVC 框架本身构建的应用程序的遗留问题。例如,如果将调用页放在 \Pages\Admin\DestinationsOrders 中,则搜索位置将如下所示:

\Pages\Admin\DestinationsOrders\ _Layout.cshtml
\Pages\Admin\ _Layout.cshtml
\Pages\_Layout.cshtml
\Pages\Shared\ _Layout.cshtml
\Views\Shared\ _Layout.cshtml

如果您想将相同的布局分配给多个页面,那么逐页设置布局并不是一种非常有效的方法,原因与您最初使用布局的原因相同:在多个位置更新它成为一件苦差事。要解决此问题,您可以使用 ViewStart 文件。这是一个名为 _ViewStart.cshtml 的特殊 Razor 文件,您可以在 Pages 文件夹中找到该文件的示例。此文件通常仅包含一个代码块,尽管它也可以包含标记。ViewStart 文件中的代码在它影响的任何页面之前执行,该页面是同一文件夹和所有子文件夹中的任何 Razor 页面。图 3.3 显示了请求传入时的执行顺序。首先是 ViewStart 文件,然后是内容页,然后是布局文件中的任何代码。

图 3.3 Razor 文件的执行顺序:ViewStart,然后是内容页,然后是布局页

ViewStart 在它影响的任何页面之前执行,这使其成为为所有这些页面设置布局的理想方式。如果您查看现有 ViewStart 文件的内容,您会发现这正是它的作用:

@{
    Layout = “_Layout”;
}

我提到过,ViewStart 代码在页面中的代码之前执行,使您能够根据需要逐页更改全局设置。在单个页面中发生的任何布局分配都将覆盖 ViewStart。同样,如果您将其他 ViewStart 文件放在 Pages 文件夹层次结构的较低位置,则其中的布局分配将覆盖层次结构较高位置的 ViewStart 文件中的任何分配。

关于 layouts 的最后一点说明:可以嵌套 layout 文件,因此一个 layout 引用另一个 layout。为此,您需要在子布局中显式分配布局。图 3.4 显示了页面、嵌套(子)布局和主布局之间的关系。Index 内容将注入到放置 RenderBody 的嵌套布局中。合并的内容将注入到布局文件中。

图 3.4 索引页面引用 _ChildLayout 文件,而该文件又引用主 _Layout 文件。

您不能依赖 ViewStart 文件在子布局中设置父布局文件。ViewStart 文件对其文件夹或子文件夹中的布局没有影响。嵌套布局可以启用一些有价值的方案,在这些方案中,您希望为页面子集显示其他内容,例如,您将这些内容应用于子布局。

3.2.2 使用 sections 注入可选内容

在某些情况下,您可能希望某些内容页面能够选择性地提供其他基于 HTML 的内容,以作为布局的一部分进行呈现。图 3.5 显示了具有不同小组件的上一个布局:Things to Do。您可以想象此微件包含有关您当前正在查看的度假地点的感兴趣景点的其他信息 - 例如,如果您选择巴黎作为目的地,则参观埃菲尔铁塔。此小组件包含在布局区域中,该区域对所有页面都是通用的,但只有在选择目标后才会显示,并且其内容将取决于所选目标。

图 3.5 “待办事项” 小组件表示位于布局中的特定于页面的内容。

Razor 包含启用此方案的部分,这些部分使用内容页中的 @section 指令定义。下面的清单显示了一个名为 ThingsToDoWidget 的部分,该部分与一些 HTML 内容一起定义。

列表 3.19 使用 @section 指令定义 Razor 部分

@section ThingsToDoWidget{
    <p>Visit Eiffel Tower</p>
}

通过在希望内容显示的位置调用 RenderSectionAsync 方法,可以在布局页中呈现部分的内容。该方法有两个版本:一个版本将部分的名称作为字符串,另一个版本也采用布尔值,指示所有内容页面是否需要定义部分 (true) 或可选 (false)。在下一个示例中,只有目标页面会为该部分提供内容,因此对于所有其他页面来说,它都是可选的。因此,您将使用用于将 false 传递给第二个参数的重载:

@await RenderSectionAsync(“ThingsToDoWidget”, false)

碰巧的是,已经有一个对 RenderSectionAsync 方法的调用,该方法引用默认项目布局文件中的 scripts 部分,位于结束 body 元素之前:

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>

本部分的目的是在布局中包含特定于页面的 JavaScript 文件,以便它们显示在全局脚本文件之后。这样,特定于页面的文件就可以使用全局文件的内容。

IsSectionDefined 方法可用于布局页,以确定内容页是否已定义特定部分。例如,如果要在调用页面未定义部分时显示一些默认内容,则此方法可能很有用。

列表 3.20 使用 IsSectionDefined 来确定调用页面是否定义了一个部分

<div class="card alert-danger p-5 border-5 mt-2">
@if (IsSectionDefined("ThingsToDoWidget"))
{
    @await RenderSectionAsync("ThingsToDoWidget")
}
else
{
    <p>Things To Do Widget default content</p>
}
</div>

如果内容页定义了一个部分,则必须在布局页中对其进行处理,通常通过调用 RenderSectionAsync 进行处理。但是,当您不想呈现部分的内容时,您可能有条件。在这些情况下,可以使用 IgnoreSection 方法来阻止呈现。

示例 3.21 使用 IgnoreSection 阻止渲染章节内容

@if(!IsAdmin)
{
    IgnoreSection(“admin”);
}
else
{
    @await RenderSectionAsync(“admin”)
}

请注意,IgnoreSection 方法返回 void,因此它不以 @ 符号为前缀,必须以分号结尾。

3.3 带有部分视图、标签帮助程序和视图组件的可重用 HTML

布局是实现可重用 HTML 的一种方式。您可以在一个位置定义网站布局,引用该布局的所有页面都会使用该布局。ASP.NET Core 提供了其他基于 Razor 的机制来处理可重用的 HTML:分部视图、标记帮助程序和视图组件。在本节中,我将介绍所有 3 个功能并解释它们的使用方法。

3.3.1 部分视图

分部视图是一个 Razor (.cshtml) 文件,其中包含一个 HTML 块和一些 Razor 语法(可选)。它与标准 Razor 页面的不同之处在于,分部视图不包含 @page 指令,因为它不打算直接浏览。可以使用分部视图

• 将复杂的 UI 分解为更易于管理的部分
• 避免代码重复
• 在 AJAX 方案中生成用于异步部分页更新的 HTML

奇怪的是,Visual Studio 中没有部分视图模板。您可以使用生成单个 Razor 文件的任何选项。我通常使用 Razor View > Empty 模板,然后删除默认内容。VS Code 用户只需添加带有 .cshtml 后缀的新文件,或使用 CLI 生成新的 ViewStart 或 ViewImports 文件,然后更改文件名并删除默认内容:

dotnet new viewimports
dotnet new viewstart

通常,分部视图在文件名中使用前导下划线命名,例如 _myPartial.cshtml。此约定不是必需的,但它可能有助于区分 partials 和其他文件。您可以将分部视图放置在 Pages 文件夹中的任意位置。局部视图的发现过程与布局相同:包含当前页面和所有父项的文件夹,后跟 Pages\Shared 和 Views\Shared 文件夹。

到目前为止,我们构建的应用程序中的布局文件非常简单,但它可能会变得更加复杂。UI 的单独部分都是分部视图的候选项。以导航为例。此区域的代码可以分离到另一个文件中,然后从布局文件中引用该文件。

若要测试如何创建分部视图,可以从清单 3.16 中的示例布局文件中剪切 nav 元素,并将其粘贴到名为 _NavigationPartial .cshtml 的新 Razor 文件中,应将其放置在 Pages\Shared 文件夹中。现在,您的布局中有一个漏洞,您需要引用分部视图。包含部分视图的推荐机制是 partial 标记帮助程序。我们稍后将更详细地介绍标记帮助程序,但现在,只需知道以下内容将在调用页面中呈现部分视图的内容就足够了:

<partial name=”_NavigationPartial” />

标记帮助程序必须放置在要从要渲染的部分视图中输出的位置。在默认模板中,它位于布局中的 header 元素下。

Listing 3.22 用于包含部分视图内容的 partial 标签助手

<body>
    <div class="container">
        <header class="card alert-success border-5 p-3 mt-3">Header</header>
        <partial name="_NavigationPartial" />
        <div class="row mt-2">
            <div class="col-3">
                <div class="card alert-warning p-5 border-5">
                    Deal Of The Day Widget 1
                </div>

图 3.6 提供了将部分文件 (_NavigationPartial.cshtml) 的内容插入到调用页中放置部分标记帮助程序的位置的过程图示。在此示例中,导航位于标题下方,而不是位于标题内部,就像默认项目模板中一样(图 3.6)。

图 3.6 _NavigationPartial的内容将插入到 Razor 文件中,通过部分标记帮助程序引用它们的位置。

还可以使用分部视图来避免代码重复。标准项目模板在 Pages\Shared 文件夹中包含一个名为 _ValidationScriptsPartial.cshtml 的部分文件。它包含两个 script 元素,这些元素引用用于验证表单值的脚本。在第 5 章中,当你查看表单验证时,你将使用这个部分。

示例中的 partial 和验证脚本 partial 由静态内容组成。Partials 还可以处理动态内容。动态内容的性质是使用 partial 文件顶部的 @model 指令指定的。例如,假设您的导航菜单的数据是由父页面生成的,它由 Dictionary<string、string> 组成,其中键表示要链接到的页面的名称,值可能表示链接的文本。这是 _NavigationPartial.cshtml 文件的第一行的外观:

@model Dictionary<string,string>

数据本身将由引用部分的页面生成,并作为属性包含在其自己的模型中。稍后,当您浏览 PageModel 时,您将看到如何完成此作。目前,您可以假定此部分的数据由名为 Nav 的主机页面的属性表示。您将此数据传递给 partial 标记帮助程序的 model 属性:

<partial name=”_NavigationPartial” model=”Model.Nav” />

或者,您可以使用 for 属性来指定部分的数据。这一次,Model 是隐式的:

<partial name=”_NavigationPartial” for=”Nav” />

在第 11 章中,您将看到如何使用分部视图生成 HTML,以便在 AJAX 方案中用于部分页面更新。

3.3.2 标签辅助函数

标记帮助程序是自动生成 HTML 的组件。它们旨在作用于 Razor 页面的 HTML 中的标记。ASP.NET Core 框架中内置了许多标记帮助程序,其中大多数标记都针对标准 HTML 标记,例如锚点(在将“欢迎”页面添加到导航时,您使用了其中一个标记)、input、link、form 和 image 标记。其他 (如您刚才看到的部分标签帮助程序) 则以您自己创建的自定义标签为目标。每个标记帮助程序都旨在作用于特定标记,并解析服务器端处理期间使用的数据的属性值,以生成生成的 HTML。标记帮助程序解析的大多数属性都是自定义的,以 asp- 开头。提醒一下,以下是您添加到 Welcome (欢迎) 页面的网站导航中的锚点标签帮助程序:

<a class="nav-link text-dark" asp-area="" asp-page="/Welcome">Welcome</a>

asp-area 和 asp-page 属性是自定义属性。他们的角色是向锚标签帮助程序提供有关标签帮助程序用于生成 URL 的区域和页面的信息。在下一章中,当您探索路由和 URL 时,您将了解区域s。当锚点标记帮助程序完成处理并将标记呈现为 HTML 时,生成的 URL 将显示为标准 href 属性。不会呈现自定义属性。

启用标签帮助程序

标记帮助程序是一项可选功能。默认情况下,它们未启用;不过,如果从标准项目模板开始,则它们将通过以下代码行在位于 Pages 文件夹中的 _ViewImports .cshtml 文件中全局启用:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

addTagHelper 指令采用两个参数:要启用的标记帮助程序和包含要启用的标记帮助程序的程序集的名称。通配符 (*) 指定应启用指定程序集中的所有标记帮助程序。框架标记帮助程序位于 Microsoft.AspNetCore.Mvc.TagHelper 中,这就是你会看到默认添加的此程序集的名称的原因。您可以创建自己的自定义标签帮助程序。本书不会介绍这一点,但如果您的自定义标记帮助程序属于 WebApplication1 项目,则可以按如下方式启用它:

@addTagHelper *, WebApplication1

有关构建自己的自定义标签助手的指南,请参阅官方文档 (https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring?view=aspnetcore-7.0) 或 Andrew Lock 的 ASP.NET Core In Action, Second Edition(Manning,2021 年)。

addTagHelper 指令具有孪生体 removeTagHelper,使您能够有选择地选择不处理某些标记。以下代码行选择退出锚点标签帮助程序处理:

@removeTagHelper "Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, 
 Microsoft.AspNetCore.Mvc.TagHelpers"

您可以通过在标签名称前放置 ! 前缀来选择不处理单个标签。例如,如果特定元素仅由客户端代码使用,则可能需要执行此作。这样就没有必要浪费周期在服务器上处理它。以下示例说明了如何将其应用于锚点标签以防止其被不必要地处理:

<!a href="https://www.learnrazorpages.com">Learn Razor Pages</!a>

前缀同时放置在 start 和 end 标记中。任何没有 ! 前缀的标签都将由关联的标签帮助程序处理。另一种方法是在解析时选择特定标签进行处理。为此,您可以向 @tagHelperPrefix 指令注册自定义前缀,然后将您选择的前缀应用于要参与处理的标签。您可以在最初启用标签帮助程序处理的 ViewImports 文件中注册您的前缀:

@tagHelperPrefix x

你几乎可以使用任何你喜欢的字符串作为前缀。然后,将其应用于 start 和 end 标签,就像 ! 前缀一样:

<xa asp-page="/Index">Home</xa>

将仅处理具有前缀的那些标签。为清楚起见,大多数开发人员可能会使用标点符号将前缀与标签名称分开,例如:

@tagHelperPrefix x:
<x:a asp-page="/Index">Home</x:a>

这应该会减少任何视觉上的混淆,特别是对于设计人员在查看 HTML 内容时。

3.3.3 查看组件

视图组件是用于生成可重用 HTML 的更高级的解决方案。它们类似于分部视图,因为它们可用于帮助分解和简化复杂的布局,也可以表示可在多个页面中使用的 UI 部分。每当需要任何类型的服务器端逻辑来获取或处理数据以包含在生成的 HTML 代码段中时(具体而言,就是对外部资源,如文件、数据库或 Web 服务)的调用,建议使用视图组件而不是部分页面。视图组件运行良好的典型场景包括数据库驱动的菜单、标签云和购物车 — 这些窗口小部件通常出现在布局页面中,并依赖于它们自己的数据源。View 组件也适合进行单元测试。

由于视图组件依赖于到目前为止尚未涵盖的高级概念,因此如果该主题有任何意义,则必须等待该主题的进一步讨论。但请放心,您将在第 14 章中构建一个视图组件。

 3.4 PageModel

现在,您已经了解了在 Razor Pages 应用程序中生成 HTML 的主要机制 — Razor 页面和 Razor 语法,该语法通过将 HTML 与服务器端 C# 代码混合来支持动态内容生成。您还了解了一些有助于 UI 代码重用的组件,包括布局、分部视图、标记帮助程序和视图组件。现在,是时候了解 Razor 页面的合作伙伴了:PageModel 类。

在本节中,您将了解 PageModel 的两个主要角色:控制器和视图模型。回想一下第 1 章中关于 MVC 中控制器的讨论。您将记住,它的作用是接收请求,使用 bid 中的信息对模型执行命令,然后获取该处理的结果并将其传递给视图进行显示。图 3.7 显示了该过程的相关部分。

图 3.7 控制器获取输入,作用于模型,并将结果数据传递给视图。

作为请求处理的一部分,控制器必须准备视图的数据,并以视图可以使用的形式将其提供给视图。此表单称为视图模型,是本节第一部分的重点。在此之前,我将讨论 ViewData,它还提供了一种将数据传递到视图页面的方法。

当您向应用程序添加新的 Razor 页面时,将自动生成 PageModel 类。它以页面命名,带有单词 Model,因此欢迎页面的 PageModel 类是 WelcomeModel。PageModel 类派生自 PageModel 类型,该类型具有许多与 PageModel 类本身中的 HTTP 请求相关的属性和方法。公共 PageModel 属性通过包含引用 PageModel 类型的 @model 指令向 Razor 页面公开。

3.4.1 将数据传递到页面

有许多选项可用于将数据传递到页面。推荐的方法是以强类型方式将数据作为视图模型处理。还有另一个选项,虽然它是弱类型,但有时也很有用。在上一章中,您已经使用此方法将 Welcome 页面的标题传递给布局。它称为 ViewData。

ViewData 是一种基于字典的功能。项作为键值对存储在 ViewDataDictionary 中,并通过引用页面中 ViewData 属性的不区分大小写的字符串键进行访问。以下是使用 ViewData 分配 Welcome 页面标题的方法:

ViewData["Title"] = "Welcome!";

在布局页面中访问此值,如下所示:

<title>@ViewData["Title"] - WebApplication1</title>

ViewDataDictionary 中的值是对象类型,这意味着您可以在其中存储任何您喜欢的内容。如果你想使用非字符串类型(例如,调用特定于类型的方法),则需要将它们强制转换为正确的类型。如果你只想渲染值,并且类型的 ToString() 方法会生成适合渲染的值,则这可能不是必需的。以下赋值将 DateTime 类型添加到 ViewData:

ViewData["SaleEnds"] = new DateTime(DateTime.Now.Year, 6, 30, 20, 0, 0);

如果要呈现该值,可以简单地执行此作:

<p>Sale ends at @ViewData[“SaleEnds”]</p>

输出将根据服务器的默认设置进行渲染。在我的例子中,这是英语(英国),这会导致呈现销售结束时间:30/06/2021 20:00:00。如果我想使用日期和时间格式字符串来控制渲染,则需要转换为 DateTime:

Sale Ends at: @(((DateTime)ViewData["SaleEnds"]).ToString("h tt, MMMM dd"))

现在我得到了我想要的输出:销售结束时间:6 月 30 日晚上 8 点。

您可以在 PageModel 类中设置 ViewData 项的值。下一个列表显示了在名为 OnGet 的处理程序方法中分配给 ViewData 的页面的标题;您很快就会了解 Handler 方法。

清单 3.23 在 PageModel 类的 OnGet 中分配 ViewData 值

public class WelcomeModel : PageModel
{
    public void OnGet()
    {
        ViewData["Title"] = "Welcome!";
    }
}

要使此分配在 Razor 页面中生效,必须确保 Razor 页面包含引用 WelcomeModel 类型的 @model 指令:

@model WelcomeModel

您还应该注意,如果在 Razor 页面本身中设置了 PageModel 中所做的分配,则它将被覆盖。

访问 ViewData 值的另一种方法是通过名为 ViewBag 的属性。此属性是 ViewData 的包装器,使您能够将其视为动态对象。因此,您可以通过与键匹配的属性名称来访问项目。由于值是动态的,因此在使用非字符串类型时无需显式强制转换:

@ViewBag.SaleEnds.ToString("h tt, MMMM dd")

ViewBag 仅在 Razor 文件中可用。它在 PageModel 类中不可用,尽管它在 MVC 控制器中可用,但它是从名为 ASP.NET Web Pages 的旧框架继承而来的。ASP.NET Core 团队决定不在 Razor Pages PageModel 类中实现 ViewBag,因为它是动态的,会对使用它的视图和页面产生可衡量的性能影响。因此,根本不建议使用 ViewBag。

ViewData 是弱类型,因此您没有编译时检查或 IntelliSense 支持。它依赖于使用基于字符串的键引用项目,这种方法有时称为使用魔术字符串。因此,使用 ViewData 可能会导致错误,因为很容易在一个位置键入错误或重命名字符串,而忘记更新其他引用。如果您尝试渲染不存在的项,则不会渲染任何内容。如果您尝试对不存在的 ViewData 条目调用方法,将引发 NullReferenceException。如果尝试将 ViewData 条目强制转换为错误的类型,将生成 InvalidCastException。这些异常将在运行时发生。

ViewData 本身应谨慎使用。它可用于将小块简单数据传递到布局页面,例如页面的标题。对于需要将数据从 PageModel 类传递到 Razor 页面的所有其他方案,应使用公共 PageModel 属性,我们接下来将介绍这些属性。

3.4.2 将 PageModel 作为视图模型

如果有一件事似乎一直让不熟悉任何形式的 MVC 和 ASP.NET 的开发人员感到困惑,那就是视图模型的概念 — 它是什么、它的用途以及如何创建一个视图模型。从本质上讲,视图模型是一件非常简单的事情。它是一个封装特定视图或页面所需数据的类。从概念上讲,它执行与静态数据库视图相同的功能。视图模型包含来自一个或多个对象的数据子集。

例如,考虑网站上的订单摘要页面。它通常包含与您要订购的产品或服务相关的详细信息的子集,可能只是其标识符、名称和价格。它还可能包括您的姓名、账单地址和一个复选框,以指示您也希望向该地址发货。这些信息将来自网站数据库中的 Products 表和 Customers 表。但是,订单摘要视图不需要这些数据库表中的其余信息。例如,订单摘要页面不会显示客户的密码或他们创建账户的日期。它也不会显示可能存储在 Products 表中的产品的内部详细信息,例如供应商的详细信息或再订购级别。

图 3.8 显示了视图所需的 Products 和 Customers 表所包含的总信息中有多少。此数据子集构成了 OrderSummaryViewModel 的基础,OrderSummaryViewModel 是订单摘要视图的模型,可能类似于清单 3.24 的内容。

图 3.8 订单摘要视图只需要分级显示的列或属性

示例 3.24 视图模型类

public class OrderSummaryViewModel
{
    public int CustomerId { get; set; }
    public string CustomerName { get; set; }
    public string BillingAddress { get; set; }
    public bool UseBillingForShipping { get; set; } 
    public int ProductId
    public string Name { get; set; }
    public decimal Price { get; set; }
}

这就是:视图模型 — 一个仅包含视图所需数据的容器。使用 MVC 框架的开发人员广泛使用视图模型。它们的视图通常包含一个 @model 指令,该指令引用为特定视图设计的 ViewModel 类:

@model OrderSummaryViewModel
你已在 Razor 页面文件中看到 @model 指令。它引用 Razor 页面附带的 PageModel 类。因此,Razor Pages 中的 PageModel 是 MVC 意义上的视图模型。添加到 PageModel 的任何公共属性都将可供引用 PageModel 的 Razor 页面访问,该页面通过页面内的特殊 Model 属性。

清单 3.25 显示了 WelcomeModel,这是您在上一章中创建的 Welcome 页面的 PageModel 类。此版本具有一个名为 SaleEnds 的公共属性,它等效于您之前看到的 ViewData 值。

清单 3.25 PageModel 中的公共属性

public class WelcomeModel : PageModel
{
    public DateTime SaleEnds { get; set; } = new DateTime(DateTime.Now.Year, 
     6, 30);
    public void OnGet()
    {
    }
}

列表 3.26 显示了 Welcome Razor 页面,其中包括引用 WelcomeModel 类型的 @model 指令和通过页面的 Model 属性访问的 SaleEnds 值。

列表 3.26 通过页面的 Model 属性公开的 PageModel 属性

@page
@model WebApplication1.Pages.WelcomeModel                          ❶
@{    
}
<p>Sale ends at @Model.SaleEnds.ToString("h tt, MMMM dd")</p>      ❷

❶ PageModel 类型由 @model 指令引用。
❷ PageModel 的 SaleEnds 属性可通过 Razor 页面的特殊 Model 属性进行访问。

与弱类型的 ViewData 不同,您可以获得对 PageModel 属性的 IntelliSense 支持,如图 3.9 所示,它显示了 IntelliSense 在 VS Code 中发挥作用,以帮助完成代码。

图 3.9 IntelliSense 支持 PageModel 属性。

这是建议使用 PageModel 属性而不是 ViewData 作为将数据传递到页面的机制的主要原因。Visual Studio 和 VS Code 中的工具支持可以提高您的工作效率,并最大限度地减少错误潜入代码的可能性。

3.4.3 作为控制器的 PageModel

控制器的主要作用是处理请求。PageModel 中的请求处理在处理程序方法中执行。对于熟悉支持它们的 MVC 框架的读者来说,PageModel 处理程序方法类似于控制器作。按照约定,处理程序方法的选择基于使用 On[method] 模式将用于请求的 HTTP 方法的名称与处理程序方法的名称进行匹配,并选择性地附加了 Async,以表示该方法旨在异步运行。为 GET 请求选择 OnGet 或 OnGetAsync 方法,为 POST 请求选择 OnPost 或 OnPostAsync 方法。

Listing 3.27 展示了 WelcomeModel 类,其中添加了一个简单的字符串属性 Message,以及 OnGet 和 OnPost 处理程序方法。每个处理程序都设置 Message 属性的值,以报告执行了哪个处理程序。

列表 3.27 不同的处理程序方法,为 Message 属性分配不同的值

public class WelcomeModel : PageModel
{
    public string Message { get; set; }    ❶

    public void OnGet()
    {
        Message = "OnGet executed";        ❷
    }

    public void OnPost()
    {
        Message = "OnPost executed";       ❸
    }
}

❶ 添加了 public 属性,该属性可通过 Model 属性在 Razor 页面中访问。
❷ 如果执行 OnGet 处理程序,则消息将包含“OnGet”。
❸ 如果执行 OnPost 处理程序,则消息将包含 “OnPost”。

清单 3.28 显示了 Welcome Razor 页面。该页面包括一个锚点标签帮助程序和一个 method 属性设置为 post 的表单。单击锚点标签帮助程序生成的链接将导致 GET 请求,提交表单将导致 POST 请求。在相应处理程序方法中设置的 Message 属性值将呈现到输出中。图 3.10 说明了基于请求页面的方法的不同输出。

图 3.10 通过将处理程序名称与 HTTP 方法匹配来选择处理程序方法。

列表 3.28 欢迎页面,包括生成 GET 和 POST 的机制

@page
@model WebApplication1.Pages.WelcomeModel                ❶
@{
}
<p>@Model.Message</p>                                    ❷
<a asp-page="/Welcome">Get</a>                           ❸
<form method="post"><button>Post</button></form>         ❹

❶ WelcomeModel 通过 @model 指令引用。
❷ WelcomeModel 的 Message 属性可通过页面的 Model 属性进行访问。
❸ 锚点标签帮助程序会导致 GET 请求。
❹ 提交表单会导致 POST 请求。

处理程序方法参数

处理程序方法可以采用参数。在 URL 中传递的数据将根据参数名称与与 URL 数据项关联的名称之间的匹配绑定到处理程序方法参数。要了解其工作原理,请查看下一个清单,其中 OnGet 方法已更改为接受名为 id 的参数,并且 Message 属性包含绑定到 id 参数的值。

示例 3.29 向 OnGet 处理程序方法添加参数

public void OnGet(int id)                         ❶
{
    Message = $"OnGet executed with id = {id}";   ❷
}

❶ 名为 id 的数值参数已添加到 OnGet 方法中。
❷ 参数的值将合并到分配给 Message 属性的值中。

Razor 页面中的 anchor 标记已更新为包含值为 5 的 asp-route-id 属性,如列表 3.30 所示。asp-route-* 属性用于在 URL 中传递数据。默认情况下,数据在查询字符串中传递,查询字符串 item 的名称取自星号表示的属性部分。

列表 3.30 使用 anchor 标签辅助程序上的 asp-route-* 属性传递一个值

<a asp-page="/Welcome" asp-route-id="5">Get</a>

首次运行页面时没有查询字符串值,因此 handler 参数设置为其默认值 0。为锚点标记的 href 属性生成的值为 /Welcome?id=5。当您点击该链接时,消息将更新为包含参数值,如图 3.11 所示。

图 3.11 查询字符串值根据匹配名称绑定到参数。

负责将传入数据与参数匹配的魔术称为模型绑定。在第 5 章中,当我们看到使用表单时,我将详细介绍模型绑定。

命名处理程序方法

C# 允许您通过改变方法接受的参数的数量和类型来创建方法的重载。虽然可以在单个 PageModel 类中创建多个版本的OnGet或OnPost方法,这些方法因参数而异,并使其成功编译,但 Razor Pages 框架不允许这样做。PageModel 中只能有一个 OnGet 或一个 OnPost 方法。事实上,你甚至不能在同一个 PageModel 类中拥有 OnGet 和 OnGetAsync 方法。当 Razor Pages 将处理程序与 HTTP 方法名称匹配时,它会忽略 Async 后缀。如果多个处理程序与给定请求匹配,您将在运行时收到 AmbiguousActionException。

有时您可能希望为同一 HTTP 方法执行不同的代码。例如,您可能有一个包含多个表单的页面。Manning 主页同时提供搜索表单和注册表单。一个采用搜索词,另一个采用电子邮件地址。假设他们都发回到同一个页面,您如何知道用户提交了哪一个页面?您可以在 OnPost 处理程序中添加一些逻辑,以尝试识别用户是尝试注册新闻稿还是通过其电子邮件地址搜索作者,也可以使用命名处理程序方法。

命名处理程序方法的开头约定与标准处理程序方法相同:On 后跟 HTTP 方法名称。这后跟处理程序方法的名称,用于消除它与标准处理程序方法和其他命名处理程序方法的歧义。例如,您可能希望创建一个名为 OnPostSearch 的方法来处理搜索表单提交,并创建另一个名为 OnPost- Register 的方法来处理注册表单提交。下面的清单显示了如何在示例应用程序的 WelcomeModel 中实现这些。

清单 3.31 显示两个命名的处理程序方法

public class WelcomeModel : PageModel
{
    public string Message { get; set; }                         ❶
    public void OnPostSearch(string searchTerm)                 ❷
    {
        Message = $"You searched for {searchTerm}";             ❸
    }

    public void OnPostRegister(string email)                    ❹
    {
        Message = $"You registered {email} for newsletters";    ❺
    }
}

❶ 添加了 Message 属性。
❷ OnPostSearch 方法采用名为 searchTerm 的字符串。
❸ 该消息包含 searchTerm 参数值。
❹ OnPostRegister 方法使用名为 email 的参数处理注册。
❺ 消息包含 email 参数值。

创建了两个处理程序方法:一个名为 OnPostSearch,另一个名为 OnPostRegister。Search 和 Register 表示命名处理程序方法的名称部分。这两种方法都采用字符串参数,但它们都根据调用的方法将 Message 属性设置为不同的值。

清单 3.32 显示了 Index 页面中的两个简单表单 — 一个用于搜索,另一个用于注册。表单标签是标签帮助程序所针对的标签之一,因此可以使用自定义属性。page-handler 属性接受页面处理程序方法的名称,该方法用于在提交表单时处理请求。

清单 3.32 page-handler 属性支持针对不同的处理程序方法

@page
@model WelcomeModel
@{
}

<div class="col">
    <form method="post" asp-page-handler="Search">     ❶
        <p>Search</p>
        <input name="searchTerm" />                    ❷
        <button>Search</button>
    </form>

    <form method="post" asp-page-handler="Register">  
        <p>Register</p>
        <input name="email" />     
        <button>Register</button>    
    </form>
    <p>@Model.Message</p>                              ❸
</div>

❶ 处理程序方法的 name 部分被分配给 form 标记帮助程序的 page-handler 属性。
❷ input name 属性与目标处理程序方法上的参数名称匹配。
❸ Message 的值呈现给浏览器。

图 3.12 显示了呈现页面并且用户搜索 Razor Pages 时会发生什么情况。表单标签帮助程序使用键处理程序将处理程序的名称附加到查询字符串中:

?handler=Search

图 3.12作中的命名处理程序方法选择

根据处理程序查询字符串值与处理程序方法名称之间的成功匹配,Razor Pages 选择了 OnPostSearch 处理程序来处理请求,并相应地生成结果输出。

处理程序方法返回类型

到目前为止,您看到的所有处理程序方法示例的 return 类型都是 void。其他支持的返回类型包括 Task 和实现 IActionResult 接口的任何类型(称为作结果),它们具有生成响应的作用。对于不同类型的响应,可以使用各种作结果。例如,您可能希望返回一个文件,而不是呈现 Razor 页面。或者,您可能希望返回具有特定 HTTP 状态代码的空响应。或者,您可能希望将用户重定向到其他位置。

此时,您可能想知道,当您的处理程序方法未返回 IActionResult 类型时,您的页面是如何生成响应的。这是因为返回类型为 void 或 Task 的处理程序方法隐式返回 PageResult,即呈现关联的 Razor 页面的作结果。下面的清单显示了 OnPostSearch 处理程序,该处理程序已更新为包含显式返回类型 PageResult。

Listing 3.33 显式返回 action 结果的处理程序方法

public PageResult OnPostSearch(string searchTerm)    ❶
{
    Message = $"You searched for {searchTerm}";
    return new PageResult();                         ❷
}

❶ 已更改 OnPostSearch 方法以返回 PageResult。
❷ 创建并返回一个新的 PageResult 实例。

PageModel 类包含许多帮助程序方法,这些方法提供了一种创建作结果的简写方法,从而避免使用 new 运算符。下一个清单显示了 Page() 帮助程序方法的用法,它是表达式 new PageResult() 的包装器。

列表 3.34 Page() 方法中new PageResult()的简写

public PageResult OnPostSearch(string searchTerm)   ❶
{
    Message = $"You searched for {searchTerm}";
    return Page();                                  ❷
}

❶ 已更改 OnPostSearch 方法以返回 PageResult。
❷ Page() 方法充当对 new PageResult() 的调用的包装器。

通常,如果您的处理程序只导致当前页面被处理和呈现,则无需显式返回 PageResult。表 3.1 显示了您最有可能在 Razor Pages 应用程序中使用的作结果类型及其帮助程序方法。

表 3.1 Razor Pages 中的action结果

Action result

class name

Helper method

Description

PageResult

Page

Renders the current Razor page.

FileContentResult

File

Returns a file from a byte array, stream, or virtual path.

NotFoundResult

NotFound

Returns an HTTP 404 status code indicating that the resource was not found.

PartialResult

Partial

Renders a partial view or page.

RedirectToPageResult

RedirectToPage,

RedirectToPagePermanent

Redirects the user to the specified page. The RedirectToPage method returns an HTTP 302 status code, indicating that the redirect is temporary.

StatusCodeResult

StatusCode

Returns an HTTP response with the specified status code.

在指定处理程序方法的返回类型时,应尽可能具体。有时,您需要根据应用程序逻辑返回两个或多个作结果类型之一。例如,您可以使用参数值在数据库中查找某个条目,如果该条目不存在,则需要返回 NotFoundResult。否则,您将返回 PageResult。在这种情况下,您应该指定 IActionResult 作为处理程序方法的返回类型。

示例 3.35 恢复 IActionResult 以表示任何 ActionResult 类型

public IActionResult OnGet(int id)    ❶
{
    var data = database.Find(id);     ❷
    if (data == null)
    {
        return NotFound();            ❸
    }
    else
    {
        return Page();                ❹
    }
}

❶ 该方法返回 IActionResult。
❷ 尝试查找与参数值匹配的数据。
❸ 如果数据库未返回匹配数据,则使用 NotFound 帮助程序方法创建 NotFoundActionResult。
❹ 如果获取了数据,则呈现页面。

在本章的开头,我提到了 @page 指令表示一个 Razor 文件,该文件表示一个旨在浏览的可导航页面。在下一章中,我们将介绍称为路由的过程,该过程负责确保将 URL 映射到包含此指令的 Razor 文件。

总结

Razor 是一种模板语法,可用于在 HTML 标记中嵌入服务器端代码。
Razor 语法放置在扩展名为 .cshtml 的 Razor 文件中。
C# 代码块括在 @{ ... } 中。
当变量和表达式以 @ 为前缀时,它们将呈现到输出中。
可以通过在 Razor 文件的标记部分前加上 @ 来嵌入控制块。
出于安全原因,Razor HTML 会对其呈现的所有输出进行编码。
您可以使用 Html.Raw 禁用 HTML 编码。
布局页面用于消除跨多个页面的公共内容重复。
标签帮助程序以特定标签为目标并自动生成 HTML。
PageModel 是一个组合的视图模型和控制器。
@model 指令使 PageModel 可用于 Razor 页面。
Razor 页面的 Model 属性提供对 PageModel 属性的访问。
PageModel 中的处理程序方法负责处理请求。它们以特定的 HTTP 方法为目标,并且可以返回 void、Task 或 IActionResult。
处理程序方法参数可以从具有相同名称的查询字符串参数中获取其值。
命名处理程序方法允许您为同一 HTTP 方法指定多个处理程序。

Leave a Reply

Your email address will not be published. Required fields are marked *