最近把项目里的 Radzen 从 5.x 升到了 6.0.0。
升级本身挺顺利,Breaking Changes 不算多。但用着用着就发现,有那么几个官方组件,API 设计得总让人觉得「差那么一口气」。
不是不能用,是用起来别扭。每次写都要翻文档、猜参数、然后心里嘀咕一句「这也太绕了吧」。
今天挑几个最有代表性的聊聊,顺便说说我是怎么绕过去的。纯个人感受,不同意的话评论区切磋。
1. RadzenDataGrid:分页器的「假联动」
先从最常用的 DataGrid 说起。
RadzenDataGrid 整体其实还不错,功能全、性能也过得去。但有个地方我一直用着别扭——自定义分页器和 DataGrid 的联动方式。
场景是这样的:我想在表格顶部加一个搜索框,下面是表格,底部是分页器。分页器不想用内置的,要自己写,因为要加「每页 N 条」切换和跳转到指定页。
按正常 Blazor 组件的思路,分页器改了之后,调一下 DataGrid 的某个方法或者改个参数,表格就刷新了对吧?
Radzen 偏不。
你得这么写:
<!-- 搜索框 -->
<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 的实现思路是对的,但参数传递的方式,我觉得设计得有点随意。
打开对话框的代码长这样:
var result = await DialogService.OpenAsync<EditUserDialog>(
"编辑用户",
new Dictionary<string, object>
{
{ "UserId", userId },
{ "UserName", userName },
{ "IsAdmin", isAdmin }
});
然后弹窗组件那边用 [Parameter] 接收:
// 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 方法,把参数封装起来:
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 的用法:
<RadzenUpload Url="api/upload"
OnChange="@OnUploadChange"
OnProgress="@OnUploadProgress"
OnComplete="@OnUploadComplete"
Multiple="true" />
看着挺正常。Url 指定上传地址,事件回调状态。
但问题是,这个组件是「自动上传」的——用户一选文件,它立刻就往 Url 发请求了。
如果你想让用户先选文件、预览一下、点个确认按钮再上传呢?
不好意思,没有内置支持。你得用一个很别扭的方式绕——把 Url 设成空字符串,在 OnChange 事件里自己处理文件,然后手动调上传。
更绝的是 OnChange 事件的参数。
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 回调。 组件告诉你当前视图的起止时间,你去后端查对应时间范围内的数据,返回给它。
模式二听起来很合理,按需加载嘛。但实际用起来有个大坑——你没法主动刷新数据。
<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:工具栏的「半残」状态
富文本编辑器这个组件,我纠结了很久要不要放进来。
因为它的基础功能其实还行——加粗、斜体、列表、链接、图片,常用的都有。但一旦你要自定义工具栏,事情就变得诡异起来。
默认工具栏是这样的:
<RadzenHtmlEditor @bind-Value="@htmlContent" />
一行搞定,工具栏按钮全给你安排好了。
但如果你想只保留一部分按钮呢?或者想调整一下按钮顺序呢?
那你就得把所有想要的按钮一个一个手写出来:
<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 圈子不大,多交流总是好的。
暂无评论,来抢沙发吧~