聊聊 Blazor 里 Radzen 6.0.0 那几个用着别扭的官方组件

大家好,我是码农刚子。最近把项目里的 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精选营

相关推荐
known13 天前
基于Blazor实现的项目管理系统
blazor·known
界面开发小八哥1 个月前
界面控件DevExpress Blazor v26.1新版亮点 - Filter Builder等功能升级
ui·界面控件·blazor·devexpress·ui开发
界面开发小八哥1 个月前
界面控件DevExpress Blazor v26.1新版亮点 - Navigation(导航)
javascript·.net·界面控件·blazor·devexpress·ui开发
界面开发小八哥1 个月前
界面控件DevExpress Blazor v26.1新版亮点 - TreeList & 数据编辑器
javascript·编辑器·.net·界面控件·blazor·devexpress·用户界面
界面开发小八哥1 个月前
界面控件DevExpress XAF v26.1新版亮点——Blazor UI功能增强(二)
ui·界面控件·blazor·devexpress·ui开发
known2 个月前
基于Blazor实现的轻量进销存管理系统
blazor
界面开发小八哥2 个月前
界面控件DevExpress Blazor v26.1新版亮点 - 辅助功能增强
.net·界面控件·blazor·devexpress·ui开发
known3 个月前
基于Blazor实现的光伏设备调试软件
blazor
known4 个月前
基于Blazor实现的跟踪光伏智能运维平台
blazor