Microsoft Blazor - 动态内容
演示Microsoft两年前作为ASP.NET Core一部分发布的这项惊人技术 - Blazor!
引言
如果你问我为什么对这项技术如此兴奋,我会回答 - 嗯,那是因为我讨厌JavaScript(像许多其他人一样),这一次Microsoft为Web开发者提供了非常有用的东西,现在它成为了JavaScript及其所有为了延续其毫无意义的生命而创建的臃肿框架(Angular,React等)的真正替代品。
在我目前工作的地方(Pro Coders),我们从预览版开始就使用Blazor,并尝试了许多惊人的东西,主要是基于SignalR构建的Blazor Server-Side。我还要提到,我今天要分享的技术也可以用于客户端Blazor WebAssembly。
如果你有兴趣,还有另一篇关于动态表单的文章。
动态内容
如你所知,我们使用Razor页面来定义Blazor UI - 这项技术已经存在大约5年了,我不会浪费你的时间深入研究Razor,假设它对你来说很简单且众所周知。
如果你在开发时不知道页面应该是什么样子,并且其内容和页面控件应该基于用户或站点管理员可以动态更改的数据来显示,会怎么样?你需要动态生成页面。
让我们举一个真实生活中的例子 - 内容管理系统,其中站点管理员可以决定用户在用户个人资料页面上可以填写哪些字段,让我来制定一个我们将要在今天实现的用户故事。
用户故事 #1:动态UI生成
- 基于从服务接收的控件列表生成UI页面。
- 支持两种类型的控件:
TextEdit
(文本编辑)和DateEdit
(日期编辑)。 - 控件列表具有UI生成的属性:
Label
(标签),Type
(类型),Required
(必填)。
实现 - 创建项目
让我们创建一个新的Blazor项目,并保持简单。打开Visual Studio 2019,点击创建新项目,然后找到Blazor模板并点击下一步。
输入项目名称,在下一页选择Blazor Server App,然后点击创建。
你将看到一个为你创建的新解决方案,它将包含Visual Studio模板添加的几个用于学习的页面。你可以通过点击播放按钮(绿色三角形)来构建和运行解决方案,以便在浏览器中查看创建的应用程序。
实现 - 定义模型和服务
我倾向于从定义模型开始,所以创建一个新类ControlDetails.cs,并将以下代码放入其中。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoDynamicContent
{
public class ControlDetails
{
public string Type { get; set; }
public string Label { get; set; }
public bool IsRequired { get; set; }
}
}
现在我们可以创建一个服务类,暂时返回一些测试数据,所以创建ControlService.cs并添加此代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoDynamicContent
{
public class ControlService
{
public List<ControlDetails> GetControls()
{
var result = new List<ControlDetails>();
result.Add(new ControlDetails { Type = "TextEdit",
Label = "First Name", IsRequired = true });
result.Add(new ControlDetails { Type = "TextEdit",
Label = "Last Name", IsRequired = true });
result.Add(new ControlDetails { Type = "DateEdit",
Label = "Birth Date", IsRequired = false });
return result;
}
}
}
在此代码中,我们指定了要在动态页面上显示的三个控件:名字、姓氏和出生日期。
我们需要做的最后一步是为依赖注入注册我们的服务,所以只需打开Startup.cs,找到[ConfigureServices(IServiceCollection services)
]方法,并在底部添加一行代码,使其看起来像这样:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
// added line for ControlService
services.AddSingleton<ControlService>();
}
实现 - 动态页面
为了简单起见,我们将重用Counter.razor
页面(已由Visual Studio模板创建)。我们需要删除除第一行以外的所有行,然后开始添加我们自己的代码,首先使用依赖注入来注入我们的服务。
page "/counter"
@inject ControlService _controlService
现在我们需要执行_controlService
并遍历返回的控件列表。
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
}
对于每个控件,我们将显示一个标签,如果控件是必填的,则会在标签旁边标记“*”。
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
@if (control.IsRequired)
{
<div>@(control.Label)*</div>
}
else
{
<div>@control.Label</div>
}
}
最后一步是在switch
语句中渲染特定类型的控件。
@page "/counter"
@inject ControlService _controlService
@foreach (var control in _controlService.GetControls())
{
@if (control.IsRequired)
{
<div>@(control.Label)*</div>
}
else
{
<div>@control.Label</div>
}
@switch (control.Type)
{
case "TextEdit":
<input required="@control.IsRequired">
break;
case "DateEdit":
<input required="@control.IsRequired" type="date">
break;
}
}
实现 - 运行
现在你可以编译并运行你的解决方案。在出现的浏览器窗口中,点击Counter菜单项以查看结果。
实现 - 添加控件绑定
进一步扩展这个想法,我们需要将生成的Razor控件绑定到我们的Razor页面中的属性,并将它们存储在一个Dictionary
中,例如,其中Key
是Label
(标签),Value
是Razor控件的值。
在这里,我添加了@code
部分,其中包含服务执行逻辑以及我们绑定到控件的所有属性和事件。绑定是双向的。
最终的代码将如下所示:
@page "/counter"
@inject ControlService _controlService
@foreach (var control in ControlList)
{
@if (control.IsRequired)
{
<div>@(control.Label)*</div>
}
else
{
<div>@control.Label</div>
}
@switch (control.Type)
{
case "TextEdit":
<input @bind-value="@Values[control.Label]" required="@control.IsRequired" />
break;
case "DateEdit":
<input type="date" value="@Values[control.Label]"
@onchange="@(a => ValueChanged(a, control.Label))"
required="@control.IsRequired" />
break;
}
}
<br/>
<button @onclick="OnClick">Submit</button>
@code
{
private List<ControlDetails> ControlList;
private Dictionary<string, string> Values;
protected override async Task OnInitializedAsync()
{
ControlList = _controlService.GetControls();
Values = ControlList.ToDictionary(c => c.Label, c => "");
}
void ValueChanged(ChangeEventArgs a, string label)
{
Values[label] = a.Value.ToString();
}
string GetValue(string label)
{
return Values[label];
}
private void OnClick(MouseEventArgs e)
{
// send your Values
}
}
如果现在运行你的解决方案,在OnClick
方法中设置一个断点,然后在页面控件中输入值,并点击Submit(提交)按钮,你将在底部的监视面板中看到输入的值。
这太棒了,我们将输入的值存储在一个Dictionary
中,现在可以将它提供给保存值的服务,并将其存入数据库。
完整的解决方案和结果代码可以在我的GitHub页面找到:
摘要
在本练习中,我们实现了一个UI页面,该页面使用从服务接收的数据生成控件。数据控制表现。通过提供存储在数据库中的数据,我们向用户展示内容。如果我们想展示略有不同的内容 - 我们只需要更改数据库中的数据,用户就会看到我们的更改,无需重新编译或部署。
但是这个解决方案有一个小限制 - 它只支持我们在Razor页面上的[switch
]语句中指定的那些控件。每次我们需要显示一个未在[switch
]语句中指定的控件时,我们需要扩展它,添加新控件的代码并重新编译解决方案。
下一个挑战 - 动态内容的自定义控件
是否有可能创建一个可外部扩展的动态页面,该页面将支持我们在以后在单独的程序集中添加的所有控件,而无需重新编译我们的动态页面?
是的,这是可能的 - 使用我在下一篇博文中分享的技术。
有一个使用开源库的类似方法。
谢谢,下次再见!
历史
- 2020年10月12日:初版