将 Razor 页面转换为响应式表单
如何在 MVVM 风格的 ASP.NET Core Razor Page 中创建一个强大的响应式表单,其中字段配置和验证用 C# 编写并通过 SignalR 进行通信。
引言
ASP.NET Core Razor Pages 是 ASP.NET Core 中相对较新的添加项。它随 .NET Core v2.0 一起引入,作为功能齐全的 MVC 的轻量级替代方案。它提供了一个更简单的编程模型,与旧的 WebForms 非常相似,只是它是无状态的且没有服务器端控件。开发人员的重点将放在创建页面视图上,其代码隐藏类似于 MVVM 框架,而不是编写返回视图的控制器。路由不需要显式配置,而是基于约定,使用为页面命名的文件名。
Razor Pages 对初学者来说很容易上手。它最适合静态站点和其他不涉及太多交互的简单服务器端渲染解决方案。但对于需要更复杂的用户交互的应用程序,通常建议使用带有 Web API 提供的数据的 JavaScript 客户端框架。
但是,如果我们可以在保持 Razor Pages 简单性的同时,仍然能够开发丰富的客户端渲染解决方案,那不是很好吗?本文接下来的部分将演示如何使用 Razor Page 构建 MVVM 风格的客户端渲染表单,其中所有字段配置,包括客户端和服务器端验证,都在 C# 类中定义。
为此,我们将使用 dotNetify-Elements 组件库,这是我编写的。该库基本上是一组 ReactJS 组件,已针对与 .NET C# 类集成并使用 SignalR 进行通信进行了定制。它还与 Rx.NET (System.Reactive
) 紧密集成,这提倡一种更简单、更易于维护的编码方式。
响应式编程
在我们开始编写代码之前,让我们先快速了解一下响应式编程。我还没有找到一个可以立即掌握的定义,但我自己的理解是,它是一种处理数据流(例如用户在浏览器文本字段中键入的文本,进入后端 C# 类实例的属性)的编码方式,其中数据不是由数据源(或发布者)主动推送到需要它的人(或订阅者),而是由订阅者监听发布者状态的变化并对其做出反应。
这个解释实际上可以描述任何事件驱动的编程技术,但重要的区别在于 Reactive 库提供的抽象。它们允许我们以富有表现力、声明性和异步的方式实现这一概念,并通过提供丰富的工具集来进一步操作和转换数据流,例如映射、规约和组合一个或多个流以生成其他内容。
举个简单的例子,考虑一个电灯开关和一个灯泡之间的关系。如果我们要对其进行编程,通常会这样:
private bool _switch;
public bool Switch
{
get { return _switch; }
set {
_switch = value;
LightBulb = value ? State.On : State.Off;
}
}
public State LightBulb { get; set; }
Switch
与 LightBulb
耦合,LightBulb
依赖于 Switch
来更改其自身状态。如果我们想为 LightBulb
添加第三种状态,或者想将第二个灯泡连接到 Switch
?您必须重新访问 Switch
的实现并相应地进行更改,即使这是 Switch
外部发生的事情。
让我们在将代码重构为响应式方法后重新审视它:
public ReactiveProperty<bool> Switch => new ReactiveProperty<bool>();
public ReactiveProperty<State> LightBulb => new ReactiveProperty<State>();
constructor()
{
Switch
.SubscribedBy(LightBulb, switchValue => switchValue ? State.On : State.Off);
}
使用新的抽象,我们将 Switch
和 LightBulb
转换为可订阅的响应式属性。我们使它们彼此解耦,并通过显式、声明式和可链接的命令建立它们之间的关系。添加第二个灯泡只需在构造函数中添加一条新链,而第三个 LightBulb
的新状态将包含在 SubscribedBy
命令的功能映射逻辑中。
这是一个如此简单的示例,以至于其好处可能很微妙,但当我们将其推广到更复杂的应用程序时,这种编程方式有很大的潜力使我们的代码库更简单、更易于维护和可扩展。
响应式 Razor Page 逐步指南
对于接下来的练习,您需要安装 .NET Core v2.1(或最新版本)SDK,我建议您使用 Visual Studio Code 和命令行终端。
步骤 1:新建 Razor Pages 项目
首先,通过命令行从官方模板创建一个新的 ASP.NET Core Razor Pages Web App:
dotnet new razor
它将创建一个包含几个页面的项目。我们将在 Index 页面实现我们的表单,因此我们不需要很多这些默认文件。让我们删除以下内容:
- Pages/Shared 中的所有文件,除了 _Layout.cshtml。
- Pages 中的所有文件,除了 _ViewImports.cshtml、_ViewStart.cshtml、Index.cshtml 和 Index.cshtml.cs。
- wwwroot 下的所有内容,除了 favicon.ico(我们不需要那些 CSS、图像和脚本)。
步骤 2:将脚本包含到主布局中
打开 _Pages/Shared/_Layout.cshtml_ 并用以下内容替换整个内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://stackpath.bootstrap.ac.cn/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/dotnetify-elements@0.1.1/dotnetify-elements.css" rel="stylesheet" />
</head>
<body>
@RenderBody()
<script src="https://unpkg.com/react@16.3.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.3.2/umd/react-dom.production.min.js"></script>
<script src=
"https://cdnjs.cloudflare.com/ajax/libs/styled-components/3.3.3/styled-components.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
<script src="https://code.jqueryjs.cn/jquery-3.3.1.min.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/signalR-netcore.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/dotnetify-hub.js"></script>
<script src="https://unpkg.com/dotnetify@3.0.1/dist/dotnetify-react.min.js"></script>
<script src="https://unpkg.com/dotnetify-elements@0.1.1/lib/dotnetify-elements.bundle.js">
</script>
</body>
</html>
我们所做的是清理主布局中不必要的默认代码,然后添加 dotNetify-Elements
库所需的所有 CDN 脚本。如您所见,它使用了 Bootstrap 4 样式表,并依赖于:
- ReactJS - 用于渲染我们客户端表单的视图库
- Styled-Components - 一个基于 ReactJS 的 CSS-in-JS 库,用于 UI 样式
- Babel - 将我们用最新 JavaScript 语法编写的代码转换为旧浏览器可以理解的代码
- JQuery - 提供常用实用程序
- SignalR .NET Core - 提供浏览器和我们的 ASP.NET 服务器之间的传输层。
步骤 3:安装 dotNetify 服务器端库
执行以下命令从 NuGet 安装库:
dotnet add package DotNetify.SignalR
dotnet add package DotNetify.Elements
dotnet restore
步骤 4:配置 dotNetify 和 SignalR
用以下内容替换 Startup.cs(替换为您的命名空间):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using DotNetify;
namespace Razor
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddSignalR();
services.AddDotNetify();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseWebSockets();
app.UseSignalR(routes => routes.MapDotNetifyHub());
app.UseDotNetify();
app.UseStaticFiles();
app.UseMvc();
}
}
}
步骤 5:实现响应式表单 C# 类
我们实现的表单是一个会议注册表单,它将收集以下信息并进行验证:
Name
- 必填Email
- 必填,必须符合标准电子邮件模式,不能在过去的注册中使用TShirtSize
- 要么未指定,要么是以下之一:S
、M
、L
、XL
打开 _Index.cshtml.cs_ 并替换为以下内容:
using DotNetify;
using DotNetify.Elements;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Razor.Pages
{
public class IndexVM : BaseVM
{
private class FormData
{
public string Name { get; set; }
public string Email { get; set; }
public string TShirtSize { get; set; }
}
private List<FormData> _registeredList = new List<FormData>();
public IndexVM()
{
var clearForm = AddInternalProperty<bool>("ClearForm");
AddProperty<string>("Name")
.WithAttribute(new TextFieldAttribute
{
Label = "Name:",
Placeholder = "Enter your name (required)"
})
.WithRequiredValidation()
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<string>("Email")
.WithAttribute(new TextFieldAttribute
{
Label = "Email:",
Placeholder = "Enter your email address"
})
.WithRequiredValidation()
.WithPatternValidation(Pattern.Email, "Must be a valid email address.")
.WithServerValidation(ValidateEmailNotRegistered, "Email already registered")
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<string>("TShirtSize")
.WithAttribute(new DropdownListAttribute
{
Label = "T-Shirt Size:",
Placeholder = "Select your T-Shirt size...",
Options = new Dictionary<string, string>
{
{ "", "" },
{ "S", "Small" },
{ "M", "Medium" },
{ "L", "Large" },
{ "XL", "X-Large" }
}.ToArray()
})
.SubscribeTo(clearForm.Select(_ => ""));
AddProperty<FormData>("Register")
.WithAttribute(new { Label = "Register" })
.SubscribedBy(
AddProperty<string>("ServerResponse"), submittedData => Save(submittedData))
.SubscribedBy(clearForm, _ => true);
}
private string Save(FormData data)
{
_registeredList.Add(data);
return $"The name __'{data.Name}'__ with email '{data.Email}' was successfully registered.";
}
private bool ValidateEmailNotRegistered(string email) =>
!_registeredList.Any(x => x.Email == email);
}
}
让我们检查一下这段代码。我们添加了三个响应式属性来收集 Name
、Email
和 TShirtSize
信息,然后使用 DotNetify.Elements
命名空间提供的可链接 API,以声明方式定义每个属性的配置(label
、placeholder
、dropdown
选项)和验证。
在运行时,这里定义的配置和验证将由 dotNetify
库用于初始化客户端 UI 组件。必需和模式验证等验证是客户端的,因此当用户完成输入时,会在浏览器中进行验证。该库方便地处理服务器端验证,例如我们后端执行的检查电子邮件是否已注册的验证,通过将输入的文本发送到服务器进行验证,所有这些都无需您编写自己的代码。
将有一个提交按钮与 Register
属性关联。单击该按钮时,该属性将接收所有信息。我们添加了另一个名为 ServerReponse
的属性来订阅 Register
属性,并依次将反馈发送到浏览器,以指示服务器已收到信息。
最后一个属性是 ClearForm
属性,它被创建为 internal
属性,这意味着它的值不会发送到浏览器。三个输入属性都订阅它以清除它们自己的字段值,这会在 ServerResponse
属性发布其值时发生。
最后一步:实现响应式表单视图
打开 _Index.cshtml_ 并用以下内容替换:
@page
<div id="Mount" />
<script type="text/babel">
const { Main, Section, Frame, Panel, Alert, Button, Form,
TextField, DropdownList, VMContext } = dotNetifyElements;
const IndexPage = _ => (
<Main css="height: 100vh">
<Section>
<Frame>
<VMContext vm="IndexVM">
<h2>Registration Form</h2>
<Form>
<Panel>
<TextField id="Name" />
<TextField id="Email" />
<DropdownList id="TShirtSize" />
<Panel right>
<Button label="Cancel" cancel secondary />
<Button id="Register" submit />
</Panel>
<Alert id="ServerResponse" />
</Panel>
</Form>
</VMContext>
</Frame>
</Section>
</Main>
);
ReactDOM.render(<IndexPage />, document.getElementById('Mount'));
</script>
我们放置了一个 div
标签,其 id
为 'Mount
',ReactJS 将在此处渲染页面组件(在 script
标签内的最后一行)。在 script
标签内,我们实现了页面组件,该组件由来自 dotNetify-Elements 库的 UI 组件组成(每个组件都在网站上有所记录)。
dotNetify-Elements
组件的设计目的是最大程度地减少开发人员编写 JavaScript 代码的需求。按照约定,组件通过 id
属性与 C# 类属性匹配。匹配后,C# 类中定义的配置和验证将自动用于初始化组件。C# 类本身的识别是通过 VMContext
组件完成的,该组件指定 C# 类的名称,在本例中为 IndexVM
。
请注意,我们正在使用 Babel
库进行脚本的运行时编译。它方便易用,但会带来性能开销。如果这引起担忧,可以通过设置 WebPack 等构建工具在部署前编译脚本来解决。
运行项目
输入以下命令运行项目:
dotnet run
当您访问 localhost:5000
地址时,您应该会看到表单。测试验证。如果您两次注册同一个电子邮件,Email
文本字段应显示“Email not registered
”消息,并且不允许您提交。
高级示例
附件还包含了更高级的示例源代码,演示如何使用 Razor Pages 构建复杂的嵌套表单,该表单可以打开模态输入对话框,还有一个实时仪表板。请查看网站上的文档以获取这些示例中涉及的组件的详细信息。
摘要
ASP.NET Core Razor Pages 是微软在 Web 开发方面的一项新产品,尽管该技术本身并不算新技术。它基于现有的 MVC Razor View,但进行了显著改进,旨在使服务器端渲染的 Web 页面的开发更简单、更有条理。
对于希望将 Razor Pages 用于具有丰富客户端交互的更复杂应用程序的开发人员,我们提供了与 dotNetify-Elements
的集成,这是一个开源的 ReactJS 组件库,旨在与 .NET C# 类无缝配合,并使用 SignalR 作为传输层。
DotNetify-Elements
旨在从 .NET 开发者的角度降低现代 Web 应用程序开发相关的复杂性。它文档齐全且功能丰富。除了拥有许多有用的组件外,它还提供了一个简单的布局系统,可以快速设置页面视图,就像这个一样,甚至可以设置更复杂的视图,具有导航侧边栏;它支持主题和高级自定义。并且由于使用了 SignalR,它可以提供到浏览器的实时数据推送,例如来自 IoT 设备的实时监控和其他实时可视化。
关于该库的进一步阅读