前言
大家好,我是开源项目 MASA Blazor 主要开发者之一,如果你还不了解 MASA Blazor,可以访问我们的 官网 和博客 《初识MASA Blazor》 一探究竟。简单来说,MASA Blazor 是一个基于 Material Design 设计语言的 Blazor 组件库,dotNET 开发者只需或者甚至不需要懂得 javascript 就能开发一个企业级中后台系统。
我这次分享的主题是《使用 MASA Blazor 开发一个标准的查询表格页》,我会先从创建项目开始手撸一个没有任何技巧的查询表格页,然后我会分享一些技巧和封装的组件,实现快速开发。
手撸查询表格页
创建应用程序
关于如何安装 MASA Blazor 模板,请移步 MASA.Blazor快速入门。
1.首先通过 MASA Blazor 模板默认的 Server 项目,项目命名为 MasaBlazorStandardTablePage。
dotnet new --install MASA.Template
dotnet new masab -o MasaBlazorStandardTablePage
复制代码
2.通过 CLI 运行应用程序,或直接通过 vs 启动项目。
cd MasaBlazorStandardTablePage
dotnet run
复制代码
3.启动成功后切换到 Fetch data 页面,此页面展示了一个简单的使用了 MDataTable
的表格。
支持单个查询条件和搜索
让我们从最简单的单个条件查询开始。
将随机数据替换成模拟数据
修改 WeatherForecastService
,将随机数据替换成死数据以便支持查询功能。下面的代码更新了数据来源和 GetForecastAsync
查询方法。
public class WeatherForecastService
{
private readonly List<WeatherForecast> _data = new()
{
new() { Date = DateTime.Now.AddDays(-1), TemperatureC = 23,
Summary = "Freezing" },
new() { Date = DateTime.Now.AddDays(-1), TemperatureC = -10,
Summary = "Bracing" },
new() { Date = DateTime.Now.AddDays(-1), TemperatureC = 37,
Summary = "Chilly" },
new() { Date = DateTime.Now.AddDays(-2), TemperatureC = 29,
Summary = "Cool" },
new() { Date = DateTime.Now.AddDays(-3), TemperatureC = 11,
Summary = "Mild" },
new() { Date = DateTime.Now.AddDays(-4), TemperatureC = 35,
Summary = "Warm" },
new() { Date = DateTime.Now.AddDays(-5), TemperatureC = 41,
Summary = "Balmy" },
new() { Date = DateTime.Now.AddDays(-5), TemperatureC = -13,
Summary = "Hot" },
new() { Date = DateTime.Now.AddDays(-6), TemperatureC = 23,
Summary = "Sweltering" },
new() { Date = DateTime.Now.AddDays(-7), TemperatureC = 2,
Summary = "Scorching" },
};
public Task<WeatherForecast[]> GetForecastAsync()
{
IEnumerable<WeatherForecast> res = _data.AsQueryable();
return Task.FromResult(res.ToArray());
}
}
复制代码
2 同时修改 FetchData.razor,因为 `WeatherForecastService.GetForecastAsync()
` 删除了 `startDate`
入参。
protected override async Task OnInitializedAsync()
{
await Task.Delay(1000); // 模拟真实环境,触发Loading效果
forecasts = await ForecastService.GetForecastAsync(); // here
}
复制代码
添加查询输入框和搜索按钮
在 FetchData.razor 页面中的 <p>
标签下添加以下代码
<MRow Class="pb-3">
<MCol Cols="12">
<MTextField @bind-Value="summary"
Dense
HideDetails="@("auto")"
Label="Summary"
Outlined>
</MTextField>
</MCol>
<MCol Cols="12" Class="d-flex py-0 pb-3">
<MSpacer></MSpacer>
<MButton Color="primary" OnClick="OnSearch">搜索</MButton>
</MCol>
</MRow>
@code {
private string summary;
private async Task OnSearch()
{
forecasts = await ForecastService.GetForecastAsync(summary);
}
}
复制代码
Line 3,17
定义了一个 string
类型的名为 summary
的变量,双向绑定给了 MTextField
组件。MTextFiled
除了 @bind-Value
属性用于设置双向绑定,其他属性的含义请阅读 文档。
Line 12
定义了一个搜索按钮,用于触发查询。
修改 WeatherForecastService.GetForecastAsync
方法,增加 summary
入参,并支持查询。
public Task<WeatherForecast[]> GetForecastAsync(string? summary = null)
{
IEnumerable<WeatherForecast> res = _data.AsQueryable();
if (!string.IsNullOrEmpty(summary))
{
res = res.Where(item => item.Summary.Contains(summary));
}
return Task.FromResult(res.ToArray());
}
复制代码
支持多个查询条件和重置
现在让我们再添加一个高温预警的选择框来查询不同高温预警状态的数据。
更新 WeatherForecastService
以支持根据高温预警筛选数据
public Task<WeatherForecast[]> GetForecastAsync(string? summary = null, WarningSigns? warningSigns = null)
{
IEnumerable<WeatherForecast> res = _data.AsQueryable();
if (!string.IsNullOrEmpty(summary))
{
res = res.Where(item => item.Summary.Contains(summary));
}
if (warningSigns.HasValue)
{
res = warningSigns switch
{
WarningSigns.Yellow => res.Where(item => item.TemperatureC >= 35 && item.TemperatureC < 37),
WarningSigns.Orange => res.Where(item => item.TemperatureC >= 37 && item.TemperatureC < 39),
WarningSigns.Red => res.Where(item => item.TemperatureC >= 39),
_ => res
};
}
return Task.FromResult(res.ToArray());
}
复制代码
增加高温预警选择框
在 Data 目录下添加名为 WarningSigns
的枚举。
public enum WarningSigns
{
[Description("高温黄色预警 35℃")]
Yellow = 1,
[Description("高温橙色预警 37℃")]
Orange,
[Description("高温红色预警 39℃")]
Red
}
复制代码
引入 Masa.Utils.Enums
包,此包提供的 GetEnumObjectList
方法能轻松的将枚举的 Description
和枚举值用于 MSelect
组件的 Items
。
dotnet add package Masa.Utils.Enums
复制代码
更新 FetchData.razor。
<MRow Class="pb-3">
<MCol Cols="12" Sm="6">
<MTextField @bind-Value="@summary"
Label="Summary"
Dense
HideDetails="@("auto")"
Outlined>
</MTextField>
</MCol>
<MCol Cols="12" Sm="6">
<MSelect @bind-Value="warningSigns"
Items="@(Enum<WarningSigns>.GetEnumObjectList<WarningSigns>())"
ItemText="item => item.Name"
ItemValue="item => item.Value"
TValue="WarningSigns?"
TItem="EnumObject<WarningSigns>"
TItemValue="WarningSigns"
Label="高温警告"
Clearable
Dense
HideDetails="@("auto")"
Outlined>
</MSelect>
</MCol>
<MCol Cols="12" Class="d-flex py-0 pb-3">
<MSpacer></MSpacer>
<MButton Class="mr-2" OnClick="OnReset">重置</MButton>
<MButton Color="primary" OnClick="OnSearch">搜索</MButton>
</MCol>
</MRow>
@code {
private WarningSigns? warningSigns;
private Task OnReset()
{
summary = null;
warningSigns = null;
return OnSearch();
}
private async Task OnSearch()
{
forecasts = await ForecastService.GetForecastAsync(summary, warningSigns);
}
}
复制代码
Line 2,10
通过设置 Sm="6"
可以让屏幕尺寸大于 768px 时一行占两个 MCol
,实现 MTextField
和 MSelect
并排显示。
Line 11-23,33,44
第 33 行定义 warningSigns
变量用于接收 MSelect
选中的值,当然也可以通过设置值更新 MSelect
选中的值,只要设置了 @bind-Value
双向绑定就行,就像第 11 行那样。第 12 行使用了 Masa.Utils.Enums
提供的方法,返回了一个包含 Name(Description)和 Value(枚举值)的列表,赋值给了 MSelect.Items
。第 44 行将 warningSigns
的传给查询接口。
Line 27,35-40
此处定义了一个重置按钮,用于清空所有查询输入框的内容并刷新表格。
支持键入回车或选择后触发查询
后来测试小姐姐说你这太难用了,回车不能触发搜索,选择完也不能触发搜索。好吧好吧,我们现在加上。
键入回车后触发
原理即捕捉 OnKeyDown
事件是否点击了 Enter
键。
<MTextField @bind-Value="@summary"
OnKeyDown="HandleOnKeyDown"
Label="Summary"
Dense
HideDetails="@("auto")"
Outlined>
</MTextField>
@code {
private async Task HandleOnKeyDown(KeyboardEventArgs args)
{
if (args.Code == "Enter")
{
// 等待156毫秒,预防输入的值在更新到变量之前按下Enter键
await Task.Delay(156);
await OnSearch();
}
}
}
复制代码
选择后触发查询
<MSelect @bind-Value="warningSigns"
Items="@(Enum<WarningSigns>.GetEnumObjectList<WarningSigns>())"
ItemText="item => item.Name"
ItemValue="item => item.Value"
TValue="WarningSigns?"
TItem="EnumObject<WarningSigns>"
TItemValue="WarningSigns"
Label="高温警告"
OnSelectedItemUpdate="OnSearch"
Clearable
Dense
HideDetails="@("auto")"
Outlined>
</MSelect>
复制代码
点击清空图标触发查询
很简单,只要给 MTextField
和 MSelect
组件添加以下属性:
Clearable
OnClearClick="OnSearch"
复制代码
加点 Loading 动画可好?
好!
<MButton Color="primary"
Loading="searching"
OnClick="HandleOnSearch">
搜索
</MButton>
...
<MDataTable Headers="_headers" Items="forecasts"
Loading="loading"
ItemsPerPage="5" Class="elevation-1">
...
@code {
private bool loading;
private bool searching;
private async Task HandleOnSearch()
{
searching = true;
await OnSearch();
searching = false;
}
private async Task OnSearch()
{
loading = true;
await Task.Delay(1000);
forecasts = await ForecastService.GetForecastAsync(summary, warningSigns);
loading = false;
}
}
复制代码
新增 searching
变量用于控制搜索按钮的 Loading
状态,同时新增了 HandleOnSearch
代替原来的 OnSearch
是为了单独控制点击搜索按钮的动画。
新增 loading
变量用于控制 MDataTable
的 Loading
状态。OnSearch
方法块中在接口请求前后设置 loading
的值。
表的行操作和自定义列样式
因为篇幅限制,我就不一一把代码贴出来了,具体代码请查阅 源码 接下来我将针对 Table 写一些常见的代码,如行操作和自定义列样式。
封装组件和技巧
我本应该用这节分享的内容将上面的例子重构的过程写出来,但感觉会使得本文太冗长。重构后的代码我也会上传到 Github 上。
封装组件
试想一下,当你被分配到好几个模块,每个模块都有至少一个查询表格页,你会如何开发?你大概会说复制最合适的代码文件,然后重命名文件名,重命名相应的变量,修修改改就完行了。当然这是一个方法,但不优雅。那优雅的方式是什么,是封装。我有段时间在全职开发 MASA.Blazor 组件库,后面因为业务需求分配到了 IoT 项目帮助 Blazor 后台系统的研发和 MASA.Blazor 的实践。在开发 IoT 项目时,经常会看见相同的代码分布在相同的类中,我试着优化重构这些代码,并从查询表格页中抽离封装了以下几个组件:
Filters:接收OnSearch
参数代理查询,通过context
提供onEnter
和onSearch
方法供单个查询组件使用。
PageHeader:一个标准的页头,包括了标题、副标题、搜索按钮,并提供Filters
组件的能力。
Actions:提供一组操作按钮,默认展示前两个,后面的按钮会移动到MMenu
中显示。
BlockText:将相同类型的两个数据并列显示。
ColorChip:提供有限的颜色列表生成带浅色字体的MChip
。
CopyableText:在文本后提供可以复制内容的图标按钮。
DateTimePicker:提高带时分秒选择器的弹出层时间选择器。
EllipsisText:根据父级盒子的宽度自动截断文本。
GenericColumnRender:渲染 DateTime、枚举、bool 和其他类型值。可以用于MDataTable
的ItemColContent
和Definitions
的DetailContent
。
PageHeader
组件作为 MASA.Blazor 预置组件的一部分已经发布,其他提及的组件还没有并入 MASA.Blazor 主库。如果你想要使用或参考,可以访问 MASA.Blazor.Experimental.Components。关于预置组件和实验性组件的详细介绍和使用的文章,后面会由其他同事编写和发布,请大家带多多关注!
MASA.Blazor.Experimental.Components 是一个实验性组件库,这意味着该库的 API 和功能可能会被重新设计。不过随着实验性组件的功能不断完善和稳定,会随便 MASA.Blazor 版本的更新而并入主库。
技巧
善用基类
Blazor 的组件其实也是一个类,它默认继承自 ComponentBase
并提供了许多虚拟方法,我们可以重写它们来影响应用程序的行为。而这些方法通过继承机制给所有 Blazor 组件使用。
在实际开发中,我会发现几乎每个页面都会注入 NavigationManager
、IJsRunTime
和其他可能存在的业务服务,或者会使用某些共同使用的组件,那我们可以在继承 ComponentBase
的基础上再写一个已经使用了这些服务和组件的基类。
按架构可以创建专门给 @page
组件用的 PageComponentBase
和单纯封装功能的 PureComponentBase
。 按业务分类就得看情况了,因为业务更加具体,基类里通常会有注入 HttpClient
或者同类型业务服务,以及任何共同使用的代码。
SetParametersAsync
SetParametersAsync sets parameters supplied by the component's parent in the render tree or from route parameters.
只需知道每当父级呈现时,都会执行此方法。这意味着它是指定默认参数值的正确位置。 拿前面的例子来说,在使用 MTextField
和 MSelect
时都会设置以下代码来维持相同的外观和行为:
Clearable
Dense
HideDetails="@("auto")"
Outlined
复制代码
那么与其每次都要写一遍,不如利用 SetParametersAsync
的特性把这些默认参数提前设置:
public class DefaultTextField<TValue> : MTextField<TValue>
{
public override async Task SetParametersAsync(ParameterView parameters)
{
Clearable = true;
Dense = true;
HideDetails = "auto";
Outlined = true;
await base.SetParametersAsync(parameters);
}
}
public class DefaultSelect<TItem, TItemValue, TValue> : MSelect<TItem, TItemValue, TValue>
{
public override async Task SetParametersAsync(ParameterView parameters)
{
Clearable = true;
Dense = true;
HideDetails = "auto";
Outlined = true;
await base.SetParametersAsync(parameters);
}
}
复制代码
<DefaultTextField @bind-Value="@summary"
OnKeyDown="@context.onEnter"
OnClearClick="@context.onSearch"
Label="Summary">
</DefaultTextField>
<DefaultSelect @bind-Value="warningSigns"
Items="@(Enum<WarningSigns>.GetEnumObjectList<WarningSigns>())"
ItemText="item => item.Name"
ItemValue="item => item.Value"
TValue="WarningSigns?"
TItem="EnumObject<WarningSigns>"
TItemValue="WarningSigns"
Label="高温警告"
OnSelectedItemUpdate="@context.onSearch"
OnClearClick="@context.onSearch">
</DefaultSelect>
复制代码
未来的计划
未来我们团队将继续优化各个组件的性能,完成缺失的组件,解决 BUG 问题,完善文档等。另外,我们也计划出 Blazor 相关的教程和分享文章,敬请期待。
感谢阅读!
资源
开源地址
MASA.BuildingBlocks:https://github.com/masastack/MASA.BuildingBlocks
MASA.Contrib:https://github.com/masastack/MASA.Contrib
MASA.Utils:https://github.com/masastack/MASA.Utils
MASA.EShop:https://github.com/masalabs/MASA.EShop
MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor
如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们
评论