作者:Rick Anderson 和 Ryan Nowak
翻译:陈广
时间:2018-2-5
Razor页面是ASP.NET Core MVC中的新功能,它在以页面为中心场景中编写代码变得更为简单及高效。 本文提供Razor页面的介绍,并非详细教程,如果你发现某些部分难以理解,请参考:Getting started with Razor Pages。
安装ASP.NET Core 2.0.0或之后版本。 如果你使用的是Visual Studio,请安装Visual Studio 2017的15.3或之后的版本,并安装以下功能:
dotnet new razor
,从Visual Studio for Mac打开生成的*.csproj*文件dotnet new razor
dotnet new razor
##Razor页面
Razor页面是在Startup.cs中启用的:public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
考虑以下基本页面:
@page
<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>
上面的代码看起来很象Razor视图文件。唯一不同的是@page
指令。@page
将文件变成一个MVC动作 - 这意味着它直接处理请求,而不需要经过controller(控制器)。@page
必须是页面的第一个Razor指令。@page
影响着其它Razor的构造行为。
以下两个文件显示了使用了PageModel
类的类似页面。
Pages/Index2.cshtml文件:
@page
@using RazorPagesIntro.Pages
@model IndexModel2
<h2>Separate page model</h2>
<p>
@Model.Message
</p>
"code-behind" 文件Pages/Index2.cshtml.cs :
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace RazorPagesIntro.Pages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";
public void OnGet()
{
Message += $" Server time is { DateTime.Now }";
}
}
}
按照惯例,PageModel
类文件的名称和Razor页面文件相同,并带.cs后缀。例如之前Razor页面名称为Pages/Index2.cshtml。PageModel
类所在文件名称则为Pages/Index2.cshtml.cs。
页面所关联的url路径取决于页面在文件系统中的位置。下表显示了Razor页面路径所对应的URL:
文件名及路径 | 对应的URL |
---|---|
/Pages/Index.cshtml | / 或 /Index |
/Pages/Contact.cshtml | /Contact |
/Pages/Store/Contact.cshtml | /Store/Contact |
/Pages/Store/Index.cshtml | /Store 或 /Store/Index |
注意:
Index
作为默认页Razor页面的特点是在web浏览器中使用公共模板变得更为容易。Model binding(模型绑定), Tag Helpers, 和HTML helpers都在一个Razor页面类工作并定义属性。假设一个页面为Contact
模型实现了一个基本的“contact us”窗体:
本文例子中的DbContext
在Startup.cs类中初始化
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;
namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("name"));
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
}
data model(数据模型):
using System.ComponentModel.DataAnnotations;
namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
}
}
db context
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Customer> Customers { get; set; }
}
}
Pages/Create.cshtml视图文件:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<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" />
</form>
</body>
</html>
视图的code-behind文件Pages/Create.cshtml.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}
根据惯例,PageModel
类被<PageName>Model
调用,并在页面中处于同一命名空间。
PageModel
类允许将页逻辑与其表现分离。它定义了页面操作者,用于处理发送至页面和渲染页面数据的请求。这种分离允许你通过依赖注入独立管理页面和进行单元测试。
引页面拥有一个OnPostAsync
处理方法,在Post
请求时运行(当用户提交表单)。你可以为任何HTTP动词添加处理方法。最常用的处理有:
OnGet
用于初始化页面所需的状态。OnGet示例。OnPost
用于处理表单提交。Async
命名后缀是可选的,但通常由异步函数约定使用。上面例子中的OnPostAsync
代码看上去和在一个controller(控制器)中所写的类似。上面的代码是典型的Razor页面。大多数的MVC原语如model binding、validation和动作结果是共享的。
OnPostAsync
基本流程:
检查验证错误。
当数据成功进入,OnPostAsync
处理方法调用RedirectToPage
helper方法来返回一个RedirectToPageResult
实例。RedirectToPage
是一个新的action结果,这点和RedirectToAction
或RedirectToRoute
类似,由它是由页面定制的。在上例中,它重定向到根索引页(/Index)。RedirectToPage
是会在URL generation for Pages这一节详细描述。
当提交的表单有验证错误时(已经提交至服务器),OnPostAsync
处理方法调用page
的helper方法。page
返回PageResult
实例。返回页类似于controllers返回的view
中的操作。PageResult
是处理方法返回的默认类型。返回void的处理方法则渲染页面。
Customerr
属性使用[BindProperty]
特性来加入model binding(模型绑定)。
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
Razor页面默认情况下仅为non-GET动词绑定属性。绑定属性可减少代码数量。可通过使用相同属性来渲染表示域(<input asp-for="Customer.Name" />
)并接收输入。
home页面(Index.cshtml):
@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>
<a asp-page="./Create">Create</a>
</form>
code behind文件index.cshtml.cs文件:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
public IndexModel(AppDbContext db)
{
_db = db;
}
public IList<Customer> Customers { get; private set; }
public async Task OnGetAsync()
{
Customers = await _db.Customers.AsNoTracking().ToListAsync();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
}
}
index.cshtml文件包含以下标记,为每个联系人创建一个edit链接:
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
Anchor Tag Helper使用asp-route-{value}
属性来生成到Edit页面的链接。这个链接包含带有联系人ID的路由数据。例如,http://localhost:5000/Edit/1
。
*Pages/Edit.cshtml/*文件:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Edit Customer";
}
<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(未找到)错误。
Pages/Edit.cshtml.cs文件:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
public EditModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Customer = await _db.Customers.FindAsync(id);
if (Customer == null)
{
return RedirectToPage("/Index");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Attach(Customer).State = EntityState.Modified;
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}
return RedirectToPage("/Index");
}
}
}
Index.cshtml文件还包含为每个客户联系创建一个删除按钮的标记:
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
当删除按钮在HTML中渲染时,它的格式包含以下参数:
asp-route-id
属性指定客户联系ID。asp-page-handler
属性指定handler
。
下例为联系ID为1
的客户呈现一个删除按钮:<button type="submit" formaction="/?id=1&handler=delete">delete</button>
当点击按钮,表单POST
请求被发送至服务器。依照惯例,处理程序的名字是依照OnPost[handler]Async
结构中的handler
参数值进行选择的。
因为本例中的handler
为delete
,OnPostDeleteAsync
处理方法用于处理POST
请求。如果asp-page-handler
设置为不同的值,比如remove
,将会选择名字为OnPostRemoveAsync
的页面处理方法。
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
OnPostDeleteAsync
方法:
id
。FindAsync
为在数据库查询客户联系人。RedirectToPage
来重定向到根索引页(/Index
)。你无需为防伪验证(antiforgery validation)编写任何代码,防伪令牌的产生和验证自动包含在Razor页面中。
页面使用Razor视图引擎的所有功能。Layouts、partials、templates、Tag Helpers、_ViewStart.cshtml、_ViewImports.cshtml和传统Razor页面具有相同的工作方式。
让我们利用其中的一些特性来整理这个页面。 在Pages/_Layout.cshtml中添加一个layout(布局)页:
<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
<a asp-page="/Index">Home</a>
@RenderBody()
<a asp-page="/Customers/Create">Create</a> <br />
</body>
</html>
Layout:
Layout属性在Pages/_ViewStart.cshtml中设置:
@{
Layout = "_Layout";
}
注意:layout(布局)在Pages文件夹中。页面从当前页的同一文件夹开始按层次查找其它视图(layouts,templates,partials),一个Pages目录下的layout可在此目录下的任意Raxor页面中使用。
我们推荐不要将layout文件放入Views/Shared文件夹。Views/Shared是一种MVC视图模板。Razor页面意味着目录层次,而不是路径约定。
视图从Pages目录所包含的Razor页面搜索而来,MVC和传统Razor视图所使用的layouts、templates、partials就可以工作了。
添加一个Pages/_ViewImportscshtml文件:
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@namespace
在稍后解释。@addTagHelper
指令将内置的Tag Helpers引入到Pages目录中的所有页面。
当@namespace
指令在页面中被显示使用:
@page
@namespace RazorPagesIntro.Pages.Customers
@model NameSpaceModel
<h2>Name space</h2>
<p>
@Model.Message
</p>
该指令设置页面的命名空间。@model
指令不需要包含命名空间。
当@namespace
指令包含在*_ViewImports.cshtml时,它所指定的命名空间会为页面生成命名空间前缀。生成的命名空间后缀部分是包含_ViewImports.cshtml*的目录和包含页面的目录之间的点分隔相对路径。
例如,code behind文件Pages/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
为Razor页面Pages/Customers/Edit.cshtml生成的命名空间和code behind文件一样。@namespace
指令被设计为如C#类一样加入项目,并且在没有为code behind文件添加一个@using
指令时页面生成代码也可以工作。
注意:@namespace
也可以工作于传统Razor视图。
原本的Pages/Create.cshtml视图文件:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<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" />
</form>
</body>
</html>
更新后的Pages/Create.cshtml视图文件:
@page
@model CreateModel
<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" />
</form>
</body>
</html>
Razor Pages starter project包含的Pages/_ValidationScriptsPartial.cshtml文件用于连接客户端验证。
之前的Create
页面使用RedirectToPage
:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
应用程序的文件/目录结构如下:
Pages/Customers/Crearte.cshtml和Pages/Customers/Edit.cshtml页面在成功后重定向到Pages/Index.cshtml。字符串/Index
可被用于生成到Pages/Index.cshtml页面的URL。例如:
Url.Page("/Index", ...)
<a asp-page="/Index">My Index Page</a>
RedirectToPage("/Index")
页面名称是相对于/Pages根目录的路径(包含一个引导符/
,如/Index
)。上述的URL生成示例比仅仅硬编码URL要丰富得多。URL生成使用路由,可以根据在目标路径中定义路由的方式生成和编码参数。页的URL生成支持相对名称。下表展示了在Pages/Customers/Create.cshtml中使用不同的RedirerctToPage
参数时,选中的是哪个Index页面。
RedirectToPage(x) | Page |
---|---|
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
的参数结合当前页面路径来计算目标页面的名称。
当建一个复杂结构的站点时,相对名称链接非常有用。如果使用相对名称一个文件夹中的页之间链接,当重命名该文件夹时,所有链接仍然有效(因为它们没有包括文件夹名称)。
ASP.NET Core在controller(控制器)中曝露了TempData属性。此属性在读取数据之前存储数据。Keep
和Peek
方法可用于在数据不被删除的情况下进行检查。当不止一个请求需要数据时,Tempdata
对于重定向非常有用。
[TempData]
是ASP.NET Core 2.0的新特性,它支持在Controllers和pages中使用。
以下代码使用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>
code-behind文件Pages/Customers/Index.cshtml.cs中给Message
属性赋予了[TempData]
特性。
[TempData]
public string Message { get; set; }
参数TempData获取更多信息。
下面的页面使用Tag Helper(标签助手)为两个页面处理程序生成标记:
@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>
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
</form>
</body>
</html>
之前的例子中的表单有两个提交按钮,每个都使用FormActionTagHelper
来提交至不同的URL。asp-page-handler
属性与asp-page
相匹配。asp-page-handler
生成提交至每一个由页面定义的处理方法的URL。没有指定asp-page
,因为示例链接至当前页。
code-behind文件:
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?.ToUpper();
return await OnPostJoinListAsync();
}
}
}
上述代码使用了named handler methods(命名处理方法)。命名处理方法的创建规则是将处理名称放至On<HTTP动词>
之后,Async
之前(如果存在)。上例中的页面方法为OnPostJoinListAsync和OnPostJoinListUCAsync。将OnPost和Async移除,处理名称为JoinList
和JoinListUC
。
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
上述代码提交至OnPostJoinListAsync
的URL路径为http://localhost:5000/Customers/CreateFATH?handler=JoinList
。提交至OnPostJoinListUCAsync
的URL路径为http://localhost:5000/Customers/CreateFATH?handler=JoinListUC
。
如果你不喜欢在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>
上述路由将处理名称放置在URL路径而不是查询字符串中。跟随在handler
后面的?
号意味着路由参数是可选的。
你可以使用@page
向页面路由添加其它段和参数,不管是什么,都会附加到页面的默认路由中。不支持使用绝对路径或虚拟路径来更改页面的路由(例如“~/Some/Other/PATH”
)。
如果要配置高级选项,请在MVC构建器上使用扩展方法AddRazorPagesOptions
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/MyPages";
options.Conventions.AuthorizeFolder("/MyPages/Admin");
});
}
现在,你可以使用RazorPagesOPtions
来为页面设置路由根目录,或为页面添加应用程序模型约定了。将来我们会以这种方式启用更多的扩展性。
预编译视图,请参考Razor View compilation 下载视图示例代码
默认情况下,Razor页面位于*/Pages*目录中。通过在AddMVC中添加WithRazorPagesAtContentRoot来将你的Razor页面指定到应用程序的内容根(content root ContentRootPath)中。
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesAtContentRoot();
在AddMvc中添加WithRazorPagesRoot可以将你的Razor页面指定到应用程序的自定义根目录中(提供相对路径):
services.AddMvc()
.AddRazorPagesOptions(options =>
{
...
})
.WithRazorPagesRoot("/path/to/razor/pages");
;