大家好,我是码农刚子。最近把项目里的 Radzen 从 5.x 升到了 6.0.0。
升级本身挺顺利,Breaking Changes 不算多。但用着用着就发现,有那么几个官方组件,API 设计得总让人觉得「差那么一口气」。
不是不能用,是用起来别扭。每次写都要翻文档、猜参数、然后心里嘀咕一句「这也太绕了吧」。
今天挑几个最有代表性的聊聊,顺便说说我是怎么绕过去的。纯个人感受,不同意的话评论区切磋。
1. RadzenDataGrid:分页器的「假联动」
先从最常用的 DataGrid 说起。
RadzenDataGrid 整体其实还不错,功能全、性能也过得去。但有个地方我一直用着别扭------自定义分页器和 DataGrid 的联动方式。
场景是这样的:我想在表格顶部加一个搜索框,下面是表格,底部是分页器。分页器不想用内置的,要自己写,因为要加「每页 N 条」切换和跳转到指定页。
按正常 Blazor 组件的思路,分页器改了之后,调一下 DataGrid 的某个方法或者改个参数,表格就刷新了对吧?
Radzen 偏不。
你得这么写:
razor
<!-- 搜索框 -->
<RadzenTextBox @bind-Value="searchTerm"
Change="@(args => grid.FirstPage())" />
<!-- 表格 -->
<RadzenDataGrid @ref="grid"
Data="@data"
Count="@totalCount"
LoadData="@LoadData"
AllowPaging="true"
PageSize="10"
ShowPaging="false">
...
</RadzenDataGrid>
<!-- 自定义分页器 -->
<RadzenPager Count="@totalCount"
PageSize="@pageSize"
PageIndex="@pageIndex"
PageChanged="@OnPageChanged" />
@code {
RadzenDataGrid<Item> grid;
int pageIndex;
int pageSize = 10;
int totalCount;
void OnPageChanged(int newPageIndex)
{
pageIndex = newPageIndex;
grid.FirstPage(); // 等等,这不是第一页吗?
}
}
看出问题了吗。
RadzenPager 的 PageChanged 事件只给你一个新页码,但 DataGrid 这边没有 GoToPage(pageIndex) 这种方法。你得用 FirstPage()、NextPage()、PrevPage()、LastPage() 这四个方法凑。
想跳转到第 5 页?没有直接方法,你得手动设置 grid.PageIndex 然后调 grid.Reload()。
⚠️ 别扭在哪: Pager 组件和 DataGrid 组件是「假联动」------看着像一套,实际上各管各的状态。Pager 改了页,DataGrid 不知道;DataGrid 改了页,Pager 也不知道。全靠开发者手动同步两边的状态。
我的解法是,干脆不用 RadzenPager,分页逻辑完全自己写,DataGrid 只负责渲染数据和触发 LoadData。
反而更清爽。
2. RadzenDialog:参数传递的玄学
DialogService 是 Radzen 里我用得比较多的功能之一。
打开一个弹窗组件,传点参数进去,用户操作完返回结果------这个模式很常见。Radzen 的实现思路是对的,但参数传递的方式,我觉得设计得有点随意。
打开对话框的代码长这样:
csharp
var result = await DialogService.OpenAsync<EditUserDialog>(
"编辑用户",
new Dictionary<string, object>
{
{ "UserId", userId },
{ "UserName", userName },
{ "IsAdmin", isAdmin }
});
然后弹窗组件那边用 [Parameter] 接收:
csharp
// EditUserDialog.razor
[Parameter]
public int UserId { get; set; }
[Parameter]
public string UserName { get; set; }
[Parameter]
public bool IsAdmin { get; set; }
问题来了。
字典的 key 是字符串,和组件的 Parameter 名称靠人工对应。 拼错了不会有编译错误,运行时也不报错,就是参数传不进去,值是默认的。
你调试半天,最后发现是 "UserID" 大写了 D,而组件里是 UserId。
还有个更隐蔽的问题------参数是在 OnInitialized 之后才设置的。
如果你在 OnInitialized 里用 UserId 去查数据,拿到的永远是 0。因为这时候参数还没注入进来。得写到 OnParametersSetAsync 里才行。
这个行为和普通 Blazor 组件不一样。普通组件的参数是在 OnInitialized 之前就设好的,但 DialogService 是先创建组件实例,再逐个设置 Parameter 属性。
⚠️ 别扭在哪:
Dictionary<string, object>传参完全没有类型安全,拼错字段名全靠肉眼查。加上参数设置时机和标准 Blazor 组件不一致,新手第一次用基本都会踩。
我现在的做法是,给每个弹窗组件写一个静态 Open 方法,把参数封装起来:
csharp
public static async Task<User?> Open(
DialogService dialogService,
int userId,
string userName,
bool isAdmin)
{
return await dialogService.OpenAsync<EditUserDialog>(
"编辑用户",
new Dictionary<string, object>
{
[nameof(UserId)] = userId,
[nameof(UserName)] = userName,
[nameof(IsAdmin)] = isAdmin
});
}
用 nameof 至少能防拼写错误,调用方也有了类型提示。但这是开发者自己补的保险,不是框架给的保障。
3. RadzenUpload:事件设计的反直觉
文件上传这个组件,是我觉得 Radzen 里最让人摸不着头脑的一个。
先说最基本的场景:用户选了文件,前端把文件内容读出来,或者传给后端 API。这是个很常见的需求吧?
来看看 RadzenUpload 的用法:
razor
<RadzenUpload Url="api/upload"
OnChange="@OnUploadChange"
OnProgress="@OnUploadProgress"
OnComplete="@OnUploadComplete"
Multiple="true" />
看着挺正常。Url 指定上传地址,事件回调状态。
但问题是,这个组件是「自动上传」的------用户一选文件,它立刻就往 Url 发请求了。
如果你想让用户先选文件、预览一下、点个确认按钮再上传呢?
不好意思,没有内置支持。你得用一个很别扭的方式绕------把 Url 设成空字符串,在 OnChange 事件里自己处理文件,然后手动调上传。
更绝的是 OnChange 事件的参数。
csharp
void OnUploadChange(UploadChangeEventArgs args)
{
foreach (var file in args.Files)
{
// file.Name 文件名
// file.Size 文件大小
// 想拿到文件内容?没有 Stream,也没有 byte[]
// 你只能拿到文件名和大小
}
}
对,你没看错。OnChange 里拿不到文件内容。
想读文件内容?你有两个选择:
一是用 JS 互操作自己调浏览器的 File API 去读。二是等 OnComplete 事件,从后端的返回值里拿。
但 OnComplete 是上传完成之后才触发的,那时候文件已经发出去了,还预览个啥。
⚠️ 别扭在哪: 组件被设计成了「选了就传」的全自动模式,而且不提供读取文件内容的 API。想做预览、确认、校验这些常见操作,全得自己用 JS 绕。这不是难,是没必要的难。
我现在项目里的文件上传,干脆就不用 RadzenUpload 了。用原生的 <InputFile> 搭配自己写的上传逻辑,反而更灵活。
外观上套一层 Radzen 的样式就行。
4. RadzenScheduler:数据绑定的「暗箱操作」
Scheduler 是 Radzen 里比较重量级的组件了,功能也确实全------日视图、周视图、月视图、拖拽调整时间、资源分组......该有的都有。
但它的数据绑定方式,我实在喜欢不起来。
正常 Blazor 组件的数据绑定是这样的:你给它一个 List,它展示这个 List。List 变了,界面跟着变。
RadzenScheduler 不这么玩。它有两种模式:
模式一:直接给 Data 传集合。 这种模式下,Scheduler 会把所有数据一次性加载进来,然后自己在客户端做过滤和展示。数据量小的时候还好,数据多了性能就拉胯了。
模式二:用 LoadData 回调。 组件告诉你当前视图的起止时间,你去后端查对应时间范围内的数据,返回给它。
模式二听起来很合理,按需加载嘛。但实际用起来有个大坑------你没法主动刷新数据。
razor
<RadzenScheduler @ref="scheduler"
LoadData="@LoadSchedulerData">
...
</RadzenScheduler>
@code {
RadzenScheduler<Appointment> scheduler;
async Task LoadSchedulerData(
SchedulerLoadDataArgs args)
{
var data = await api.GetAppointments(
args.Start, args.End);
args.Data = data;
}
void RefreshData()
{
// 想刷新?没有 Reload 方法
// scheduler.Reload() ← 不存在
// 你得这么干:
scheduler.SelectedDate = scheduler.SelectedDate.AddDays(1);
scheduler.SelectedDate = scheduler.SelectedDate.AddDays(-1);
// 改一下再改回去,触发重新加载
}
}
是的,你没看错。要刷新数据,你得「晃一下」SelectedDate,让组件以为日期变了,从而触发 LoadData。
这是官方论坛的工作人员自己给的解决方案。
6.0.0 里依然是这样。
⚠️ 别扭在哪: LoadData 模式下没有公开的 Reload 方法,开发者要用「改日期再改回来」这种 hack 来触发刷新。一个企业级组件库的核心组件,刷新数据要靠 trick,这事说出去有点不太好听。
我的处理方式是,自己维护一份数据缓存,LoadData 从缓存里拿,需要刷新的时候清缓存再「晃一下」。至少业务代码里不用到处写这种 magic 操作。
5. RadzenHtmlEditor:工具栏的「半残」状态
富文本编辑器这个组件,我纠结了很久要不要放进来。
因为它的基础功能其实还行------加粗、斜体、列表、链接、图片,常用的都有。但一旦你要自定义工具栏,事情就变得诡异起来。
默认工具栏是这样的:
razor
<RadzenHtmlEditor @bind-Value="@htmlContent" />
一行搞定,工具栏按钮全给你安排好了。
但如果你想只保留一部分按钮呢?或者想调整一下按钮顺序呢?
那你就得把所有想要的按钮一个一个手写出来:
razor
<RadzenHtmlEditor @bind-Value="@htmlContent">
<RadzenHtmlEditorBold />
<RadzenHtmlEditorItalic />
<RadzenHtmlEditorUnderline />
<RadzenHtmlEditorSeparator />
<RadzenHtmlEditorAlignLeft />
<RadzenHtmlEditorAlignCenter />
<RadzenHtmlEditorAlignRight />
<RadzenHtmlEditorSeparator />
<RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo />
...
</RadzenHtmlEditor>
行,这也能接受,虽然啰嗦了点。
真正别扭的是------你想加个自定义按钮?可以,但样式得自己写。
RadzenHtmlEditorButton 是有的,你可以加自定义按钮、绑点击事件。但按钮的图标、大小、hover 效果,和内置按钮不一样。你得自己调 CSS 才能让它看起来像是一组的。
还有个更细节的问题------编辑器的高度。
默认编辑器高度是跟着内容走的,内容多了就往下撑。这在某些场景下没问题,但如果你想让编辑器固定高度、内容滚动呢?
你会发现没有 Height 参数。设置 Style 高度也没用,因为内部的 contenteditable 元素不会跟着撑。
最后还是得自己写 CSS 覆盖内部样式。
⚠️ 别扭在哪: 默认模式下开箱即用,但一旦需要定制,就进入了「全手动」模式。没有中间地带------没有「默认工具栏但去掉某几个按钮」的便捷方式,也没有和内置按钮风格一致的自定义按钮模板。
6. 不是黑,是希望它更好
说了这么多「坏话」,得说句公道话。
Radzen 是 Blazor 生态里组件数量最多、功能覆盖最全面的开源组件库之一,这点没什么争议。从基础的表单控件到复杂的 DataGrid、Scheduler、HtmlEditor,覆盖面很广。
而且它是真开源,MIT 协议,没有商业版功能阉割这一说。这点必须给个赞。
但「能用」和「好用」之间,还是有距离的。
上面吐槽的这几个组件,不是不能用,是 API 设计上总感觉少想了一步------少一个 Reload 方法、少一个 GoToPage 方法、少一个读取文件的 API、少一个高度参数。
这些东西说大不大,加起来可能也就几十个方法和参数。但缺了它们,开发者每次用的时候都要绕一下、hack 一下、自己封装一下。
积少成多,项目里就多了一堆「适配 Radzen」的胶水代码。
一句话总结: Radzen 组件库量大管饱,但部分组件的 API 设计偏「够用就行」,离「优雅好用」还有差距。核心组件建议自己包一层,把别扭的地方挡在业务代码外面。
当然,以上都是我个人的使用感受。
如果你有不同的看法,或者有更巧妙的使用姿势,欢迎评论区聊聊。
毕竟 Blazor 圈子不大,多交流总是好的。
关于作者
码农刚子,六年 ERP / 制造业系统开发,专注 C# / .NET / Blazor。 写 PCB 行业 ERP 与多租户 SaaS 的实战与踩坑,不写面试八股。
博客:https://www.coderlog.net/ | 公众号:CSharp精选营