Razor Pages 是 ASP.NET Core 2.0 中引入的 ASP.NET Core MVC 的一个新方面。它提供了一种“基于页面”的方法,用于在 ASP.NET Core 中构建服务器端呈现的应用程序,并且可以与“传统”MVC 或 Web API 控制器共存。在这篇文章中,我将介绍 Razor Pages、入门基础知识以及 Razor Pages 与 MVC 的不同之处。
剃刀页面与 MVC
如果您使用 ASP.NET Core 构建服务器端呈现的应用程序,那么您将熟悉传统的模型-视图-控制器 (MVC) 模式。Razor Pages 在 MVC 之上提供了一个抽象,这可以使它更适合一些基于页面的应用程序。
在 MVC 中,控制器用于将相似的操作组合在一起。收到请求时,路由会将请求定向到单个操作方法。此方法通常执行一些处理,并返回一个IActionResult
,通常是 aViewResult
或 a RedirectResult
。如果 a返回,则使用提供的视图模型呈现ViewResult
Razor视图。
MVC 提供了很大的灵活性,因此将操作分组到控制器中可以是高度自由的,但您通常会以某种方式对相关的操作进行分组,例如通过 URL 路由或按功能。例如,您可以按域组件进行分组,以便在电子商务应用程序中,与“产品”相关的操作将位于 中ProductController
,而“购物车”操作将位于CartController
. 或者,可以根据技术方面对动作进行分组;例如,控制器上的所有操作共享一组通用的授权要求。
您会发现一个常见的模式是在控制器中包含成对的相关操作。在您使用 HTML 表单时尤其如此,您通常需要一个操作来处理初始GET
请求,而另一个操作来处POST
理请求。这两个操作都使用相同的 URL 路由和相同的 Razor 视图。从用户(或开发人员)的角度来看,它们在逻辑上是同一“页面”的两个方面。
在某些情况下,您可能会发现您的控制器充满了这些操作方法对。例如,AccountController
MVC 应用程序的默认 ASP.NET Core 标识包含许多这样的对:
GET 和 POST 对动作高度耦合,因为它们都返回相同的视图模型,可能需要类似的初始化逻辑,并使用相同的 Razor 视图。这对操作也与它们所在的整体控制器相关(它们都与身份和帐户相关),但它们之间的关系更密切。
Razor Pages 提供与传统 MVC 大致相同的功能,但通过利用这种配对使用稍微不同的模型。每条路由(每对动作)都成为一个单独的 Razor 页面,而不是将许多相似的动作组合在一个控制器下。该页面可以有多个处理程序,每个处理程序都响应不同的 HTTP 动词,但使用相同的视图。因此,上面的相同身份AccountController
可以在 Razor 页面中重写,如下所示。事实上,从 ASP.NET Core 2.1 开始,新的项目模板使用 Razor Pages for Identity,即使在 MVC 应用程序中也是如此。
Razor Pages 具有高度凝聚力的优势。与应用程序中给定页面相关的所有内容都在一个地方。与 MVC 控制器相比,其中一些动作高度相关,但控制器作为一个整体的凝聚力较低。
使用 Razor 页面的另一个好指标是,当您的 MVC 控制器仅返回 Razor 视图而无需进行大量处理时。一个经典的例子是HomeController
来自 ASP.NET Core 2.0 模板,其中包括四个操作:
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
这些动作并不是真正相关的,但是每个动作都需要一个控制器,并且HomeController
放置它们是一个比较方便的位置。Razor Pages 等效项将Index
(Home) About
、、、Contact
和Error
页面放在根目录中,删除它们之间的隐式链接。作为额外的奖励,与About
页面相关的所有内容(例如)都可以在文件About.cshtml
和中找到About.cshtml.cs
,它们一起位于磁盘和解决方案资源管理器中。这与控制器、视图模型和视图文件通常位于完全不同的文件夹中的 MVC 方法形成对比。
但是,这两种方法在功能上是相同的,那么您应该选择哪一种呢?
什么时候应该使用 Razor Pages?
重要的是要意识到您不必全力以赴使用 Razor Pages。Razor Pages 使用与传统 MVC 完全相同的基础架构,因此您可以将 Razor Pages 与 MVC 和 Web API 控制器混合在同一个应用程序中。Razor Pages 还使用与传统 MVC 相同的 ASP.NET Core 原语,因此您仍然可以获得模型绑定、验证和操作结果。
从可维护性的角度来看,我发现 Razor Pages 提供的额外凝聚力使其更适合新开发。不必在控制器、视图模型和视图文件之间来回跳转,令人耳目一新!
话虽如此,在某些情况下,最好还是坚持使用传统的 MVC 控制器:
- 当你的控制器上有很多MVC 动作过滤器时。您也可以在 Razor Pages 中使用过滤器,但它们通常提供的细粒度控制不如传统 MVC。例如,您不能将 Razor 页面过滤器应用于 Razor 页面中的单个处理程序(例如,
GET
vsPOST
处理程序)。 - 当您的 MVC 控制器不呈现视图时。Razor 页面专注于“页面”模型,您在其中为用户呈现视图。如果你的控制器要么是 Web API 控制器,要么不是为提供用户导航的页面而设计的,那么 Razor 页面就没有任何意义。
- 当您的控制器已经高度内聚时,将动作方法集中在一个文件中是有意义的。
另一方面,在某些情况下 Razor Pages 真的很出色:
- 当您的操作方法几乎没有逻辑并且只是返回视图时(例如,
HomeController
前面显示的)。 - 当您的 HTML 表单具有成对的
GET
和POST
动作时。Razor Pages 使每一对都成为一个有凝聚力的页面,我发现在开发时需要更少的认知开销,而不必在多个文件之间跳转。 - 当您以前使用ASP.NET 网页 (WebMatrix)时。该框架提供了一个基于页面的轻量级模型,但它与 ASP.NET 完全分离。相比之下,Razor Pages 具有类似级别的简单性,但在需要时还具有 ASP.NET Core 的全部功能。
现在您已经看到了 MVC 和 Razor Pages 之间的高级差异,是时候深入了解细节了。如何创建 Razor 页面,它与传统的 Razor 视图有何不同?
剃刀页面模型
Razor Pages 构建在 MVC 之上,但它们使用的范式与 MVC 模式略有不同。使用 MVC,控制器通常为操作提供逻辑和行为,最终生成包含用于呈现视图的数据的视图模型。Razor Pages 采用了稍微不同的方法,即使用Page Model。
与 MVC 相比,页面模型既充当微型控制器,又充当视图的视图模型。它负责页面的行为和公开用于生成视图的数据。这种模式更接近于某些桌面和移动框架中使用的模型-视图-视图模型 (MVVM)模式,尤其是当业务逻辑被推出页面模型并进入您的“业务”模型时。
从技术上讲,Razor 页面与 Razor 视图非常相似,只是它@page
在文件顶部有一个指令:
@page
<div>The time is @DateTime.Now</div>
与 Razor 视图一样,Razor 页面中的任何 HTML 都会呈现给客户端,您可以使用该@
符号呈现 C# 值或使用 C# 控制结构。有关 Razor 语法的完整参考指南,请参阅文档。
添加@page
是公开页面所需的全部内容,但此页面尚未使用页面模型。更典型的是,您创建一个派生自文件PageModel
并将其与cshtml
文件关联的类。如果您愿意,您可以将您的PageModel
视图和 Razor 视图包含在同一个cshtml
文件中,但最佳做法是将其保存PageModel
在“代码隐藏”文件中,并且仅在cshtml
文件中包含演示数据。按照惯例,如果您的剃须刀页面被调用MyPage.cshtml
,则代码隐藏文件应命名为MyPage.cshtml.cs
:
该类PageModel
是 Razor 视图的页面模型。呈现 Razor 页面时,在视图上公开的属性在视图PageModel
中可用。.cshtml
例如,您可以在Index.cshtml.cs
文件中公开当前时间的属性:
using System;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel: PageModel
{
public DateTime CurrentTime => DateTime.UtcNow;
}
Index.cshtml
并使用标准 Razor 语法将其呈现在您的文件中:
@page
@model IndexModel
<div>The current time is @Model.CurrentTime.ToShortTimeString()</div>
如果你熟悉 Razor 视图,这应该很熟悉。您使用在属性@model
上公开的指令声明模型的类型。Model
不同之处在于,您的 MVC 控制器不是传入类型为 View Model 的 View Model,而是将IndexModel
其PageModel
本身作为Model
属性公开。
Razor 页面中的路由
Razor Pages 与 MVC 类似,混合使用约定、配置和声明性指令来控制应用程序的行为方式。它们在底层使用与 MVC 相同的路由基础设施;不同之处在于路由的配置方式。
- 对于 MVC 和 Web API,您可以使用属性或基于约定的路由将传入 URL 与控制器和操作相匹配。
- 对于 Razor 页面,磁盘上文件的路径用于计算可以访问页面的 URL。按照惯例,所有 Razor 页面都嵌套在
Pages
目录中。
例如,如果您在应用程序中创建一个 Razor 页面/Pages/MyFolder/Test.cshtml
,它将在 URL 处公开/MyFolder/Test
。这对于使用 Razor 页面来说绝对是一个积极的特性——导航和可视化应用程序公开的 URL 就像查看文件结构一样简单。
话虽如此,Razor Page 路由是完全可定制的;如果您需要在与磁盘上的路径不对应的路由上公开您的页面,只需在指令中提供一个路由模板。@page
这也可以包括其他路由参数,如下所示:
@page "/customroute/customized/{id?}"
该页面将在 URL 处公开/customroute/customized
,并且它还可以id
在 URL 中绑定可选段,/customroute/customized/123
例如。id
可以使用模型绑定将值绑定到PageModel
属性。
Razor Pages 中的模型绑定
在 MVC 中,控制器中的操作的方法参数通过匹配 URL、查询字符串或请求正文中的值来绑定到传入请求(有关详细信息,请参阅文档)。在 Razor Pages 中,传入的请求被绑定到的属性PageModel
。
出于安全原因,您必须通过装饰属性以与属性绑定来明确选择要绑定的[BindProperty]
属性:
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
[BindProperty]
public string Search { get;set; }
public DateTime CurrentTime { get; set; };
}
在此示例中,Search
属性将绑定到请求,因为它用 装饰[BindProperty]
,但CurrentTime
不会被绑定。对于 GET 请求,您必须更进一步并在SupportsGet
属性上设置属性:
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
[BindProperty(SupportsGet = true)]
public string Search { get;set; }
}
如果您要绑定复杂的模型,例如回发表单,那么在[BindProperty]
任何地方添加属性都会变得乏味。相反,我喜欢创建一个属性作为“输入模型”并用[BindProperty]
. 这使您的PageModel
公共表面区域保持明确和可控。这种方法的一个常见扩展是使您的输入模型成为一个嵌套类。这通常是有道理的,因为您通常不想在应用程序的其他地方使用您的 UI 层模型:
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
public bool IsEmailConfirmed { get; set; }
[BindProperty]
public InputModel Input { get; set; }
public class InputModel
{
[Required, EmailAddress]
public string Email { get; set; }
[Required, Phone, Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
}
在此示例中,只有属性Input
绑定到传入请求。这使用嵌套InputModel
类来定义要绑定的所有预期值。如果需要绑定其他值,可以添加其他属性到InputModel
.
使用 Razor 页面处理程序处理多个 HTTP 动词
Razor Pages 的主要卖点之一是它们可以使用 MVC 控制器带来额外的凝聚力。通过使用页面处理程序响应请求,单个 Razor 页面包含与给定 Razor 视图关联的所有 UI 代码。
页面处理程序类似于 MVC 中的操作方法。当 Razor 页面收到请求时,会根据传入的请求和处理程序名称选择运行单个处理程序。处理程序通过命名约定匹配On{Verb}[Async]
,其中{Verb}
是 HTTP 方法,并且[async]
是可选的。例如:
OnPost
并OnPostAsync
响应POST
请求运行OnGet
并OnGetAsync
响应GET
请求(以及可选HEAD的 ASP.NET Core 2.1+ 中的请求)运行。
OnGet
对于 HTML 表单,有一个显示初始空表单的处理程序和一个OnPost
处理来自客户端的返回的处理程序是很常见POST
的。例如,以下表单显示了更新用户显示名称的 Razor 页面的代码隐藏。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
public class UpdateDisplayNameModel : PageModel
{
private readonly IUserService _userService;
public IndexModel(IUserService userService)
{
_userService = userService;
}
[BindProperty]
public InputModel Input { get; set; }
public void OnGet()
{
Input.DisplayName = _userService.GetDefaultDisplayName();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
await _userService.UpdateDisplayName(User, Input.DisplayName);
return RedirectToPage("/Index");
}
public class InputModel
{
[Required, StringLength(50)]
public string DisplayName { get; set; }
}
}
这个 Razor Page 使用了一个虚构IUserService
的注入PageModel
构造函数。页面处理程序在OnGet
最初请求表单时运行,并将默认显示名称设置为从IUserService
. 表格被发送给客户,客户填写详细信息并将其发回。
OnPostAsync
处理程序响应 , 运行,POST
并遵循与 MVC 操作方法类似的模式。您应该首先使用 来检查模型验证PageModel
是否通过,如果没有则使用 重新显示表单。在语义上等价于 MVC 控制器中的方法;它用于渲染视图并返回响应。ModelState.IsValid
Page()
Page()
View()
如果PageModel
有效,则表单使用提供的DisplayName
值来更新当前登录用户的名称,User
。PageModel
提供对许多与基类相同的属性的访问,Controller
例如HttpContext
, Request
,在本例中为User
. RedirectToPage()
最后,处理程序使用该方法重定向到另一个 Razor 页面。这在功能上等同于 MVCRedirectToAction()
方法。
当表单只有一个可能的角色时,OnGet
和处理程序对很常见,但也可以有一个 Razor Page 和多个处理程序用于同一个动词。要创建命名处理程序,请使用命名约定。例如,也许我们想向Razor 页面添加一个处理程序,允许用户将其用户名重置为默认值。我们可以将以下处理程序添加到现有的 Razor 页面:OnPost
On{Verb}{Handler}[Async]
UpdateDisplayName
ResetName
public async Task<IActionResult> OnPostResetNameAsync()
{
await _userService.ResetDisplayName(User);
return RedirectToPage("/Index");
}
要调用处理程序,请在 的查询字符串中传递处理程序名称POST
,例如?handler=resetName
。这确保调用命名处理程序而不是默认OnPostAsync
处理程序。如果您不喜欢在此处使用查询字符串,则可以使用自定义路由并将处理程序名称包含在路径段中。
本节展示了 和 的处理程序GET
,POST
但也可以为其他 HTTP 动词(如DELETE
、PUT
和PATCH
. HTML 表单通常不使用这些动词,因此在面向页面的 Razor Pages 应用程序中通常不需要。但是,如果您出于某种原因需要 API 调用它们,它们遵循与其他页面处理程序相同的命名约定和行为。
在 Razor 页面中使用标签助手
当您使用 MVC 操作和视图时,ASP.NET Core 提供了各种标记帮助asp-action
器,例如asp-controller
用于从 Razor 视图生成指向您的操作的链接。Razor 页面具有等效的标记帮助程序,您可以使用其中asp-page
生成指向特定 Razor 页面的路径,并asp-page-handler
设置特定的处理程序。
例如,您可以使用asp-page
和asp-page-handler
标签在表单中创建一个“重置名称”按钮:
<button asp-page="/Index" asp-page-handler="ResetName" type="submit">Reset Display Name</button>
概括
Razor 页面是 ASP.NET Core MVC 的一个新方面,在 ASP.NET Core 2.0 中引入。它们构建在现有 ASP.NET Core 基元之上,并提供与传统 MVC 相同的整体功能,但具有基于页面的模型。对于许多应用程序,使用 a 的基于页面的方法PageModel
可以产生比传统 MVC 更具凝聚力的代码。Razor Pages 可以与传统的 MVC 或 Web API 控制器在同一应用程序中无缝使用,因此您只需要在合适的地方使用它。
如果您使用 Razor 创建新应用程序,我强烈建议您将 Razor Pages 视为默认方法。一开始对于有经验的 MVC 开发人员可能会觉得奇怪,但我对改进的开发体验感到惊喜。对于现有的 MVC 应用程序,添加新的 Razor 页面很容易,但不太值得迁移整个 MVC 应用程序来使用它们。它们在功能上与 MVC 相同,因此主要优点是更方便常见开发任务的工作流。
其他资源
Learn Razor Pages是Microsoft MVP Mike Brind的教程网站设置。除了官方文档之外,我强烈推荐这是一个很好的资源。
Andrew Lock 是 Microsoft MVP 和Manning 的 ASP.NET Core in Action 的作者。可以在 Twitter 上@andrewlocknet或通过他的博客https://andrewlock.net联系到他。