Blazor 是将 C# 引入浏览器的 Microsoft 试验框架,正好可以填补欠缺的 C# 一环。如今,C# 程序员可以编写桌面、服务器端 Web、云、电话、平板电脑、手表、电视和 IoT 应用程序。
Blazor 填补了欠缺的一环,C# 开发人员现在可以直接在用户浏览器中共享代码和业务逻辑。对于 C# 开发人员来说,这是一项十分强大的功能,可显著提升工作效率。.
本文将展示常见的代码共享用例。我将展示如何在 Blazor 客户端和 WebAPI 服务器应用程序之间共享验证逻辑。目前,你不仅要在服务器中验证输入,还要在客户端浏览器中验证输入。新式 Web 应用程序的用户希望获得准实时反馈。在填写长窗体并单击“提交”后仅看到红色错误返回的日子已经一去不复返了。
在浏览器中运行的 Blazor Web 应用程序可以与 C# 后端服务器共享代码。可以将逻辑放入共享库中,并在前端和后端使用它。这会带来很多好处。
可以将所有规则都集中放置在一处,并知道只需在一处更新它们。它们的工作方式确实相同,因为它们是相同的代码。
在客户端和服务器逻辑并不总是完全相同的情况下,可以节省大量测试和故障排除时间。
也许最值得一提的是,可以在客户端和服务器上使用一个库进行验证。
以前,JavaScript 前端强制开发人员编写两个版本的验证规则:一个是用适用于前端的 JavaScript 编写,另一个是用适用于后端的语言编写。若要尝试解决这种不匹配问题,需要涉及复杂的规则框架和额外的抽象层。使用 Blazor,可以在客户端和服务器上运行同一.NET Core 库。
虽然 Blazor 仍是试验框架,但它的进展迅速。生成此示例前,请先确保已安装正确版本的 Visual Studio、.NET Core SDK 和 Blazor 语言服务。有关入门步骤,请访问 blazor.net。
新建 Blazor 应用程序
首先,新建 Blazor 应用程序。
在“新建项目”对话框中,依次单击“ASP.NET Core Web 应用程序”和“确定”,再选择图 1 所示对话框中的“Blazor”图标。单击“确定”。
这会创建默认的 Blazor 示例应用程序。如果已试用过 Blazer,便会对此默认应用程序很熟悉。
图 1:选择 Blazor 应用程序
新的注册窗体将展示验证业务规则的共享逻辑。图 2 展示了包含“名字”、“姓氏”、“电子邮件地址”和“电话”字段的简单窗体。
在此示例中,它会验证所有字段是否都为必填、姓名字段是否有长度上限,以及电子邮件地址和电话字段的格式是否正确。它会在每个字段下显示错误消息,这些消息会在用户键入内容的同时更新。最后,只有在没有错误的情况下,“注册”按钮才处于启用状态。
图 2:注册窗体
共享库
所有需要在服务器和 Blazor 客户端之间共享的代码都位于一个独立的共享库项目中。共享库包含模型类和非常简单的验证引擎。模型类保留注册窗体中的数据字段。该命令如下所示:
public class RegistrationData : ModelBase
{
[RequiredRule]
[MaxLengthRule(50)]
public String FirstName { get; set; }
[RequiredRule]
[MaxLengthRule(50)]
public String LastName { get; set; }
[EmailRule]
public String Email { get; set; }
[PhoneRule]
public String Phone { get; set; }
}
RegistrationData 类继承自 ModelBase 类,后者包含所有可用于验证规则并返回绑定到 Blazor 页面的错误消息的逻辑。每个字段都使用映射到验证规则的属性进行修饰。我选择了创建非常简单的模型,它很像实体框架 (EF) 数据注释模型。此模型的所有逻辑都包含在共享库中。
ModelBase 类包含 Blazor 客户端应用程序或服务器应用程序可用来确定是否有任何验证错误的方法。它还会在此模型更改时触发事件,以便客户端能够更新 UI。任何模型类都可以继承自它,并自动获取所有验证引擎逻辑。
首先,我将在 SharedLibrary 项目中新建 ModelBase 类,如下所示:
public class ModelBase
{
}
错误和规则
现在,我将向 ModelBase 类添加包含验证错误列表的专用字典。_errors 字典先以字段名称为键,再以规则名称为键。值是要显示的实际错误消息。
通过此设置,可以轻松确定特定字段是否有验证错误,并快速检索错误消息。
代码如下:
private Dictionary<String, Dictionary<String, String>> _errors =
new Dictionary<string, Dictionary<string, string>>();
现在,我将添加 AddError 方法,以将错误输入内部错误字典。
AddError 有 fieldName、ruleName 和 errorText 参数。它先搜索内部错误字典,并删除已有条目,再添加新的错误条目,如下面的代码所示:
private void AddError(String fieldName, String ruleName, String errorText)
{
if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
new Dictionary<string, string>()); }
if (_errors[fieldName].ContainsKey(ruleName))
{ _errors[fieldName].Remove(ruleName); }
_errors[fieldName].Add(ruleName, errorText);
OnModelChanged();
}
最后,我将添加 RemoveError 方法,它接受 fieldName 和 ruleName 参数,并在内部错误字典中搜索并删除匹配的错误。代码如下:
private void RemoveError(String fieldName, String ruleName)
{
if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
new Dictionary<string, string>()); }
if (_errors[fieldName].ContainsKey(ruleName))
{ _errors[fieldName].Remove(ruleName);
OnModelChanged();
}
}
下一步是添加 CheckRules 函数,这些函数负责查找并执行附加到此模型的验证规则。有两种不同的 CheckRules 函数:一种是缺少参数,但对所有字段验证全部规则;另一种有 fieldName 参数,并仅验证特定字段。在字段更新时,使用的是第二种函数,并立即对此字段验证规则。
CheckRules 函数使用反射来查找附加到字段的属性列表。然后,它测试每个属性,以确定属性类型是否为 IModelRule。找到 IModelRule 后,它调用 Validate 方法,并返回结果,如图 3 所示。
图 3:CheckRules 函数
public void CheckRules(String fieldName)
{
var propertyInfo = this.GetType().GetProperty(fieldName);
var attrInfos = propertyInfo.GetCustomAttributes(true);
foreach (var attrInfo in attrInfos)
{
if (attrInfo is IModelRule modelrule)
{
var value = propertyInfo.GetValue(this);
var result = modelrule.Validate(fieldName, value);
if (result.IsValid)
{
RemoveError(fieldName, attrInfo.GetType().Name);
}
else
{
AddError(fieldName, attrInfo.GetType().Name, result.Message);
}
}
}
}
public bool CheckRules()
{
foreach (var propInfo in this.GetType().GetProperties(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance))
CheckRules(propInfo.Name);
return HasErrors();
}
接下来,我将添加 Errors 函数。此函数需要使用 fieldname 参数,并返回包含相应字段的错误列表的字符串。它使用内部 _errors 字典来确定相应字段是否有任何错误,如下所示:
public String Errors(String fieldName)
{
if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
new Dictionary<string, string>()); }
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (var value in _errors[fieldName].Values)
sb.AppendLine(value);
return sb.ToString();
}
现在,我需要添加 HasErrors 函数,它会在此模型的任意字段有任何错误时返回 true。客户端使用此方法来确定是否应启用“注册”按钮。
另外,WebAPI 服务器也使用此方法来确定传入的模型数据是否有错误。此函数的代码如下:
public bool HasErrors()
{
foreach (var key in _errors.Keys)
if (_errors[key].Keys.Count > 0) { return true; }
return false;
}
值和事件
是时候添加 GetValue 方法了,它需要使用 fieldname 参数,并使用反射来查找此模型中的字段并返回字段值。Blazor 客户端使用此方法来检索当前值,并在输入框中显示它,如下所示:
public String GetValue(String fieldName)
{
var propertyInfo = this.GetType().GetProperty(fieldName);
var value = propertyInfo.GetValue(this);
if (value != null) { return value.ToString(); }
return String.Empty;
}
现在,添加 SetValue 方法。它使用反射来查找此模型中的字段,并更新字段值。然后,它触发 CheckRules 方法,以对相应字段验证所有规则。Blazor 客户端使用此方法,以在用户在输入文本框中键入内容的同时更新值。代码如下:
public void SetValue(String fieldName, object value)
{
var propertyInfo = this.GetType().GetProperty(fieldName);
propertyInfo.SetValue(this, value);
CheckRules(fieldName);
}
最后,我添加 ModelChanged 事件。如果此模型中的值已更改或在内部错误字典中添加或删除了验证规则,便会触发这个事件。Blazor 客户端侦听此事件,并在事件触发时更新 UI。正因为此,显示的错误会更新,如下面的代码所示:
public event EventHandler<EventArgs> ModelChanged;
protected void OnModelChanged()
{
ModelChanged?.Invoke(this, new EventArgs());
}
得承认此验证引擎的设计非常简单,还有很多改进机会。在生产业务应用程序中,设置错误的严重性级别(如“信息”、“警告”和“错误”)会很有用。在某些情况下,如果无需修改代码,即可从配置文件动态加载规则,将会很有帮助。我不是在提倡创建你自己的验证引擎;只是有很多选择。此验证引擎既要足够好,以便演示实际示例;又要足够简单,以适应本文且易于理解。
创建规则
此时,有包含窗体字段的 RegistrationData 类。
此类中的字段使用 RequiredRule 和 EmailRule 等属性进行修饰。
RegistrationData 类继承自 ModelBase 类,后者包含所有用于验证规则并向客户端通知更改的逻辑。验证引擎的最后一部分是规则逻辑本身。接下来,我将对此进行探索。
首先,我在 SharedLibrary 中新建 IModelRule 类。
此规则由一个返回 ValidationResult 的 Validate 方法组成。每个规则都必须实现 IModelRule 接口,如下所示:
public interface IModelRule
{
ValidationResult Validate(String fieldName, object fieldValue);
}
接下来,我在 SharedLibrary 中新建 ValidationResult 类,它由两个字段组成。IsValid 字段指明规则是否有效,而 Message 字段则包含要在规则无效时显示的错误消息。代码如下所示:
public class ValidationResult
{
public bool IsValid { get; set; }
public String Message { get; set; }
}
示例应用程序使用四个不同的规则,所有规则都是继承自 Attribute 类并实现 IModelRule 接口的公共类。
现在,是时候创建规则了。请注意,所有验证规则都只是继承自 Attribute 类并实现 IModelRule 接口的 Validate 方法的类。如果输入的文本超过指定的长度上限,图 4 中的长度上限规则返回错误。其他用于验证必填字段、电话和电子邮件地址字段格式的规则的工作方式类似,区别在于它们对要验证的数据类型采用不同的逻辑。
图 4:MaxLengthRule 类
public class MaxLengthRule : Attribute, IModelRule
{
private int _maxLength = 0;
public MaxLengthRule(int maxLength) { _maxLength = maxLength; }
public ValidationResult Validate(string fieldName, object fieldValue)
{
var message = $"Cannot be longer than {_maxLength} characters";
if (fieldValue == null) { return new ValidationResult() { IsValid = true }; }
var stringvalue = fieldValue.ToString();
if (stringvalue.Length > _maxLength )
{
return new ValidationResult() { IsValid = false, Message = message };
}
else
{
return new ValidationResult() { IsValid = true };
}
}
}
创建 Blazor 注册窗体
至此,验证引擎已在共享库中完成,它可以应用于 Blazor 应用程序中的新注册窗体。首先,我在 Blazor 应用程序中添加对共享库项目的引用。为此,可使用“引用管理器”对话框的“解决方案”窗口,如图 5 所示。
图 5:添加对共享库的引用
接下来,我向应用程序的 NavMenu 添加新导航链接。
打开Shared\NavMenu.cshtml 文件,并向列表添加新注册窗体链接如图 6 所示。
图 6:添加注册窗体链接
<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match=NavLinkMatch.All>
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="registrationform">
<span class="oi oi-list-rich" aria-hidden="true"></span> Registration Form
</NavLink>
</li>
</ul>
</div>
最后,我在 Pages 文件夹中添加新 RegistrationForm.cshtml 文件。为此,可使用图 7 中的代码。
图 7 中的 cshtml 代码在 <form> 标记内有四个 <TextInput> 字段。<TextInput> 标记是自定义 Blazor 组件,用于处理字段的数据绑定和错误显示逻辑。此组件只需要三个参数即可正常运行:
-
Model 字段:标识数据要绑定到的类。
-
FieldName:标识数据要绑定到的数据成员。
-
DisplayName 字段:让组件可以显示易记消息。
图 7:添加 RegistrationForm.cshtml 文件
@page "/registrationform"
@inject HttpClient Http
@using SharedLibrary
<h1>Registration Form</h1>
@if (!registrationComplete)
{
<form>
<div class="form-group">
<TextInput Model="model" FieldName="FirstName" DisplayName="First Name" />
</div>
<div class="form-group">
<TextInput Model="model" FieldName="LastName" DisplayName="Last Name" />
</div>
<div class="form-group">
<TextInput Model="model" FieldName="Email" DisplayName="Email" />
</div>
<div class="form-group">
<TextInput Model="model" FieldName="Phone" DisplayName="Phone" />
</div>
<button type="button" class="btn btn-primary" onclick="@Register"
disabled="@model.HasErrors()">Register</button>
</form>
}
else
{
<h2>Registration Complete!</h2>
}
@functions {
bool registrationComplete = false;
RegistrationData model { get; set; }
protected override void OnInit()
{
base.OnInit();
model = new RegistrationData() { FirstName =
"test", LastName = "test", Email = "test@test.com", Phone = "1234567890" };
model.ModelChanged += ModelChanged;
model.CheckRules();
}
private void ModelChanged(object sender, EventArgs e)
{
base.StateHasChanged();
}
async Task Register()
{
await Http.PostJsonAsync<RegistrationData>(
"https://localhost:44332/api/Registration", model);
registrationComplete = true;
}
}
在页面的 @functions 块内,代码只有一点点。OnInit 方法使用其中的一些测试数据来初始化模型类。
它绑定到 ModelChanged 事件,并调用 CheckRules 方法来验证规则。
ModelChanged 处理程序调用 base.StateHasChanged 方法,以强制执行 UI 刷新。Register 方法在“注册”按钮获得单击时调用,并将注册数据发送到后端WebAPI 服务。
TextInput 组件包含输入标签、输入文本框、验证错误消息,以及在用户键入内容的同时更新模型的逻辑。Blazor 组件非常易于编写,并提供了将接口分解为可重用部分的强大方法。参数成员使用 Parameter 属性进行修饰,以便让 Blazor 知道它们是组件参数。
输入文本框的 oninput 事件连接到 OnFieldChanged 处理程序。每当输入更改,都会触发此事件。然后,OnFieldChanged 处理程序调用 SetValue 方法,以对相应字段执行规则,并在用户键入内容的同时实时更新错误消息。图 8 展示了代码。
图 8:更新错误消息
@using SharedLibrary
<label>@DisplayName</label>
<input type="text" class="form-control" placeholder="@DisplayName"
oninput="@(e => OnFieldChanged(e.Value))"
value="@Model.GetValue(FieldName)" />
<small class="form-text" style="color:darkred;">@Model.Errors(FieldName)
</small>
@functions {
[Parameter]
ModelBase Model { get; set; }
[Parameter]
String FieldName { get; set; }
[Parameter]
String DisplayName { get; set; }
public void OnFieldChanged(object value)
{
Model.SetValue(FieldName, value);
}
}
服务器上的验证
验证引擎现已开始在客户端上运行。下一步是在服务器上使用共享库和验证引擎。为此,我先向解决方案添加另一个 ASP.NET Core Web 应用程序项目。这次,我在图 1 所示的“新建 ASP.NET Core Web 应用程序”对话框中选择的是“API”,而不是“Blazor”。
新建 API 项目后,我就添加对共享项目的引用,就像在 Blazor 客户端应用程序中(见图 5)一样。接下来,我向 API 项目添加新控制器。
新控制器接受来自 Blazor 客户端的 RegistrationData 调用,如图 9 所示。注册控制器在服务器上运行,并且是后端 API 服务器的典型特征。区别在于,它现在运行在客户端上运行的相同验证规则。
图 9:注册控制器
[Route("api/Registration")]
[ApiController]
public class RegistrationController : ControllerBase
{
[HttpPost]
public IActionResult Post([FromBody] RegistrationData value)
{
if (value.HasErrors())
{
return BadRequest();
}
// TODO: Save data to database
return Created("api/registration", value);
}
}
注册控制器有一个 POST 方法,它接受 RegistrationData 作为自己的值。它调用 HasErrors 方法,以验证所有规则并返回布尔值。
若有错误,控制器返回 BadRequest 响应;否则,它返回成功响应。我特意省略掉了将注册数据保存到数据库的代码,这样我就可以验证方案为重点了。
现在,共享验证逻辑在客户端和服务器上运行。
远景
此简单示例展示了如何在浏览器和后端之间共享验证逻辑,仅仅触及全栈 C# 环境强大功能的皮毛。
Blazor 的神奇之处在于,使用它,现有 C# 开发人员大军可以生成功能强大的新式响应式单页应用程序,且最大限度地缩短启动时间。使用它,企业可以重用和重新打包现有代码,以便能够直接在浏览器中运行现有代码。
能够在浏览器、桌面、服务器、云和移动平台之间共享 C# 代码,将大大提升开发人员的工作效率。它还便于开发人员更快地向客户交付更多功能和更多业务价值。