ASP.NET Core 中的 Razor Pages 介绍

作者:Rick AndersonDave BrockKirk Larkin

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 ASP.NET Core 8.0 版本

通过 Razor Pages 对基于页面的场景编码比使用控制器和视图更轻松、更高效。

若要查找使用模型视图控制器方法的教程,请参阅 ASP.NET Core MVC 入门

本文档介绍 Razor Pages。 它并不是分步教程。 如果认为某些部分过于复杂,请参阅 Razor Pages 入门。 有关 ASP.NET Core 的概述,请参阅 ASP.NET Core 简介

先决条件

创建 Razor Pages 项目

请参阅 Razor Pages 入门,获取关于如何创建 Razor Pages 项目的详细说明。

Razor Pages

Program.cs 中已启用 Razor Pages:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

在上述代码中:

请考虑一个基本页面:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

前面的代码与具有控制器和视图的 ASP.NET Core 应用中使用的 Razor 视图文件非常相似。 不同之处在于 @page 指令。 @page 将文件转换为 MVC 操作,这意味着它可以直接处理请求,而无需经过控制器。 @page 必须是页面上的第一个 Razor 指令。 @page 会影响其他 Razor 构造的行为。 Razor Pages 文件名有 .cshtml 后缀。

将在以下两个文件中显示使用 PageModel 类的类似页面。 Pages/Index2.cshtml 文件:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Pages/Index2.cshtml.cs 页面模型:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

按照惯例,PageModel 类文件的名称与追加 .cs 的 Razor Page 文件名称相同。 例如,前面的 Razor Page 的名称为 Pages/Index2.cshtml。 包含 PageModel 类的文件的名称为 Pages/Index2.cshtml.cs

页面的 URL 路径的关联由页面在文件系统中的位置决定。 下表显示了 Razor Page 路径及匹配的 URL:

文件名和路径 匹配的 URL
/Pages/Index.cshtml //Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store/Store/Index

注意:

  • 默认情况下,运行时在“Pages”文件夹中查找 Razor Pages 文件。
  • URL 未包含页面时,Index 为默认页面。

编写基本窗体

由于 Razor Pages 的设计,在构建应用时可轻松实施用于 Web 浏览器的常用模式。 模型绑定标记帮助程序和 HTML 帮助程序可使用 Razor Page 类中定义的属性。 请参考为 Contact 模型实现的基本的“联系我们”窗体页面:

在本文档中的示例中,DbContextProgram.cs 文件中进行初始化。

内存中数据库需要 Microsoft.EntityFrameworkCore.InMemory NuGet 包。

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

数据模型:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

数据库上下文:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext (DbContextOptions<CustomerDbContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}

Pages/Customers/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

Pages/Customers/Create.cshtml.cs 页面模型:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

按照惯例,PageModel 类命名为 <PageName>Model并且它与页面位于同一个命名空间中。

使用 PageModel 类,可以将页面的逻辑与其展示分离开来。 它定义了页面处理程序,用于处理发送到页面的请求和用于呈现页面的数据。 这种隔离可实现:

页面包含 OnPostAsync 处理程序方法,它在 POST 请求上运行(当用户发布窗体时)。 可以添加任何 HTTP 谓词的处理程序方法。 最常见的处理程序是:

  • OnGet,用于初始化页面所需的状态。 在上面的代码中,OnGet 方法显示 CreateModel.cshtmlRazor Page。
  • OnPost,用于处理窗体提交。

Async 命名后缀为可选,但是按照惯例通常会将它用于异步函数。 前面的代码通常用于 Razor Pages。

如果你熟悉使用控制器和视图的 ASP.NET 应用:

  • 前面示例中的 OnPostAsync 代码类似于典型的控制器代码。
  • 大多数 MVC 基元(例如模型绑定验证和操作结果)通过控制器和通过 Razor Pages 的运作方式相同。

之前的 OnPostAsync 方法:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

OnPostAsync 的基本流:

检查验证错误。

  • 如果没有错误,则保存数据并重定向。
  • 如果有错误,则再次显示页面并附带验证消息。 很多情况下,都会在客户端上检测到验证错误,并且从不将它们提交到服务器。

Pages/Customers/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

Pages/Customers/Create.cshtml 中呈现的 HTML:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

在前面的代码中,发布窗体:

  • 对于有效数据:

    • OnPostAsync 处理程序方法调用 RedirectToPage 帮助程序方法。 RedirectToPage 返回 RedirectToPageResult 的实例。 RedirectToPage:

      • 是操作结果。
      • 类似于 RedirectToActionRedirectToRoute(用于控制器和视图)。
      • 针对页面自定义。 在前面的示例中,它将重定向到根索引页 (/Index)。 页面 URL 生成部分中详细介绍了 RedirectToPage
  • 对于传递给服务器的验证错误:

    • OnPostAsync 处理程序方法调用 Page 帮助程序方法。 Page 返回 PageResult 的实例。 返回 Page 的过程与控制器中的操作返回 View 的过程相似。 PageResult 是处理程序方法的默认返回类型。 返回 void 的处理程序方法将显示页面。
    • 在前面的示例中,在 ModelState.IsValid 中的值结果不返回 false 的情况下发布窗体。 在此示例中,客户端上不显示任何验证错误。 本文档的后面将介绍验证错误处理。
    [BindProperty]
    public Customer? Customer { get; set; }
    
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • 对于客户端验证检测到的验证错误:

    • 数据不会发布到服务器。
    • 本文档的后面将介绍客户端验证。

Customer 属性使用 [BindProperty] 特性来选择加入模型绑定:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

[BindProperty] 不应 用于包含不应由客户端更改的属性的模型。 有关详细信息,请参阅过度发布

Razor Pages 只绑定带有非 GET 谓词的属性。 如果绑定到属性,则无需通过编写代码将 HTTP 数据转换为模型类型。 绑定通过使用相同的属性显示窗体字段 (<input asp-for="Customer.Name">) 来减少代码,并接受输入。

警告

出于安全原因,必须选择绑定 GET 请求数据以对模型属性进行分页。 请在将用户输入映射到属性前对其进行验证。 当处理依赖查询字符串或路由值的方案时,选择加入 GET 绑定非常有用。

若要将属性绑定在 GET 请求上,请将 [BindProperty] 特性的 SupportsGet 属性设置为 true

[BindProperty(SupportsGet = true)]

有关详细信息,请参阅 ASP.NET Core Community Standup:GET 上的绑定讨论 (YouTube)

查看 Pages/Customers/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
  • 在前面的代码中,输入标记帮助程序<input asp-for="Customer.Name" />将 HTML <input> 元素绑定到 Customer.Name 模型表达式。
  • 使用 @addTagHelper 提供标记帮助程序。

主页

Index.cshtml 是主页:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

关联的 PageModel 类 (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer>? Customers { get; set; }

    public async Task OnGetAsync()
    {
        Customers = await _context.Customer.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customer.FindAsync(id);

        if (contact != null)
        {
            _context.Customer.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

Index.cshtml 文件包含以下标记:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

<a /a>定位点标记帮助程序使用 asp-route-{value} 属性生成“编辑”页面的链接。 此链接包含路由数据及联系人 ID。 例如 https://localhost:5001/Edit/1标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

Index.cshtml 文件包含用于为每个客户联系人创建删除按钮的标记:

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

呈现的 HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

删除按钮采用 HTML 呈现,其 formaction 包括参数:

  • asp-route-id 属性指定的客户联系人 ID。
  • asp-page-handler 属性指定的 handler

选中按钮时,向服务器发送窗体 POST 请求。 按照惯例,根据方案 OnPost[handler]Async 基于 handler 参数的值来选择处理程序方法的名称。

因为本示例中 handlerdelete,因此 OnPostDeleteAsync 处理程序方法用于处理 POST 请求。 如果 asp-page-handler 设置为其他值(如 remove),则选择名称为 OnPostRemoveAsync 的处理程序方法。

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customer.FindAsync(id);

    if (contact != null)
    {
        _context.Customer.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

OnPostDeleteAsync 方法:

  • 获取来自查询字符串的 id
  • 使用 FindAsync 查询客户联系人的数据库。
  • 如果找到客户联系人,则会将其删除,并更新数据库。
  • 调用 RedirectToPage,重定向到根索引页 (/Index)。

Edit.cshtml 文件

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Customer</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Customer!.Id" />
            <div class="form-group">
                <label asp-for="Customer!.Name" class="control-label"></label>
                <input asp-for="Customer!.Name" class="form-control" />
                <span asp-validation-for="Customer!.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

第一行包含 @page "{id:int}" 指令。 路由约束 "{id:int}" 告诉页面接受包含 int 路由数据的页面请求。 如果页面请求未包含可转换为 int 的路由数据,则运行时返回 HTTP 404(未找到)错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

Edit.cshtml.cs 文件:

public class EditModel : PageModel
{
    private readonly RazorPagesContacts.Data.CustomerDbContext _context;

    public EditModel(RazorPagesContacts.Data.CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id == id);
        
        if (Customer == null)
        {
            return NotFound();
        }
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null)
        {
            _context.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(Customer.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
        }

        return RedirectToPage("./Index");
    }

    private bool CustomerExists(int id)
    {
        return _context.Customer.Any(e => e.Id == id);
    }
}

验证

验证规则:

  • 在模型类中以声明方式指定。
  • 在应用中的所有位置强制执行。

System.ComponentModel.DataAnnotations 命名空间提供一组内置验证特性,可通过声明方式应用于类或属性。 DataAnnotations 还包含 [DataType] 等格式特性,有助于格式设置但不提供任何验证。

请考虑 Customer 模型:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

使用以下 Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

前面的代码:

  • 包括 jQuery 和 jQuery 验证脚本。

  • 使用 <div /><span />标记帮助程序以实现:

    • 客户端验证。
    • 验证错误呈现。
  • 则会生成以下 HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

如果在不使用名称值的情况下发布“创建”窗体,则将在窗体上显示错误消息“名称字段是必需的”。 如果客户端上已启用 JavaScript,浏览器会显示错误,而不会发布到服务器。

[StringLength(10)] 特性在呈现的 HTML 上生成 data-val-length-max="10"data-val-length-max 阻止浏览器输入超过指定最大长度的内容。 如果使用 Fiddler 等工具来编辑和重播文章:

  • 对于长度超过 10 的名称。
  • 返回错误消息“‘名称’字段必须是最大长度为 10 的字符串。”

考虑下列 Movie 模型:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

验证特性指定要对应用这些特性的模型属性强制执行的行为:

  • RequiredMinimumLength 特性表示属性必须有值,但用户可输入空格来满足此验证。

  • RegularExpression 特性用于限制可输入的字符。 在上述代码中,即“Genre”(分类):

    • 只能使用字母。
    • 第一个字母必须为大写。 不允许使用空格、数字和特殊字符。
  • RegularExpression“Rating”(分级):

    • 要求第一个字符为大写字母。
    • 允许在后续空格中使用特殊字符和数字。 “PG-13”对“分级”有效,但对于“分类”无效。
  • Range 特性将值限制在指定范围内。

  • StringLength 特性可以设置字符串属性的最大长度,以及可选的最小长度。

  • 从本质上来说,需要值类型(如 decimalintfloatDateTime),但不需要 [Required] 特性。

Movie 模型的“创建”页面显示无效值错误:

Movie view form with multiple jQuery client-side validation errors

有关详细信息,请参阅:

CSS 隔离

将 CSS 样式隔离到各个页面、视图和组件以减少或避免:

  • 依赖难以维护的全局样式。
  • 嵌套内容中的样式冲突。

若要为页面或视图添加限定范围的 CSS 文件,请将 CSS 样式置于与 .cshtml 文件的名称匹配的配套 .cshtml.css 文件中。 在下面的示例中,Index.cshtml.css 文件提供只应用于 Index.cshtml 页面或视图的 CSS 样式。

Pages/Index.cshtml.css (Razor Pages) 或 Views/Index.cshtml.css (MVC):

h1 {
    color: red;
}

CSS 隔离在生成时发生。 框架会重写 CSS 选择器,以匹配应用页面或视图所呈现的标记。 重写的 CSS 样式作为静态资产 {APP ASSEMBLY}.styles.css 进行捆绑和生成。 占位符 {APP ASSEMBLY} 是项目的程序集名称。 指向捆绑 CSS 样式的链接放置在应用的布局中。

在应用 Pages/Shared/_Layout.cshtml (Razor Pages) 或 Views/Shared/_Layout.cshtml (MVC) 的 <head> 内容中,添加或确认是否存在指向捆绑 CSS 样式的链接:

<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />

在下面的示例中,应用的程序集名称为 WebApp

<link rel="stylesheet" href="WebApp.styles.css" />

在限定范围的 CSS 文件中定义的样式仅应用于匹配文件的呈现输出。 在上面的示例中,在应用的其他位置定义的任何 h1 CSS 声明都不会与 Index 标头样式冲突。 CSS 样式级联和继承规则对限定范围的 CSS 文件仍然有效。 例如,直接应用于 Index.cshtml 文件中的 <h1> 元素的样式会替代 Index.cshtml.css 中限定范围的 CSS 文件的样式。

注意

为了保证发生捆绑时的 CSS 样式隔离,不支持在 Razor 代码块中导入 CSS。

CSS 隔离仅适用于 HTML 元素。 标记帮助程序不支持 CSS 隔离。

在捆绑 CSS 文件中,每个页面、视图或 Razor 组件都与格式为 b-{STRING} 的范围标识符相关联,其中 {STRING} 占位符是框架生成的十个字符的字符串。 下面的示例提供了 Razor Pages 应用 Index 页面中前面 <h1> 元素的样式:

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}

在从捆绑文件应用 CSS 样式的 Index 页面中,范围标识符追加为 HTML 属性:

<h1 b-3xxtam6d07>

标识符对应用是唯一的。 在生成时,会使用约定 {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css 创建项目捆绑包,其中占位符 {STATIC WEB ASSETS BASE PATH} 是静态 Web 资产的基路径。

如果利用了其他项目(如 NuGet 包或Razor 类库),则捆绑的文件将发生以下情况:

  • 使用 CSS 导入引用这些样式。
  • 不会发布为使用这些样式的应用的静态 Web 资产。

CSS 预处理器支持

利用 CSS 预处理器的变量、嵌套、模块、混合和继承等功能,可有效改进 CSS 开发。 虽然 CSS 隔离并不原生支持 CSS 预处理器(如 Sass 或 Less),但只要在生成过程中框架重写 CSS 选择器的步骤之前进行预处理器编译,就可以无缝集成 CSS 预处理器。 例如,使用 Visual Studio 将现有预处理器编译配置为 Visual Studio 任务运行程序资源管理器中的“生成前”任务。

许多第三方 NuGet 包(如 AspNetCore.SassCompiler)都可以在生成过程开始时编译 SASS/SCSS 文件,再进行 CSS 隔离,而无需其他配置。

CSS 隔离配置

CSS 隔离允许在某些高级场景(例如依赖于现有工具或工作流)下进行配置。

自定义范围标识符格式

在此部分中,{Pages|Views} 占位符为 Razor Pages 应用的 Pages 或 MVC 应用的 Views

默认情况下,范围标识符使用格式 b-{STRING},其中 {STRING} 占位符是框架生成的十个字符的字符串。 若要自定义范围标识符格式,请将项目文件更新为所需模式:

<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

在上面的示例中,为 Index.cshtml.css 生成的 CSS 将其范围标识符从 b-{STRING} 更改为了 custom-scope-identifier

使用范围标识符来实现与限定范围的 CSS 文件的继承。 在下面的项目文件示例中,BaseView.cshtml.css 文件包含跨视图的通用样式。 DerivedView.cshtml.css 文件继承了这些样式。

<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

使用通配符 (*) 运算符跨多个文件共享范围标识符:

<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

更改静态 Web 资产的基路径

限定范围的 CSS 文件在应用的根目录生成。 在项目文件中,请使用 StaticWebAssetBasePath 属性来更改默认路径。 下面的示例将限定范围的 CSS 文件以及应用的其余资产放在 _content 路径:

<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

禁用自动捆绑

若要禁用框架在运行时发布和加载限定范围的文件,请使用 DisableScopedCssBundling 属性。 使用此属性时,由其他工具或进程从 obj 目录中捕获隔离的 CSS 文件,并在运行时发布和加载这些文件:

<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor 类库 (RCL) 支持

Razor 类库 (RCL) 提供隔离的样式,<link> 标记的 href 属性指向 {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css,其中占位符为:

  • {STATIC WEB ASSET BASE PATH}:静态 Web 资产基路径。
  • {PACKAGE ID}:库的包标识符。 如果项目文件中未指定包标识符,则包标识符默认为项目的程序集名称。

如下示例中:

  • 静态 Web 资产基路径为 _content/ClassLib
  • 类库的程序集名称为 ClassLib

Pages/Shared/_Layout.cshtml (Razor Pages) 或 Views/Shared/_Layout.cshtml (MVC):

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

有关 RCL 的详细信息,请参阅以下文章:

有关 Blazor CSS 隔离的信息,请参阅 ASP.NET Core Blazor CSS 隔离

使用 OnGet 处理程序回退来处理 HEAD 请求

HEAD 请求可检索特定资源的标头。 与 GET 请求不同,HEAD 请求不返回响应正文。

通常,针对 HEAD 请求创建和调用 OnHead 处理程序:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

如果未定义 OnHead 处理程序,则 Razor Pages 会回退到调用 OnGet 处理程序。

XSRF/CSRF 和 Razor Pages

Razor Pages 由 防伪造验证保护。 FormTagHelper 将防伪造令牌注入 HTML 窗体元素。

将布局、分区、模板和标记帮助程序用于 Razor Pages

页面可使用 Razor 视图引擎的所有功能。 布局、分区、模板、标记帮助程序、_ViewStart.cshtml_ViewImports.cshtml 的工作方式与它们在传统的 Razor 视图中的工作方式相同。

让我们使用其中的一些功能来整理此页面。

Pages/Shared/_Layout.cshtml 添加布局页面

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

布局

  • 控制每个页面的布局(页面选择退出布局时除外)。
  • 导入 HTML 结构,例如 JavaScript 和样式表。
  • 调用 @RenderBody() 时,呈现 Razor Page 的内容。

有关详细信息,请参阅布局页面

Pages/_ViewStart.cshtml 中设置 Layout 属性:

@{
    Layout = "_Layout";
}

布局位于“页面/共享”文件夹中。 页面按层次结构从当前页面的文件夹开始查找其他视图(布局、模板、分区)。 可以从“Pages”文件夹下的任意 Razor 页面使用“Pages/Shared”文件夹中的布局。

布局文件应位于 Pages/Shared 文件夹中。

建议不要将布局文件放在“视图/共享”文件夹中。 视图/共享 是一种 MVC 视图模式。 Razor Pages 旨在依赖文件夹层次结构,而非路径约定。

Razor Page 中的视图搜索包含“Pages”文件夹。 用于 MVC 控制器和传统 Razor 视图的布局、模板和分区可正常运行。

添加 Pages/_ViewImports.cshtml 文件:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

本教程的后续部分中将介绍 @namespace@addTagHelper 指令将内置标记帮助程序引入“页面”文件夹中的所有页面。

页面上设置的 @namespace 指令:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

@namespace 指令将为页面设置命名空间。 @model 指令无需包含命名空间。

_ViewImports.cshtml 中包含 @namespace 指令后,指定的命名空间将为在导入 @namespace 指令的页面中生成的命名空间提供前缀。 生成的命名空间的剩余部分(后缀部分)是包含 _ViewImports.cshtml 的文件夹与包含页面的文件夹之间以点分隔的相对路径。

例如,PageModelPages/Customers/Edit.cshtml.cs 显式设置命名空间:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Pages/_ViewImports.cshtml 文件设置以下命名空间:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Pages/Customers/Edit.cshtmlRazor Page 生成的命名空间与 PageModel 类相同。

@namespace也适用于传统 Razor 视图。

考虑 Pages/Customers/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

包含 _ViewImports.cshtml 的已更新的 Pages/Customers/Create.cshtml 视图文件和前面的布局文件:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

在前面的代码中,_ViewImports.cshtml 导入了命名空间和标记帮助程序。 布局文件导入了 JavaScript 文件。

Razor Pages 初学者项目包含 Pages/_ValidationScriptsPartial.cshtml,它与客户端验证联合。

有关分部视图的详细信息,请参阅 ASP.NET Core 中的分部视图

页面的 URL 生成

之前显示的 Create 页面使用 RedirectToPage

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

应用具有以下文件/文件夹结构:

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

成功后,Pages/Customers/Create.cshtmlPages/Customers/Edit.cshtml 页面将重定向到 Pages/Customers/Index.cshtml。 字符串 ./Index 是用于访问前一页的相对页名称。 它用于生成 Pages/Customers/Index.cshtml 页面的 URL。 例如:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

绝对页名称 /Index 用于生成 Pages/Index.cshtml 页面的 URL。 例如:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

页面名称是从根“/Pages”文件夹到页面的路径(包含前导 /,例如 /Index)。 与硬编码 URL 相比,前面的 URL 生成示例提供了改进的选项和功能。 URL 生成使用路由,并且可以根据目标路径定义路由的方式生成参数并对参数编码。

页面的 URL 生成支持相对名称。 下表显示了 Pages/Customers/Create.cshtml 中不同的 RedirectToPage 参数选择的索引页。

RedirectToPage(x) 页面
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index")RedirectToPage("./Index")RedirectToPage("../Index") 是相对名称。 结合RedirectToPage 参数与当前页的路径来计算目标页面的名称。

构建结构复杂的站点时,相对名称链接很有用。 如果使用相对名称链接文件夹中的页面:

  • 重命名文件夹不会破坏相对链接。
  • 链接不会中断,因为它们不包含文件夹名称。

若要重定向到不同区域中的页面,请指定区域:

RedirectToPage("/Index", new { area = "Services" });

有关详细信息,请参阅 ASP.NET Core 中的区域ASP.NET Core 中的 Razor Pages 路由和应用约定

ViewData 特性

可以通过 ViewDataAttribute 将数据传递到页面。 具有 [ViewData] 特性的属性从 ViewDataDictionary 保存和加载值。

在下面的示例中,AboutModel[ViewData] 特性应用于 Title 属性:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

在“关于”页面中,以模型属性的形式访问 Title 属性:

<h1>@Model.Title</h1>

在布局中,从 ViewData 字典读取标题:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core 公开 TempData。 此属性存储未读取的数据。 KeepPeek 方法可用于检查数据,而不执行删除。 TempData 在多个请求需要数据的情况下对重定向很有用。

下面的代码使用 TempData 设置 Message 的值:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Pages/Customers/Index.cshtml 文件中的以下标记使用 TempData 显示 Message 的值。

<h3>Msg: @Model.Message</h3>

Pages/Customers/Index.cshtml.cs 页面模型将 [TempData] 属性应用到 Message 属性。

[TempData]
public string Message { get; set; }

有关详细信息,请参阅 TempData

针对一个页面的多个处理程序

以下页面使用 asp-page-handler 标记帮助程序为两个处理程序生成标记:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

前面示例中的窗体包含两个提交按钮,每个提交按钮均使用 FormActionTagHelper 提交到不同的 URL。 asp-page-handlerasp-page 的配套属性。 asp-page-handler 生成提交到页面定义的各个处理程序方法的 URL。 未指定 asp-page,因为示例已链接到当前页面。

页面模型:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

前面的代码使用已命名处理程序方法。 已命名处理程序方法通过采用名称中 On<HTTP Verb> 之后及 Async 之前的文本(如果有)创建。 在前面的示例中,页面方法是 OnPostJoinListAsync 和 OnPostJoinListUCAsync。 删除 OnPost 和 Async 后,处理程序名称为 JoinListJoinListUC

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

使用前面的代码时,提交到 OnPostJoinListAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH?handler=JoinList。 提交到 OnPostJoinListUCAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH?handler=JoinListUC

自定义路由

使用 @page 指令,执行以下操作:

  • 指定页面的自定义路由。 例如,使用 @page "/Some/Other/Path" 将“关于”页面的路由设置为 /Some/Other/Path
  • 将段追加到页面的默认路由。 例如,可使用 @page "item" 将“item”段添加到页面的默认路由。
  • 将参数追加到页面的默认路由。 例如,@page "{id}" 页面需要 ID 参数 id

支持开头处以波形符 (~) 指定的相对于根目录的路径。 例如,@page "~/Some/Other/Path"@page "/Some/Other/Path" 相同。

如果你不喜欢 URL 中的查询字符串 ?handler=JoinList,请更改路由,将处理程序名称放在 URL 的路径部分。 可以通过在 @page 指令后面添加使用双引号括起来的路由模板来自定义路由。

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

使用前面的代码时,提交到 OnPostJoinListAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH/JoinList。 提交到 OnPostJoinListUCAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH/JoinListUC

handler 前面的 ? 表示路由参数为可选。

JavaScript (JS) 文件的并置

为页面和视图并置 JavaScript (JS) 文件是在应用中组织脚本的简便方法。

使用以下文件名扩展约定并置 JS 文件:

  • Razor Pages 应用的页面和 MVC 应用的视图:.cshtml.js。 示例:
    • 对于位于 Pages/Index.cshtml 的 Razor Pages 应用的 Index 页面,使用 Pages/Index.cshtml.js
    • 对于位于 Views/Home/Index.cshtml 的 MVC 应用的 Index 视图,使用 Views/Home/Index.cshtml.js

使用项目中文件的路径可以公开寻址并置的 JS 文件:

  • 应用中并置脚本文件的页面和视图:

    {PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • 占位符 {PATH} 是页面、视图或组件的路径。
    • 占位符 {PAGE, VIEW, OR COMPONENT} 是页面、视图或组件。
    • 占位符 {EXTENSION} 与页面、视图或组件的扩展名匹配,为 razorcshtml

    Razor Pages 示例:

    Index 页面的 JS 文件放置在 Index 页面 (Pages/Index.cshtml) 旁边的 Pages 文件夹 (Pages/Index.cshtml.js) 中。 在 Index 页面中,脚本在 Pages 文件夹中的路径引用:

    @section Scripts {
      <script src="~/Pages/Index.cshtml.js"></script>
    }
    

    发布应用后,框架会自动将脚本移动到 Web 根目录。 在前面的示例中,脚本将移动到 bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js,其中 {TARGET FRAMEWORK MONIKER} 占位符是目标框架名字对象 (TFM)。 无需更改 Index 页面中脚本的相对 URL。

    发布应用后,框架会自动将脚本移动到 Web 根目录。 在前面的示例中,脚本被移动到 bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Components\Pages\Index.razor.js,其中 {TARGET FRAMEWORK MONIKER} 占位符是目标框架名字对象 (TFM)。 无需更改 Index 组件中脚本的相对 URL。

  • 对于 Razor 类库 (RCL) 提供的脚本:

    _content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js

    • 占位符 {PACKAGE ID} 是 RCL 的包标识符(或由应用引用的类库的库名称)。
    • 占位符 {PATH} 是页面、视图或组件的路径。 如果 Razor 组件位于 RCL 的根目录下,则不包括路径段。
    • 占位符 {PAGE, VIEW, OR COMPONENT} 是页面、视图或组件。
    • 占位符 {EXTENSION} 与页面、视图或组件的扩展名匹配,为 razorcshtml

高级配置和设置

大多数应用不需要以下部分中的配置和设置。

要配置高级选项,请使用 AddRazorPages 重载,该重载配置 RazorPagesOptions

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/MyPages";
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

使用 RazorPagesOptions 设置页面的根目录,或者为页面添加应用程序模型约定。 有关约定的详细信息,请参阅 Razor Pages 授权约定.

若要预编译视图,请参阅 Razor 视图编译

指定 Razor Pages 位于内容根目录中

默认情况下,Razor Pages 位于 /Pages 目录的根位置。 添加 WithRazorPagesAtContentRoot 以指定 Razor Pages 位于应用的内容根 (ContentRootPath):

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

指定 Razor Pages 位于自定义根目录中

添加 WithRazorPagesRoot,以指定 Razor Pages 位于应用中自定义根目录位置(提供相对路径):

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

其他资源

创建 Razor Pages 项目

请参阅 Razor Pages 入门,获取关于如何创建 Razor Pages 项目的详细说明。

Razor Pages

Startup.cs 中已启用 Razor Pages:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

请考虑一个基本页面:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

前面的代码与具有控制器和视图的 ASP.NET Core 应用中使用的 Razor 视图文件非常相似。 不同之处在于 @page 指令。 @page 使文件转换为一个 MVC 操作 ,这意味着它将直接处理请求,而无需通过控制器处理。 @page 必须是页面上的第一个 Razor 指令。 @page 会影响其他 Razor 构造的行为。 Razor Pages 文件名有 .cshtml 后缀。

将在以下两个文件中显示使用 PageModel 类的类似页面。 Pages/Index2.cshtml 文件:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

Pages/Index2.cshtml.cs 页面模型:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}

按照惯例,PageModel 类文件的名称与追加 .cs 的 Razor Page 文件名称相同。 例如,前面的 Razor Page 的名称为 Pages/Index2.cshtml。 包含 PageModel 类的文件的名称为 Pages/Index2.cshtml.cs

页面的 URL 路径的关联由页面在文件系统中的位置决定。 下表显示了 Razor Page 路径及匹配的 URL:

文件名和路径 匹配的 URL
/Pages/Index.cshtml //Index
/Pages/Contact.cshtml /Contact
/Pages/Store/Contact.cshtml /Store/Contact
/Pages/Store/Index.cshtml /Store/Store/Index

注意:

  • 默认情况下,运行时在“Pages”文件夹中查找 Razor Pages 文件。
  • URL 未包含页面时,Index 为默认页面。

编写基本窗体

由于 Razor Pages 的设计,在构建应用时可轻松实施用于 Web 浏览器的常用模式。 模型绑定标记帮助程序和 HTML 帮助程序均只可用于 Razor Page 类中定义的属性。 请参考为 Contact 模型实现的基本的“联系我们”窗体页面:

在本文档中的示例中,DbContextStartup.cs 文件中进行初始化。

内存中数据库需要 Microsoft.EntityFrameworkCore.InMemory NuGet 包。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}

数据模型:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

数据库上下文:

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}

Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Pages/Create.cshtml.cs 页面模型:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

按照惯例,PageModel 类命名为 <PageName>Model并且它与页面位于同一个命名空间中。

使用 PageModel 类,可以将页面的逻辑与其展示分离开来。 它定义了页面处理程序,用于处理发送到页面的请求和用于呈现页面的数据。 这种隔离可实现:

页面包含 OnPostAsync 处理程序方法,它在 POST 请求上运行(当用户发布窗体时)。 可以添加任何 HTTP 谓词的处理程序方法。 最常见的处理程序是:

  • OnGet,用于初始化页面所需的状态。 在上面的代码中,OnGet 方法显示 CreateModel.cshtmlRazor Page。
  • OnPost,用于处理窗体提交。

Async 命名后缀为可选,但是按照惯例通常会将它用于异步函数。 前面的代码通常用于 Razor Pages。

如果你熟悉使用控制器和视图的 ASP.NET 应用:

  • 前面示例中的 OnPostAsync 代码类似于典型的控制器代码。
  • 大多数 MVC 基元(例如模型绑定验证和操作结果)通过控制器和通过 Razor Pages 的运作方式相同。

之前的 OnPostAsync 方法:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

OnPostAsync 的基本流:

检查验证错误。

  • 如果没有错误,则保存数据并重定向。
  • 如果有错误,则再次显示页面并附带验证消息。 很多情况下,都会在客户端上检测到验证错误,并且从不将它们提交到服务器。

Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

Pages/Create.cshtml 中呈现的 HTML:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

在前面的代码中,发布窗体:

  • 对于有效数据:

    • OnPostAsync 处理程序方法调用 RedirectToPage 帮助程序方法。 RedirectToPage 返回 RedirectToPageResult 的实例。 RedirectToPage

      • 是操作结果。
      • 类似于 RedirectToActionRedirectToRoute(用于控制器和视图)。
      • 针对页面自定义。 在前面的示例中,它将重定向到根索引页 (/Index)。 页面 URL 生成部分中详细介绍了 RedirectToPage
  • 对于传递给服务器的验证错误:

    • OnPostAsync 处理程序方法调用 Page 帮助程序方法。 Page 返回 PageResult 的实例。 返回 Page 的过程与控制器中的操作返回 View 的过程相似。 PageResult 是处理程序方法的默认返回类型。 返回 void 的处理程序方法将显示页面。
    • 在前面的示例中,在 ModelState.IsValid 中的值结果不返回 false 的情况下发布窗体。 在此示例中,客户端上不显示任何验证错误。 本文档的后面将介绍验证错误处理。
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • 对于客户端验证检测到的验证错误:

    • 数据不会发布到服务器。
    • 本文档的后面将介绍客户端验证。

Customer 属性使用 [BindProperty] 特性来选择加入模型绑定:

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

[BindProperty] 不应 用于包含不应由客户端更改的属性的模型。 有关详细信息,请参阅过度发布

Razor Pages 只绑定带有非 GET 谓词的属性。 如果绑定到属性,则无需通过编写代码将 HTTP 数据转换为模型类型。 绑定通过使用相同的属性显示窗体字段 (<input asp-for="Customer.Name">) 来减少代码,并接受输入。

警告

出于安全原因,必须选择绑定 GET 请求数据以对模型属性进行分页。 请在将用户输入映射到属性前对其进行验证。 当处理依赖查询字符串或路由值的方案时,选择加入 GET 绑定非常有用。

若要将属性绑定在 GET 请求上,请将 [BindProperty] 特性的 SupportsGet 属性设置为 true

[BindProperty(SupportsGet = true)]

有关详细信息,请参阅 ASP.NET Core Community Standup:GET 上的绑定讨论 (YouTube)

查看 Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
  • 在前面的代码中,输入标记帮助程序<input asp-for="Customer.Name" />将 HTML <input> 元素绑定到 Customer.Name 模型表达式。
  • 使用 @addTagHelper 提供标记帮助程序。

主页

Index.cshtml 是主页:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

关联的 PageModel 类 (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

Index.cshtml 文件包含以下标记:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

<a /a>定位点标记帮助程序使用 asp-route-{value} 属性生成“编辑”页面的链接。 此链接包含路由数据及联系人 ID。 例如 https://localhost:5001/Edit/1标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素。

Index.cshtml 文件包含用于为每个客户联系人创建删除按钮的标记:

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

呈现的 HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

删除按钮采用 HTML 呈现,其 formaction 包括参数:

  • asp-route-id 属性指定的客户联系人 ID。
  • asp-page-handler 属性指定的 handler

选中按钮时,向服务器发送窗体 POST 请求。 按照惯例,根据方案 OnPost[handler]Async 基于 handler 参数的值来选择处理程序方法的名称。

因为本示例中 handlerdelete,因此 OnPostDeleteAsync 处理程序方法用于处理 POST 请求。 如果 asp-page-handler 设置为其他值(如 remove),则选择名称为 OnPostRemoveAsync 的处理程序方法。

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

OnPostDeleteAsync 方法:

  • 获取来自查询字符串的 id
  • 使用 FindAsync 查询客户联系人的数据库。
  • 如果找到客户联系人,则会将其删除,并更新数据库。
  • 调用 RedirectToPage,重定向到根索引页 (/Index)。

Edit.cshtml 文件

@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

第一行包含 @page "{id:int}" 指令。 路由约束 "{id:int}" 告诉页面接受包含 int 路由数据的页面请求。 如果页面请求未包含可转换为 int 的路由数据,则运行时返回 HTTP 404(未找到)错误。 若要使 ID 可选,请将 ? 追加到路由约束:

@page "{id:int?}"

Edit.cshtml.cs 文件:

public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}

验证

验证规则:

  • 在模型类中以声明方式指定。
  • 在应用中的所有位置强制执行。

System.ComponentModel.DataAnnotations 命名空间提供一组内置验证特性,可通过声明方式应用于类或属性。 DataAnnotations 还包含 [DataType] 等格式特性,有助于格式设置但不提供任何验证。

请考虑 Customer 模型:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}

使用以下 Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

前面的代码:

  • 包括 jQuery 和 jQuery 验证脚本。

  • 使用 <div /><span />标记帮助程序以实现:

    • 客户端验证。
    • 验证错误呈现。
  • 则会生成以下 HTML:

    <p>Enter a customer name:</p>
    
    <form method="post">
        Name:
        <input type="text" data-val="true"
               data-val-length="The field Name must be a string with a maximum length of 10."
               data-val-length-max="10" data-val-required="The Name field is required."
               id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
        <input type="submit" />
        <input name="__RequestVerificationToken" type="hidden"
               value="<Antiforgery token here>" />
    </form>
    
    <script src="/lib/jquery/dist/jquery.js"></script>
    <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
    

如果在不使用名称值的情况下发布“创建”窗体,则将在窗体上显示错误消息“名称字段是必需的”。 如果客户端上已启用 JavaScript,浏览器会显示错误,而不会发布到服务器。

[StringLength(10)] 特性在呈现的 HTML 上生成 data-val-length-max="10"data-val-length-max 阻止浏览器输入超过指定最大长度的内容。 如果使用 Fiddler 等工具来编辑和重播文章:

  • 对于长度超过 10 的名称。
  • 返回错误消息“‘名称’字段必须是最大长度为 10 的字符串。”

考虑下列 Movie 模型:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}

验证特性指定要对应用这些特性的模型属性强制执行的行为:

  • RequiredMinimumLength 特性表示属性必须有值,但用户可输入空格来满足此验证。

  • RegularExpression 特性用于限制可输入的字符。 在上述代码中,即“Genre”(分类):

    • 只能使用字母。
    • 第一个字母必须为大写。 不允许使用空格、数字和特殊字符。
  • RegularExpression“Rating”(分级):

    • 要求第一个字符为大写字母。
    • 允许在后续空格中使用特殊字符和数字。 “PG-13”对“分级”有效,但对于“分类”无效。
  • Range 特性将值限制在指定范围内。

  • StringLength 特性可以设置字符串属性的最大长度,以及可选的最小长度。

  • 从本质上来说,需要值类型(如 decimalintfloatDateTime),但不需要 [Required] 特性。

Movie 模型的“创建”页面显示无效值错误:

Movie view form with multiple jQuery client-side validation errors

有关详细信息,请参阅:

使用 OnGet 处理程序回退来处理 HEAD 请求

HEAD 请求可检索特定资源的标头。 与 GET 请求不同,HEAD 请求不返回响应正文。

通常,针对 HEAD 请求创建和调用 OnHead 处理程序:

public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}

如果未定义 OnHead 处理程序,则 Razor Pages 会回退到调用 OnGet 处理程序。

XSRF/CSRF 和 Razor Pages

Razor Pages 由 防伪造验证保护。 FormTagHelper 将防伪造令牌注入 HTML 窗体元素。

将布局、分区、模板和标记帮助程序用于 Razor Pages

页面可使用 Razor 视图引擎的所有功能。 布局、分区、模板、标记帮助程序、_ViewStart.cshtml_ViewImports.cshtml 的工作方式与它们在传统的 Razor 视图中的工作方式相同。

让我们使用其中的一些功能来整理此页面。

Pages/Shared/_Layout.cshtml 添加布局页面

<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>

布局

  • 控制每个页面的布局(页面选择退出布局时除外)。
  • 导入 HTML 结构,例如 JavaScript 和样式表。
  • 调用 @RenderBody() 时,呈现 Razor Page 的内容。

有关详细信息,请参阅布局页面

Pages/_ViewStart.cshtml 中设置 Layout 属性:

@{
    Layout = "_Layout";
}

布局位于“页面/共享”文件夹中。 页面按层次结构从当前页面的文件夹开始查找其他视图(布局、模板、分区)。 可以从“Pages”文件夹下的任意 Razor 页面使用“Pages/Shared”文件夹中的布局。

布局文件应位于 Pages/Shared 文件夹中。

建议不要将布局文件放在“视图/共享”文件夹中。 视图/共享 是一种 MVC 视图模式。 Razor Pages 旨在依赖文件夹层次结构,而非路径约定。

Razor Page 中的视图搜索包含“Pages”文件夹。 用于 MVC 控制器和传统 Razor 视图的布局、模板和分区可正常运行。

添加 Pages/_ViewImports.cshtml 文件:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

本教程的后续部分中将介绍 @namespace@addTagHelper 指令将内置标记帮助程序引入“页面”文件夹中的所有页面。

页面上设置的 @namespace 指令:

@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>

@namespace 指令将为页面设置命名空间。 @model 指令无需包含命名空间。

_ViewImports.cshtml 中包含 @namespace 指令后,指定的命名空间将为在导入 @namespace 指令的页面中生成的命名空间提供前缀。 生成的命名空间的剩余部分(后缀部分)是包含 _ViewImports.cshtml 的文件夹与包含页面的文件夹之间以点分隔的相对路径。

例如,PageModelPages/Customers/Edit.cshtml.cs 显式设置命名空间:

namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.

Pages/_ViewImports.cshtml 文件设置以下命名空间:

@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Pages/Customers/Edit.cshtmlRazor Page 生成的命名空间与 PageModel 类相同。

@namespace也适用于传统 Razor 视图。

考虑 Pages/Create.cshtml 视图文件:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>

包含 _ViewImports.cshtml 的已更新的 Pages/Create.cshtml 视图文件和前面的布局文件:

@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

在前面的代码中,_ViewImports.cshtml 导入了命名空间和标记帮助程序。 布局文件导入了 JavaScript 文件。

Razor Pages 初学者项目包含 Pages/_ValidationScriptsPartial.cshtml,它与客户端验证联合。

有关分部视图的详细信息,请参阅 ASP.NET Core 中的分部视图

页面的 URL 生成

之前显示的 Create 页面使用 RedirectToPage

public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

应用具有以下文件/文件夹结构:

  • /Pages

    • Index.cshtml

    • Privacy.cshtml

    • /Customers

      • Create.cshtml
      • Edit.cshtml
      • Index.cshtml

成功后,Pages/Customers/Create.cshtmlPages/Customers/Edit.cshtml 页面将重定向到 Pages/Customers/Index.cshtml。 字符串 ./Index 是用于访问前一页的相对页名称。 它用于生成 Pages/Customers/Index.cshtml 页面的 URL。 例如:

  • Url.Page("./Index", ...)
  • <a asp-page="./Index">Customers Index Page</a>
  • RedirectToPage("./Index")

绝对页名称 /Index 用于生成 Pages/Index.cshtml 页面的 URL。 例如:

  • Url.Page("/Index", ...)
  • <a asp-page="/Index">Home Index Page</a>
  • RedirectToPage("/Index")

页面名称是从根“/Pages”文件夹到页面的路径(包含前导 /,例如 /Index)。 与硬编码 URL 相比,前面的 URL 生成示例提供了改进的选项和功能。 URL 生成使用路由,并且可以根据目标路径定义路由的方式生成参数并对参数编码。

页面的 URL 生成支持相对名称。 下表显示了 Pages/Customers/Create.cshtml 中不同的 RedirectToPage 参数选择的索引页。

RedirectToPage(x) 页面
RedirectToPage("/Index") Pages/Index
RedirectToPage("./Index"); Pages/Customers/Index
RedirectToPage("../Index") Pages/Index
RedirectToPage("Index") Pages/Customers/Index

RedirectToPage("Index")RedirectToPage("./Index")RedirectToPage("../Index") 是相对名称。 结合RedirectToPage 参数与当前页的路径来计算目标页面的名称。

构建结构复杂的站点时,相对名称链接很有用。 如果使用相对名称链接文件夹中的页面:

  • 重命名文件夹不会破坏相对链接。
  • 链接不会中断,因为它们不包含文件夹名称。

若要重定向到不同区域中的页面,请指定区域:

RedirectToPage("/Index", new { area = "Services" });

有关详细信息,请参阅 ASP.NET Core 中的区域ASP.NET Core 中的 Razor Pages 路由和应用约定

ViewData 特性

可以通过 ViewDataAttribute 将数据传递到页面。 具有 [ViewData] 特性的属性从 ViewDataDictionary 保存和加载值。

在下面的示例中,AboutModel[ViewData] 特性应用于 Title 属性:

public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}

在“关于”页面中,以模型属性的形式访问 Title 属性:

<h1>@Model.Title</h1>

在布局中,从 ViewData 字典读取标题:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

TempData

ASP.NET Core 公开 TempData。 此属性存储未读取的数据。 KeepPeek 方法可用于检查数据,而不执行删除。 TempData 在多个请求需要数据的情况下对重定向很有用。

下面的代码使用 TempData 设置 Message 的值:

public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}

Pages/Customers/Index.cshtml 文件中的以下标记使用 TempData 显示 Message 的值。

<h3>Msg: @Model.Message</h3>

Pages/Customers/Index.cshtml.cs 页面模型将 [TempData] 属性应用到 Message 属性。

[TempData]
public string Message { get; set; }

有关详细信息,请参阅 TempData

针对一个页面的多个处理程序

以下页面使用 asp-page-handler 标记帮助程序为两个处理程序生成标记:

@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>

前面示例中的窗体包含两个提交按钮,每个提交按钮均使用 FormActionTagHelper 提交到不同的 URL。 asp-page-handlerasp-page 的配套属性。 asp-page-handler 生成提交到页面定义的各个处理程序方法的 URL。 未指定 asp-page,因为示例已链接到当前页面。

页面模型:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}

前面的代码使用已命名处理程序方法。 已命名处理程序方法通过采用名称中 On<HTTP Verb> 之后及 Async 之前的文本(如果有)创建。 在前面的示例中,页面方法是 OnPostJoinListAsync 和 OnPostJoinListUCAsync。 删除 OnPost 和 Async 后,处理程序名称为 JoinListJoinListUC

<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />

使用前面的代码时,提交到 OnPostJoinListAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH?handler=JoinList。 提交到 OnPostJoinListUCAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH?handler=JoinListUC

自定义路由

使用 @page 指令,执行以下操作:

  • 指定页面的自定义路由。 例如,使用 @page "/Some/Other/Path" 将“关于”页面的路由设置为 /Some/Other/Path
  • 将段追加到页面的默认路由。 例如,可使用 @page "item" 将“item”段添加到页面的默认路由。
  • 将参数追加到页面的默认路由。 例如,@page "{id}" 页面需要 ID 参数 id

支持开头处以波形符 (~) 指定的相对于根目录的路径。 例如,@page "~/Some/Other/Path"@page "/Some/Other/Path" 相同。

如果你不喜欢 URL 中的查询字符串 ?handler=JoinList,请更改路由,将处理程序名称放在 URL 的路径部分。 可以通过在 @page 指令后面添加使用双引号括起来的路由模板来自定义路由。

@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div>Name: <input asp-for="Customer.Name" /></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>

使用前面的代码时,提交到 OnPostJoinListAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH/JoinList。 提交到 OnPostJoinListUCAsync 的 URL 路径为 https://localhost:5001/Customers/CreateFATH/JoinListUC

handler 前面的 ? 表示路由参数为可选。

高级配置和设置

大多数应用不需要以下部分中的配置和设置。

要配置高级选项,请使用 AddRazorPages 重载,该重载配置 RazorPagesOptions

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}

使用 RazorPagesOptions 设置页面的根目录,或者为页面添加应用程序模型约定。 有关约定的详细信息,请参阅 Razor Pages 授权约定.

若要预编译视图,请参阅 Razor 视图编译

指定 Razor Pages 位于内容根目录中

默认情况下,Razor Pages 位于 /Pages 目录的根位置。 添加 WithRazorPagesAtContentRoot 以指定 Razor Pages 位于应用的内容根 (ContentRootPath):

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}

指定 Razor Pages 位于自定义根目录中

添加 WithRazorPagesRoot,以指定 Razor Pages 位于应用中自定义根目录位置(提供相对路径):

public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}

其他资源