从MVC到Razor页面
介绍
使用ASP.NET Core 2,Microsoft为我们提供了创建Web应用程序的MVC(Model-View-Controller)方法的全新替代方案。微软将其命名为“剃刀页”,尽管采用不同的方法,但在某些方面仍然很熟悉。
本文将展示一个场景,其中Razor Pages用于生产一个小型电子商务网站,并在Facebook的React / ReactJS.NET帮助下,为其提供基于JavaScript代码的视图呈现/绑定引擎。
引用:免责声明:本文主要关注剃须刀页面,并补充了我几个月前发布的以前的 React Shop - 微型电子商务文章。如果您想了解有关在ASP.NET应用程序中使用React和React JS.NET的更多信息,请参阅其他文章。
安装
为了在附带的源代码中运行应用程序,请安装以下内容:
- .NET Core 2.0.0 SDK或更高版本。
- Visual Studio 2017版本15.3或更高版本与ASP.NET和Web开发工作负载。
- Microsoft®SQLServer®2014 Express或更高版本。
- 下载本文顶部的项目源代码。
背景
如果您从过去十年开始关注ASP.NET开发,您可能已经注意到,Microsoft曾经和微软一起提出了一种新的开发工具或框架的创新 - 甚至是新的设计模式。其中一些已被证明是非常强大可靠的,如ASP.NET MVC和Razor视图引擎,而其他还没有通过时间的考验,被放弃,如AJAX Control Toolkit。
但是,自从2016年发布ASP.NET Core 1.0以来,很多事情都发生了变化。微软重新编写了ASP.NET作为一个开放源代码的GitHub托管项目,并首次向其Web开发框架介绍了多平台。
自从第一个版本开始,ASP.NET Core为ASP.NET 3.0提供了标准的Web开发框架,这个ASP.NET MVC是基于Model-View-Controller设计模式的。
MVC设计模式是1979年在神话中的Xerox Palo Alto研究中心(XPARC)开发的,但是在几个Web框架开始采用之后才开始出现:2002年的Spring为Java,2005年为Django和Ruby on Rails以及ASP。 NET在2009年。
而ASP.NET MVC是我们社区的一大热门。在此之前,我们必须处理从ASP.NET Web窗体中出现的问题,例如更新面板,增加ViewState,Postbacks以及相当复杂的页面事件管理。使用ASP.NET MVC,我们被教导在开发我们的Web应用程序时说出分离问题的语言:数据属于模型,表示逻辑仅适用于视图,每个请求必须由Controller操作处理,而这些操作又决定哪个视图将与其适当的模型一起呈现。没有更多的需要从程序员隐藏网络的无国籍性质。
但ASP.NET MVC虽然给我们带来了很多好处,但有时仍然遇到了一些批评。虽然用户 - 当然是Web开发人员 - 将Web应用程序基本看作一组“网页”,ASP.NET MVC没有一个清晰的Web页面概念。相反,在ASP.NET MVC项目中,每个组件通常都有自己的文件,并且属于不同的文件夹,具体取决于它在MVC框架内的作用。
现在,暂停一会,想像你如何组织你的电脑文件夹。想象一下,您有不同的工作要做,涉及不同的文件,而且您的计算机文件夹不是按主题或工作或主题组织,而是由文件类型组织。并且想像您正在执行多个不同的作业,而不是分配每个作业的文件,您将每个作业的文件分散在不同的文件夹中,如下所示:
这会是个好主意吗?
类似地,我们可以从下面的图像中看出,典型的MVC项目如何将一个文件中的单个页面的组件分散在许多文件和文件夹中:
所以在MVC中没有一个“网页”文件。向技术新手的人解释一下有点尴尬。
那么微软有人认为同样的事情可以用不同的方式完成。
介绍剃刀页面
如果您使用MVC应用程序,然后将您的View称为页面(例如,在 Index.cshtml文件中),并且您不仅集中了模型数据,还集中了与该页面相关的服务器端代码(以前驻留在你的控制器)里面一个专门的页面(在一个Index.cshtml.cs文件里面 ) - 你现在称之为页面模型?
如果您已经在本地移动应用程序中工作,那么在Model-View-ViewModel(MVVM)模式中可能已经看到类似的内容。
剃刀页面中的页面和页面模型
这就是剃刀页出生!ASP.NET Core 2.0 - 更准确地说,Visual Studio 2017 - 使用Razor Page作为默认内容引入了一个新的Web应用程序模板。
那么你可能会认为“但等一下......这看起来有点太熟悉了”。因为现在你有一个页面,一个执行服务器端功能的类。页面模型不是与文件中的旧代码相同吗?Web窗体不是一遍又一遍吗?
不,让我解释一下为什么Razor Pages不是网络形式的复兴。首先,我们必须意识到,一个Razor页面与MVC设计模式并没有很大的不同。过去,通过Web窗体,您通常会将业务规则,用户界面和数据层混合在一起。在ASP.NET Web窗体中,您已经拥有了以简单,性能和带宽为代价的事件处理的人造管道,另一方面,所有MVC组件在Razor Pages中都可以看到。它们只是位于不同的类/文件/文件夹中,以方便开发页面。
有些人对于剃刀页面的另一个误解是关于它主要是初级开发人员或较不复杂的应用程序。尽管新手开发人员可以轻松了解Razor Pages而不是MVC网络应用程序,但仍然可以构建复杂的应用程序,我们将在本文附带的源代码中看到。
创建新的Razor Pages网页应用程式后,您也可能会以某种方式“丢失”使用MVC项目的能力。但幸运的是,这不是真的。记住,这两个模板(MVC和Razor Pages)都依赖于相同的ASP.NET Core MVC框架。所以,例如,您可以创建一个新的Razor Page项目,然后创建MVC文件夹(控制器,视图等)和所需的文件,以便在同一个项目中使用MVC控制器和视图以及Razor页面!
在Razor Pages项目中,在项目根目录下创建一个新的Controllers文件夹,然后创建一个TestController
类。现在我们实现了Index
返回纯文本的操作:
public class TestController : Controller
{
public IActionResult Index()
{
//Accessible through /Test
return Content("You can work with MVC Controllers and Views " +
"alongside Razor Pages in the same project!");
}
}
运行应用程序并在浏览器的地址栏中输入http:// localhost:XXXXX / test,我们得到:
不是很棒吗
组态
当您创建一个新的ASP.NET Core Razor Pages Web App时,这是您在Program.cs文件中获得的:
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Program.cs文件
现在,注意 .UseStartup<Startup>()
线。它告诉ASP.NET
Core指定要由Web主机使用的启动类。
该 启动类 也是自动创建当您选择剃刀页面模板。它配有 ConfigureServices方法,它为Web应用程序添加服务,以及配置HTTP请求管道的 Configure方法。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
}
Startup.cs文件
上面的代码显示了 调用MVC服务services.AddMvc()的 ConfigureServices方法; (请看Razor Pages如何 依赖于MVC框架?)此外, 由运行时调用Configure方法来设置 请求管道。 从上面的代码,我们看到 配置 方法:
- 指定错误页面配置
- 启用当前请求路径的静态文件服务
- 将MVC添加到Microsoft.AspNetCore.Builder.IApplicationBuilder请求执行管道
所以你有它。就像ASP.NET Core MVC应用程序一样,Razor Pages Web应用程序也使用 Startup 类来配置有关Web应用程序的所有内容。
新的剃刀页面Web应用程序
记得当我们说Razor Pages就像ASP.NET Core MVC做的不同吗?尽管与MVC不同,Razor
Pages仍然依赖于ASP.NET Core MVC Framework。使用Razor Pages模板创建新项目后,Visual Studio将通过Startup.cs
文件配置应用程序以启用ASP.NET
Core MVC框架,正如我们刚刚看到的那样。
该模板不仅可以配置新的Web应用程序以供MVC使用,还可以为示例应用程序创建Page文件夹和一组Razor页面和页面模型:
对于Razor Page Shop 网络应用程序,我需要3个不同的视图(页面):
- 产品目录: 用户将从目录中选择放入购物车的产品
- 购物车: 为采购订单选择的产品
- 订单详细信息:运送数据,客户信息和刚刚发货的订单的产品列表
所以我保持Index.cshtml 作为产品目录页面,并包含另外2个Razor页面页面: Cart.cshtml和CheckoutSuccess.cshtml,用于购物车和订单明细,因此:
2个新的剃刀页面:Cart.cshtml和CheckoutSuccess.cshtml。
页面文件夹
以前的图像显示了Pages 文件夹中每个视图(ahem,Razor Page)如何包含 。新的Pages文件夹和传统的MVC Web应用程序“Views”文件夹之间的区别超出了文件夹名称。事实上,由于我们在Razor Page web应用程序中没有控制器和操作的概念,所以 在页面文件夹内的cshtml文件的位置 定义了应该访问哪个url路由。例如:
- /Pages/Index.cshtml - > “/”或“/ Index”
- /Pages/Cart.cshtml - > “/ Cart”
- /Pages/CheckoutSuccess.cshtml - >“/ CheckoutSuccess”
同样,您可能希望在Pages文件夹中创建子文件夹,以创建更复杂的URL路由方案:
- /Pages/Products/WhatsNew.cshtml - > “/ Products / WhatsNew ”
- /Pages/Categories/Listing.cshtml - > “/ Categories / Listing ”
- /Pages/Admin/Dashboard.cshtml - >“/ Admin / Dashboard ”
剃刀页的解剖学
一见钟情,Razor页面看起来就像一个普通的ASP.NET MVC View文件。但剃刀页需要一个新的指令。每个剃须刀页面都必须以这个开头@page
directive
,它告诉ASP.NET Core将其视为Razor页面。下面的图片显示了一些关于典型剃须刀页面的更多细节。
@page - 将文件标识为剃须刀页面。没有它,该页面是简单的无法由ASP.NET核心。
@model - 就像在MVC应用程序中一样,定义了发起绑定数据的类,以及页面请求的Get / Post方法。
@using - 定义命名空间的常规指令。
@inject - 配置哪些接口实例应该注入到页面模型类中。
@ {} - Razor括号中的一段C#代码,在这种情况下用于定义页面标题。
<div ...> - 与启用Razor的C#代码一起提供的常规HTML代码。
Web开发简单
如何在“剃刀页面”应用程序中创建静态页面?假设您必须创建使用条款页面。
在Razor Pages之前,使用常规的MVC Web应用程序,您必须遵循一些步骤才能在Web应用程序中包含一个简单的静态页面:
- 添加控制器(Controllers / TermsOfUseController.cs)
- 添加一个Action方法(Index())
- 添加查看文件夹(/ Views / TermsOfUse)
- 添加视图(/Views/TermsOfUse/Index.cshtml)
现在,使用Razor页面,您的工作变得更加简单:
- 添加页面(Pages / TermsOfUse.cshtml)
现在你可能会想:怎么样的页面模型的使用条款上面提到的页面。由于页面只是静态页面,因此不需要任何页面模型,这是MVC Web应用程序的另一个优点。
页面模型类
不同于MVC视图,通常是绑定到自定义Model
或ViewModel
,一个典型的剃刀页将指定其Model
作为一个类从继承PageModel
类。看看下面的IndexModel
课程:
public class IndexModel : PageModel
{
public IList<ProductDTO> Products { get; set; }
readonly ICheckoutManager _checkoutManager;
public IndexModel(ICheckoutManager checkoutManager)
{
this._checkoutManager = checkoutManager;
}
public void OnGet()
{
Products = this._checkoutManager.GetProducts();
}
public async Task<IActionResult> OnPostAddAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
}
Index.cshtml.cs文件
上述页面模型通常放在.cshtml.cs文件中,有时被称为“代码后面”文件。但是,请不要误以为过去Web Web表单的臭名昭着的“代码隐藏”文件。他们有很少的共同点。
现在看看IndexPageModel
构造函数:
readonly ICheckoutManager _checkoutManager;
public IndexModel(ICheckoutManager checkoutManager)
{
this._checkoutManager = checkoutManager;
}
你可能想知道谁使用ICheckoutManager checkoutManager
参数调用这个构造函数。幸运的是,从ASP.NET
Core 1开始,该框架具有内置的依赖注入服务。
依赖注入的概念是某些类实例必须由框架自动创建,但是这样的类有一个依赖另一个接口实例的构造函数。所以,在第一个类(在这种情况下IndexModel
)实例化之前,框架应该创建任何依赖关系(在这种情况下是ICheckoutManager
接口),然后将它们传递给构造函数。
但是为了从接口创建实例,应该为框架提供有关哪些类实现这种接口的信息。您应该在Startup.cs文件中配置此依赖注入信息,更准确地说在ConfigureServices
方法中:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ICheckoutManager, CheckoutManager>();
.
.
.
在Startup.cs文件中配置依赖注入。
请注意,services.AddScoped<ICheckoutManager,
CheckoutManager>();
代码如何将CheckoutManager
类定义为在ICheckoutManager
需要新实例时应该创建的类型。这是通过该services.AddScoped
方法完成的。
说到配置,请注意我们使用的AddScoped
方法。ASP.NET
Core为我们提供了一些这些实例应该多久创建一些选项,以及它们应该用于依赖注入的次数。这被称为服务生命周期,并且这些寿命可以配置如下:
瞬态:每次请求时创建一个新的实例。
范围:每个请求创建一个新的实例。
Singleton:在第一次请求时创建一个新的实例,并且在每个后续请求中使用相同的实例。
除了Startup.cs
配置之外,您还应该@injection
在页面文件中包含一个指令,指示依赖注入服务应根据请求生成哪些接口:
@inject ICheckoutManager ICheckoutManager;
配置@inject指令
介绍页面处理程序
如果您习惯使用MVC模式,您可能会想知道如何使用Razor Pages替换MVC Action Methods。在MVC Web应用程序中,Controller是您应用程序中每个请求的入口点。在MVC模式中,控制器是可以响应应用程序中不同视图的许多操作的集合,它们可以方便地分组在一起,以共享过滤器和路由模板等资源。
Razor Pages现在具有页面处理程序,它是MVC控制器操作的替代。这些处理程序响应特定的HTTP动词,例如GET和POST。处理程序存在于Razor Pages中,名称约定为“on {HTTP Verb}”。
这意味着针对Razor页面的每个HTTP Get请求都位于“代码后面”文件OnGet
中的页面模型上。Index
例如,在Razor页面中,我们有一个OnGet
处理程序方法,用于实例化Bootstrap产品转盘中显示的产品列表:
public void OnGet()
{
Products = this._checkoutManager.GetProducts();
}
但是如果页面模型没有这样的OnGet方法,或者页面模型甚至不存在,该怎么办?幸运的是,在这种情况下,Razor Page无论如何也是无误的。这是一个方便的解决方案,避免了简单的剃刀页面的仪式和样板。
现在,如果要处理HTTP Post请求,例如由HTML <form>
元素提交的那些请求,例如:
<form method="post"> . . . <button type="submit" class="btn btn-link" name="SKU" value="@product.SKU"> <i class="fa fa-shopping-cart" aria-hidden="false"></i> Add to Cart </button> . . . </form>
那么你应该遵循“on {HTTP Verb}”的名称约定,并创建一个新的剃刀页面处理程序(方法)OnPostAsync
:
public async Task<IActionResult> OnPostAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
还有一种情况,您可能希望在Razor页面中为不同目的提交不同的HTTP POST请求。例如,您可能有3个按钮:一个用于添加,另一个用于更新,另一个用于删除操作。您应该如何在单个OnPostAsync
处理程序中容纳不同的POST请求?
幸运的是,在这种情况下,Razor Pages提供了一个方便的替代方法,涉及使用Tag Helpers。标签帮助者,如ASP.NET
Core 1.0中首次看到的,使服务器端代码能够在Razor文件中创建和呈现HTML元素。使用标签助手,您可以指定要在同一页面中使用的多个POST处理程序方法。例如,假设您要将方法名称更改OnPostAsync
为OnPostAddAsync
:
public async Task<IActionResult> OnPostAddAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
显然,以前的<form>
HTML将无法向新重命名的处理程序方法提交请求。但是,您可以将<form>
HTML元素更改为Tag
Helper,并使用asp-page-handler来为POST请求指定新的处理程序变体:
<form asp-page-handler="Add" . . . <button type="submit" class="btn btn-link" name="SKU" value="@product.SKU"> <i class="fa fa-shopping-cart" aria-hidden="false"></i> Add to Cart </button> . . . </form>
将ReactJS.NET集成到ASP.NET Core 2.0中
最后一次使用React和ASP.NET 4.x时,我按照React的“服务器端渲染”指南,ReactConfig
使用包含每个视图内容的.jsx文件配置静态类:
public static class ReactConfig
{
public static void Configure()
{
ReactSiteConfiguration.Configuration
.AddScript("~/Scripts/showdown.js")
.AddScript("~/Scripts/react-bootstrap.js")
.AddScript("~/Scripts/Components.jsx")
.AddScript("~/Scripts/Cart.jsx")
.AddScript("~/Scripts/CheckoutSuccess.jsx");
}
}
Old ReactConfig.cs配置文件
但是该教程是针对ASP.NET 4.x而进行的,并且在ASP.NET Core Web应用程序中不起作用。所以我不得不将服务器端渲染部分移植到ASP.NET
Core Startup.cs文件中,遵循新的React教程。所以我不得不将这些脚本包含Configure
在Startup
该类中,以便ReactJS.NET可以从.jsx文件中呈现服务器端的HTML内容。
app.UseReact(config =>
{
config
.AddScript("~/lib/react-bootstrap/react-bootstrap.min.js")
.AddScript("~/js/Components.jsx")
.AddScript("~/js/Cart.jsx")
.AddScript("~/js/CheckoutSuccess.jsx")
.SetUseDebugReact(true);
});
启动配置文件中的新配置方法
但是React.AspNet扩展还需要调用services.AddReact();
方法来注册ReactJS.NET所需的所有服务:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ICheckoutManager, CheckoutManager>();
services.AddReact();
.
.
.
}
应用页面
由于这个网络应用程序的页面从另一个MVC应用程序的视图移植到这个Razor Pages Web应用程序,并且在我的其他文章“ React Shop - A Tiny e-Commerce”中已经讨论过了,而且由于它们几乎保持不变,我选择不再解释一切。如果您想了解有关React-Bootstrap组件,React JS视图和.jsx文件以及ASP.NET应用程序的详细信息,请访问我的其他文章。
然而,这里是对应用页面的简单概述。
产品目录
产品目录页。
产品目录页面显示了一个简单的产品旋转木马控制。此Bootstrap控件对于显示无限动画非常有用,并且只能使用一小部分页面空间来显示许多产品。
产品转盘通过Razor视图引擎在服务器端呈现。旋转木马一次显示四个产品,因此Index.cshtml视图中的代码定义了一个foreach
循环,每个循环遍历4个产品的“页面”。
购物车
购物车页面。
与“产品目录”相比,“购物车”页以非常不同的方式呈现。首先,剃须刀引擎不用于直接渲染视图。相反,Razor调用了React.Web.Mvc.HtmlHelperExtensions类的React
方法,并将模型传递给它。该剃须刀反过来致使已被宣布为一个阵营的组成部分。CartView
结帐细节
结帐成功页面。
虽然该CheckoutSuccess
页面不包含任何交互(除了“返回产品目录”按钮),它完全作为一个React组件实现。原因是我们可以利用我们已经在另一篇文章中解释的React-Bootstrap库的组件提供的简单语法。所有绑定值都通过道具传递,并且不需要使用React的状态对象。
结论
我希望这篇文章可以帮助您快速开始这个新的“剃刀页面”世界。我相信你会发现许多场景,其中Razor Pages可能是传统ASP.NET MVC应用程序的诱人替代。
非常感谢你的时间和耐心!如果您有任何建议,请让我知道。不要忘了在下面的评论部分留下您的意见。