You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor 7:如何同步两个Virtualize组件?

解决Blazor双Virtualize组件行对齐同步问题

核心问题出在两个Virtualize组件各自独立计算可见范围,加上JS滚动同步的时序误差,导致偶尔出现加载行数不一致、行对齐失效。以下是可落地的修复方案:


1. 强制统一视图容器与行高

两个表格的父容器必须保证高度完全一致,避免padding、margin或边框差异导致Virtualize计算的可见行数不同:

  • 给左右表格容器设置相同的固定高度,或绑定同一个动态计算的高度值
  • 给所有行设置固定高度(如tr { height: 40px; }),如果行高动态变化,提前在C#端计算每一行的精确高度并传给Virtualize的ItemSize参数:
    <Virtualize ItemsProvider="@LoadRows" ItemSize="@RowHeight" ...>
    
    其中RowHeight是预先计算好的每行高度值,确保左右Virtualize使用完全相同的ItemSize。

2. 共享滚动状态与可见行索引

放弃JS双向同步scrollTop,改为C#端统一管理滚动状态:

  • 在组件中定义共享的滚动状态变量:
    private int _sharedScrollTop;
    private int _firstVisibleIndex;
    
  • 给左侧表格绑定滚动事件,通过JSInterop将scrollTop同步到_sharedScrollTop,然后强制右侧表格滚动到该值:
    <div id="leftTableContainer" @onscroll="OnLeftScroll">...</div>
    
    private async Task OnLeftScroll()
    {
        _sharedScrollTop = await JSRuntime.InvokeAsync<int>("getScrollTop", "leftTableContainer");
        await JSRuntime.InvokeVoidAsync("setScrollTop", "rightTableContainer", _sharedScrollTop);
        // 强制同步可见行索引
        _firstVisibleIndex = leftVirtualize.FirstVisibleIndex;
        rightVirtualize.FirstVisibleIndex = _firstVisibleIndex;
        await rightVirtualize.RefreshDataAsync();
    }
    
  • 让两个Virtualize的FirstVisibleIndex绑定到同一个_firstVisibleIndex变量,确保起始加载行完全一致。

3. 统一ItemsProvider的请求参数

不要依赖Virtualize自动生成的request.Count,而是手动计算当前视图能容纳的行数,让左右两个ItemsProvider使用相同的count值:

private int CalculateVisibleRowCount()
{
    // 根据容器高度和行高计算可见行数
    var containerHeight = await JSRuntime.InvokeAsync<int>("getElementHeight", "tableContainer");
    return (int)Math.Ceiling(containerHeight / (double)RowHeight) + 2; // 额外加载2行避免空白
}

private async ValueTask<ItemsProviderResult<VirtualizedRow>> LoadFixedRows(ItemsProviderRequest request)
{
    var visibleCount = CalculateVisibleRowCount();
    var startIndex = _firstVisibleIndex >= 0 ? _firstVisibleIndex : request.StartIndex;
    var rows = VisibleRows.Skip(startIndex).Take(visibleCount).ToList();
    return new ItemsProviderResult<VirtualizedRow>(rows, VisibleRows.Count);
}

// 右侧滚动列的ItemsProvider使用完全相同的逻辑
private async ValueTask<ItemsProviderResult<VirtualizedRow>> LoadScrollableRows(ItemsProviderRequest request)
{
    var visibleCount = CalculateVisibleRowCount();
    var startIndex = _firstVisibleIndex >= 0 ? _firstVisibleIndex : request.StartIndex;
    var rows = VisibleRows.Skip(startIndex).Take(visibleCount).ToList();
    return new ItemsProviderResult<VirtualizedRow>(rows, VisibleRows.Count);
}

4. 优化JS同步逻辑,避免竞争条件

如果仍需要JS同步,使用requestAnimationFrame确保滚动同步在浏览器重绘前执行,并且避免循环触发:

window.syncScroll = function(sourceElId, targetElId) {
    const source = document.getElementById(sourceElId);
    const target = document.getElementById(targetElId);
    let isSyncing = false;

    source.addEventListener('scroll', () => {
        if (isSyncing) return;
        isSyncing = true;
        requestAnimationFrame(() => {
            target.scrollTop = source.scrollTop;
            isSyncing = false;
        });
    }, { passive: true });
};

只给一个方向绑定同步(比如左侧滚动同步右侧),禁止右侧滚动触发反向同步,避免死循环。


内容的提问来源于stack exchange,提问作者Grandizer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 03:51:28