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

虚拟化DataGrid刷新后保持滚动位置的问题求助

解决虚拟化DataGrid刷新后滚动位置丢失的问题

我之前也踩过这个坑——虚拟化DataGrid替换数据源后,滚动位置总是跳回顶部,明明记录了VerticalOffset却没用。核心问题在于你直接替换了整个ICollectionView,导致DataGrid的虚拟化容器全部被销毁,这时候立刻设置滚动偏移,新的虚拟项还没生成,自然不会生效。结合你的代码,给你几个可行的解决方案:

方案一:不替换ICollectionView,只更新源数据(推荐)

如果你的数据源结构没有大变化,尽量保留原来的ICollectionView,只更新其底层的集合内容,这样虚拟化的状态能完整保留,滚动位置也不会丢失。

修改你的刷新逻辑:

// ViewModel中先定义底层可观察集合(初始化时创建)
private ObservableCollection<YourItemType> _sourceCollection = new ObservableCollection<YourItemType>();
private ICollectionView _itemCollection;

// 构造函数初始化ICollectionView
public YourViewModel()
{
    _itemCollection = CollectionViewSource.GetDefaultView(_sourceCollection);
}

// 刷新集合的函数改成这样:
public void RefreshCollection()
{
    // 先清空并更新源集合
    _sourceCollection.Clear();
    var orderedItems = Items.OrderByDescending(item => item.BeginDate);
    foreach (var item in orderedItems)
    {
        _sourceCollection.Add(item);
    }

    // 刷新现有ICollectionView的分组和过滤,不用重新赋值
    using (_itemCollection.DeferRefresh())
    {
        _itemCollection.GroupDescriptions.Clear();
        _itemCollection.GroupDescriptions.Add(new PropertyGroupDescription("ShortStartDate"));
        _itemCollection.Filter = ApplyFilter;
    }
}

这样DataGrid的ItemsSource没有被替换,虚拟化容器得以复用,滚动位置会自动保留,根本不需要手动记录和恢复。

方案二:必须替换ICollectionView时,延迟设置滚动位置

如果因为业务需求必须替换整个ICollectionView,那要等DataGrid完成新项的虚拟化加载后,再设置滚动位置。可以通过Dispatcher延迟执行,确保UI布局完成。

修改你的SetScrollPosition函数:

public void SetScrollPosition()
{
    if (Test != null && _currentScrollPosition != 0)
    {
        var scrollViewer = GetScrollViewer(Test);
        if (scrollViewer != null)
        {
            // 用Background优先级的Dispatcher,等待UI布局完成后再滚动
            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                scrollViewer.ScrollToVerticalOffset(_currentScrollPosition);
                // 强制更新布局,确保滚动生效
                Test.UpdateLayout();
            }), DispatcherPriority.Background);
        }
    }
}

另外,优化你的GetScrollViewer方法,直接通过DataGrid的模板获取,比遍历VisualTree更可靠:

private ScrollViewer GetScrollViewer(DataGrid dataGrid)
{
    return dataGrid.Template.FindName("DG_ScrollViewer", dataGrid) as ScrollViewer;
}

方案三:记录唯一项,滚动到具体项(应对分组变化)

如果刷新前后分组结构发生变化,原来的VerticalOffset对应的逻辑位置已经失效,这时候记录滚动位置不如记录一个唯一标识的项,刷新后找到该项并滚动到它。

1. 记录当前可见项的唯一ID

private int? _currentVisibleItemId; // 假设你的数据项有唯一的Id属性

private void GetScrollPosition()
{
    if (Test != null)
    {
        var scrollViewer = GetScrollViewer(Test);
        if (scrollViewer != null)
        {
            _currentScrollPosition = scrollViewer.VerticalOffset;
            // 获取第一个可见的DataGridRow对应的项
            var firstVisibleItem = GetFirstVisibleDataItem(Test);
            if (firstVisibleItem != null)
            {
                _currentVisibleItemId = firstVisibleItem.Id;
            }
        }
    }
}

private YourItemType GetFirstVisibleDataItem(DataGrid dataGrid)
{
    var virtualizingPanel = GetVisualChild<VirtualizingStackPanel>(dataGrid);
    if (virtualizingPanel != null && virtualizingPanel.Children.Count > 0)
    {
        var firstRow = virtualizingPanel.Children[0] as DataGridRow;
        return firstRow?.Item as YourItemType;
    }
    return null;
}

private T GetVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);
        if (child is T tChild) return tChild;
        
        var nestedChild = GetVisualChild<T>(child);
        if (nestedChild != null) return nestedChild;
    }
    return null;
}

2. 刷新后滚动到目标项

public void SetScrollPosition()
{
    if (Test != null && _currentVisibleItemId.HasValue)
    {
        // 从新的ICollectionView中找到目标项
        var targetItem = ItemCollection.Cast<YourItemType>()
                                      .FirstOrDefault(item => item.Id == _currentVisibleItemId.Value);
        
        if (targetItem != null)
        {
            // 先滚动到该项,确保它在视图中
            Test.ScrollIntoView(targetItem, Test.Columns[0]);
            
            // 延迟微调位置,还原到原来的相对位置
            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                var scrollViewer = GetScrollViewer(Test);
                if (scrollViewer != null)
                {
                    // 可以根据需要调整偏移,比如回到项的中间位置
                    scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - (scrollViewer.ViewportHeight / 2));
                }
            }), DispatcherPriority.Background);
        }
    }
}

这三种方案里,方案一是最省心的,能最大化保留虚拟化的性能,同时避免滚动位置问题。如果必须替换数据源,方案二或三可以解决你的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:48:56