内容来自书籍:
Pro ASP.NET Core 6
Develop Cloud-Ready Web Applications Using MVC, Blazor, and Razor Pages (Ninth Edition)Author: Adam Freeman
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
并非 Web 应用程序的每个特性都需要 MVC 框架的灵活性。对于许多特性,单个操作方法将用于处理范围广泛的请求,所有这些请求都使用相同的视图进行处理。Razor 页面提供了一种更加集中的方法,它将标记和 C # 代码结合在一起,牺牲了灵活性来集中注意力。
builder.Services.AddRazorPages();
app.MapRazorPages();
AddRazorPages 方法设置使用 Razor 页面所需的服务,而/nMapRazorPages 方法创建将 URL 与页匹配的路由配置
@page @model IndexModel @using Microsoft.AspNetCore.Mvc.RazorPages @using WebApp.Models; <!DOCTYPE html> <html> <head> <link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <div class="bg-primary text-white text-center m-2 p-2">@Model.Product?.Name</div> </body> </html> @functions { public class IndexModel: PageModel { private DataContext context; public Product? Product { get; set; } public IndexModel(DataContext ctx) { context = ctx; } public async Task OnGetAsync(long id = 1) { Product = await context.Products.FindAsync(id); } } }
@ Page 指令必须是 Razor 页面中的第一个指令,以确保该文件不会被误认为是与控制器关联的视图。但最重要的区别在于,@function 指令用于定义支持同一文件中 Razor 内容的 C # 代码。
Razor 网页的 URL 路由是基于文件名和相对于Pages文件夹的位置。Razor 页面位于 Pages 文件夹中一个名为 Index.cshtml 的文件中,这意味着它将处理/index 的请求
在 Razor 页面中,@model 指令用于选择页面模型类,而不是标识/n行动方法所提供的对象。
页面模型在@function 指令中定义,并从 PageModel 类派生
当 Razor 页面被选择来处理一个 HTTP 请求时,页面模型类的一个新实例就会被创建,并且依赖注入被用来解决使用构造函数参数声明的任何依赖关系
创建页面模型对象后,调用一个处理程序方法。处理程序方法的名称是 On,后面跟着请求的 HTTP 方法,以便在选择 Razor Page 来处理 HTTP GET 请求时调用 OnGet 方法。处理程序方法可以是异步的,在这种情况下,GET 请求将调用 OnGetAsync 方法,这是由 IndexModel 类实现的方法。
Razor 页面使用相同的 HTML 片段和代码表达式组合来生成内容,这定义了呈现给用户的视图。页面模型的方法和属性可以通过@Model 表达式在 Razor 页面中访问
@ Model 表达式返回一个 IndexModel 对象,该表达式读取 Product 属性返回的对象的 Name 属性。空条件运算符(?)不是 Model 属性所必需的,因为它将始终被分配页模型类的实例,并且不能为空。然而,页面模型类定义的属性可以是 null
在幕后,Razor 页面被转换成 c # 类,就像普通的 Razor 视图一样
Razor 页面依赖于 CSHTML 文件的位置进行路由,因此对 http://localhost:5000/index 的请求由 Pages/index.cshtml 文件处理。为应用程序添加更复杂的 URL 结构是通过添加文件夹来完成的,这些文件夹的名称表示您希望支持的 URL 中的段
MapRazorPages 方法为 Index.cshtml Razor 页面的默认 URL 设置路由,遵循 MVC 框架使用的类似约定。因此,添加到项目中的第一个 Razor 页通常称为 Index.cshtml。然而,当应用程序将 Razor 页面和 MVC 框架混合在一起时,由 Razor 页面定义的默认路由优先,因为它是以较低的顺序创建的。这意味着请求 http://localhost:5000由示例项目中的 Index.cshtml Razor Page 处理,而不是 Home 控制器上的 Index 操作
如果你想让 MVC 框架来处理默认的 URL,那么你可以改变分配给 Razor 网页路由的顺序
app.MapRazorPages().Add(b => ((RouteEndpointBuilder)b).Order = 2);
使用文件夹和文件结构执行路由意味着没有段变量供模型绑定过程使用。相反,请求处理程序方法的值是从 URL 查询字符串获取的
@page 指令可以与路由模式一起使用,该模式允许定义段变量
@page "{id:long?}"
@page 指令还可以用来覆盖基于文件的 Razor 页路由约定
@page "/lists/suppliers"
使用@page 指令替换 Razor 页面的默认基于文件的路由。如果您想为一个页面定义多个路由,那么可以将配置语句添加到 Program.cs 文件中
builder.Services.Configure<RazorPagesOptions>(opts => { opts.Conventions.AddPageRoute("/Index", "/extra/page/{id:long?}"); });
选项模式用于使用 RazorpageOptions 类为 Razor 页添加其他路由。对 Convention 属性调用 AddPageRoute 扩展方法以为页添加路由。第一个参数是页的路径,没有文件扩展名,相对于 Pages 文件夹。第二个参数是要添加到路由配置中的 URL 模式
在同一个文件中定义代码和标记非常方便,但对于更复杂的应用程序来说,可能难以管理。Razor 页面也可以被分割成独立的视图和代码文件
命名 Razor 代码隐藏文件的约定是将. cs 文件扩展名附加到/n视图文件的名称
和MVC一样,可以创建_ViewImports.cshtml 文件来统一将所有会用到的namespace导入
但是这次是在Pages文件夹下创建
虽然这并不明显,但是 Razor 的 Page 处理程序方法使用相同的 IActionResult 接口来控制它们生成的响应。为了使页面模型类更容易开发,处理程序方法具有显示页面视图部分的隐含结果
public async Task<IActionResult> OnGetAsync(long id = 1) { Product = await context.Products.FindAsync(id); return Page(); }
Page 方法继承自 PageModel 类并创建一个 PageResult 对象,它告诉框架呈现页面的视图部分。与 MVC 操作方法中使用的 View 方法不同,Razor 的 Pages Page 方法不接受参数,并且总是呈现页面中选择用于处理请求的视图部分
@page "{id:long}" @model EditorModel <!DOCTYPE html> <html> <head> <link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <div class="bg-primary text-white text-center m-2 p-2">Editor</div> <div class="m-2"> <table class="table table-sm table-striped table-bordered"> <tbody> <tr><th>Name</th><td>@Model.Product?.Name</td></tr> <tr><th>Price</th><td>@Model.Product?.Price</td></tr> </tbody> </table> <form method="post"> @Html.AntiForgeryToken() <div class="form-group"> <label>Price</label> <input name="price" class="form-control" value="@Model.Product?.Price" /> </div> <button class="btn btn-primary mt-2" type="submit">Submit</button> </form> </div> </body> </html>
Razor 页面视图中的元素创建了一个简单的 HTML 表单,向用户显示一个包含 Product 对象 Price 属性值的输入元素。Form 元素的定义中没有 action 属性,这意味着当用户单击 Submit 按钮时,浏览器将向 Razor 页面的 URL 发送一个 POST 请求。
namespace WebApp.Pages { public class EditorModel : PageModel { private DataContext context; public Product? Product { get; set; } public EditorModel(DataContext ctx) { context = ctx; } public async Task OnGetAsync(long id) { Product = await context.Products.FindAsync(id); } public async Task<IActionResult> OnPostAsync(long id, decimal price) { Product? p = await context.Products.FindAsync(id); if (p != null) { p.Price = price; } await context.SaveChangesAsync(); return RedirectToPage(); } } }
Page 模型类定义了两个处理程序方法,方法的名称告诉 Razor Pages 框架每个处理哪个 HTTP 方法。OnGetAsync 方法用于处理 GET 请求,方法是定位 Product,其详细信息由视图显示
OnPostAsync 方法用于处理 POST 请求,这些请求将在用户提交 HTML 表单时由浏览器发送。OnPostAsync 方法的参数是从请求获得的,因此 id 值是从 URL 路由获得的,price 值是从表单获得的
Razor 页面的视图部分使用与控制器相同的语法和功能。Razor 页面可以使用各种表达式和特性,例如会话、临时数据和布局。除了使用@page 指令和页面模型类之外,唯一的区别是在配置布局和部分视图等特性方面存在一定数量的重复
如果一个 Razor 页面只是简单地向用户显示数据,那么结果可能是一个页面模型类,它只是声明一个构造函数依赖项来设置视图中使用的属性
@page @inject DataContext context; <h5 class="bg-primary text-white text-center m-2 p-2">Categories</h5> <ul class="list-group m-2"> @foreach (Category c in context.Categories) { <li class="list-group-item">@c.Name</li> } </ul>
这个例子中的页面模型不会转换数据、执行计算或者做任何事情,只是让视图通过依赖注入访问数据。为了避免这种模式(页面模型类仅用于访问服务) ,可以使用@inject 指令在视图中获取服务,而不需要页面模型
Partial views 用于创建视图中需要的可重用标记,从而避免在应用程序的多个位置复制相同的内容。Partial views 是一个非常有用的特性,但是它们只包含 HTML 和 Razor 指令的片段,并且它们操作的数据是从父视图接收的。如果需要显示不同的数据,则会遇到问题。您可以直接从部分视图访问所需的数据,但这会破坏开发模型,并产生难以理解和维护的应用程序。或者,您可以扩展应用程序使用的视图模型,以便它包含您需要的数据,但是这意味着您必须更改每个操作方法,这使得很难隔离操作方法的功能,以便进行有效的维护和测试。
这就是视图组件的用武之地。View 组件是一个 C # 类,它独立于 action 方法或 Razor Page,提供一个包含所需数据的部分视图。在这方面,视图组件可以被看作是一个专门的操作或页面,但是它只用于提供一个包含数据的部分视图; 它不能接收 HTTP 请求,并且它提供的内容将始终包含在父视图中。
视图组件是名称以 ViewComponent 结尾并定义 Invoke 或 InvokeAsync 方法的任何类,或者是从 ViewComponent 基类派生的任何类,或者是用 ViewComponent 属性修饰的任何类
视图组件可以在项目中的任何地方定义,但约定是将它们分组到名为 Component 的文件夹中
public class CitySummary : ViewComponent { private CitiesData data; public CitySummary(CitiesData cdata) { data = cdata; } public string Invoke() { return $"{data.Cities.Count()} cities, " + $"{data.Cities.Sum(c => c.Population)} people"; } }
有两种方法使用View components,第一种是Component 属性,添加到Razor或者view中,然后这个属性会返回IViewComponentHelper接口的实例,它提供了一个InvokeAsync方法
namespace WebApp.Components { public class CitySummary : ViewComponent { private CitiesData data; public CitySummary(CitiesData cdata) { data = cdata; } public string Invoke() { return $"{data.Cities.Count()} cities, " + $"{data.Cities.Sum(c => c.Population)} people"; } } }
@section Summary { <div class="bg-info text-white m-2 p-2"> @await Component.InvokeAsync("CitySummary") </div> }
tag helpers 其实就是用C#代码来自定义并生成HTML
视图组件可以使用一个实现为 Tag Helper 的 HTML 元素来应用View Components
<vc:city-summary />
类名中的每个大写单词都被转换为小写,并用连字符分隔,这样 CitySummary 就变成了 city-Summary,并且 CitySummary 视图组件使用 vc: city-Summary 元素应用。
<vc:city-summary />
将简单的字符串值插入视图或页面的能力并不特别有用,但幸运的是,视图组件能够提供更多功能。更复杂的效果可以通过 Invoke 或 InvokeAsync 方法返回一个实现 iViewComponent/Result 接口的对象来实现。
最有用的响应是叫做 ViewViewComponentResult 的对象,它告诉 Razor 呈现一个部分视图,并将结果包含在父视图中。ViewComponent 基类提供用于创建 ViewViewComponent 的 View 方法
public IViewComponentResult Invoke() { return View(new CityViewModel { Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population) }); }
在这之后,如果请求这个部分,会出出错,因为这个View没有找到对应的HTML,或者说是cshtml文件
Razor会查找一个叫做Default.cshtml的文件,当我们的View方法没有指定视图的名称,如果这个component是在controller使用的,那么查找的路径是:
/Views/[controller]/Components/[viewcomponent]/Default.cshtml /Views/Shared/Components/[viewcomponent]/Default.cshtml /Pages/Shared/Components/[viewcomponent]/Default.cshtml
如果是在Razor Page中使用的component,那么路径是:
/Pages/Components/[viewcomponent]/Default.cshtml /Pages/Shared/Components/[viewcomponent]/Default.cshtml /Views/Shared/Components/[viewcomponent]/Default.cshtml
上面的两个路径中,可以看到,controller和Razor Page 的查找路径有部分重复了,所以如果有component同时提供给两者使用,那么不需要重复这个component
public IViewComponentResult Invoke() { return Content("This is a <h3><i>string</i></h3>"); }
Content只能返回string,如果string有HTML格式,页面是不会显示的,如果需要将HTML显示出来,应该使用HtmlContentViewComponentResult
public IViewComponentResult Invoke() { return new HtmlContentViewComponentResult( new HtmlString("This is a <h3><i>string</i></h3>")); }
有关当前请求和父视图的详细信息通过 ViewComponent 基类定义的属性提供给视图组件
可以以任何方式使用上下文数据来帮助视图组件完成工作,包括改变数据的选择方式或呈现不同的内容或视图。很难设计一个在视图组件中使用上下文数据的代表性示例,因为它解决的问题是特定于每个项目的
父组件向view component传递额外的数据
<vc:city-summary theme-name=”secondary” />
component可以在方法中接收这个参数
public IViewComponentResult Invoke(string themeName) { ViewBag.Theme = themeName; return View(new CityViewModel { Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population) }); }
然后component再将这个数据通过viewBag传递到view中
<table class="table table-sm table-bordered text-white bg-@ViewBag.Theme">
public IViewComponentResult Invoke(string themeName = "success") { ViewBag.Theme = themeName; return View(new CityViewModel { Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population) }); }
如果Invoke方法内部依赖的是异步的方法获取数据,那么可以切换为InvokeAsync方法
namespace WebApp.Components { public class PageSize : ViewComponent { public async Task<IViewComponentResult> InvokeAsync() { HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync("http://apress.com"); return View(response.Content.Headers.ContentLength); } } }
用于结帐并完成购买。在这种情况下,您可以创建一个类,它既是一个视图组件,也是一个控制器或 Razor 页面。
namespace WebApp.Pages { [ViewComponent(Name = "CitiesPageHybrid")] public class CitiesModel : PageModel { public CitiesModel(CitiesData cdata) { Data = cdata; } public CitiesData? Data { get; set; } [ViewComponentContext] public ViewComponentContext Context { get; set; } = new(); public IViewComponentResult Invoke() { return new ViewViewComponentResult() { ViewData = new ViewDataDictionary<CityViewModel>( Context.ViewData, new CityViewModel { Cities = Data?.Cities.Count(), Population = Data?.Cities.Sum(c => c.Population) }) }; } } }
此页面模型类使用 ViewComponent 属性进行修饰,该属性允许将其用作视图组件。Name 参数指定将应用视图组件的名称。由于页面模型不能从 ViewComponent 基类继承,所以一个属性的类型为 ViewComponentContext的属性被赋予了 ViewComponentContext属性,这表明在调用 Invoke 或 InvokeAsync 方法之前。View 方法不可用,所以我必须创建一个 ViewViewComponentResult 对象,它依赖于通过修饰属性接收到的上下文对象
namespace WebApp.Controllers { [ViewComponent(Name = "CitiesControllerHybrid")] public class CitiesController : Controller { private CitiesData data; public CitiesController(CitiesData cdata) { data = cdata; } public IActionResult Index() { return View(data.Cities); } public IViewComponentResult Invoke() { return new ViewViewComponentResult() { ViewData = new ViewDataDictionary<CityViewModel>( ViewData, new CityViewModel { Cities = data.Cities.Count(), Population = data.Cities.Sum(c => c.Population) }) }; } } }
控制器实例化方式的一个特殊之处意味着不需要用 ViewComponent entContext 属性修饰的属性,从 Controller 基类继承的 ViewData 属性可用于创建视图组件结果。