collectionview 刷新时的 DataGrid 虚拟化

问题描述 投票:0回答:2

具体来说,这是这个问题 DataGrid 过滤器性能的后续问题,但是 StackOverflow 上还有许多与 WPF DataGrid 性能相关的类似问题。

经过大量分析和浏览 .NET 源代码后,我意识到许多性能问题,例如过滤和排序,归结为一个问题:CollectionView.Reset 事件不会回收容器(如滚动)是)。

我的意思是,不是为现有行分配新的数据上下文,而是从可视化树中删除所有行,生成并添加新行,并执行布局周期(测量和排列)。

所以主要问题是: 有人成功解决这个问题吗?例如。通过手动操作 ItemContainerGenerator,或通过创建自己的 DataGridRowsPresenter 版本?

这就是迄今为止我的方法的要点。

public class CollectionViewEx
{
    public event EventHandler Refresh;

    public override void Refresh()
    {
        Refresh?.Invoke(this, EventArgs.Empty);
    }
}

public class DataGridEx : DataGrid
{
    protected override OnItemsSourceChanged(IEnumerable oldSource, IEnumerable newSource)
    {
        if (newSource is CollectionViewEx cvx)
        {
            cvx.Refresh += (o,a) => OnViewRefreshing;
        }
    }

    private void OnViewRefreshing()
    {
        RowsPresenter.Refresh();
    }
}

public class DataGridRowsPresenterEx : DataGridRowsPresenter
{
    public void Refresh()
    {
        var generator = (IRecyclingItemContainerGenerator)ItemContainerGenerator;

        generator.Recycle(new GeneratorPosition(0, 0), ???);         
        RemoveInternalChildRange(0, VisualChildrenCount);

        using (generator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward))
        {
            UIElement child;
            bool isNewlyRealised = false;
            while ((child = generator.GenerateNext(out isNewlyRealised) as UIElement) != null)
            {
                AddInternalChild(child);
                generator.PrepareItemContainer(child);
            }
        }
    }
}

但结果非常令人困惑 - 显然是因为我不太明白如何与 ICG 合作。

我浏览了 .net 源代码以查看它们的实现(添加/删除/替换项目时),并且还找到了一些关于如何创建新的虚拟化面板(例如 virtualizingwrappanel)的在线资源,但没有一个真正解决这个特定问题问题,我们希望将所有现有容器重新用于一组新的项目

所以第二个问题是:任何人都可以解释这种方法是否可行吗?我该怎么做?

wpf performance datagrid virtualization
2个回答
0
投票

我从不直接从 CollectionView 使用重置,因为 CollectionView 的 Source 上的方法可以做到这一点。这个 IList 是根据我的需要修改的。我按照 Paul McClean 解释的那样做了这里

在这个类中你可以通知OnCollectionChanged来通知CollectionView。 sondergard 解释了 NotifyCollectionChangedAction.Reset 的作用。但 NotifyCollectionChangedAction.Replace 继续运行项目的回收。

也许我的研究有帮助。


0
投票

我最近在一个相当复杂的 DataGrid 中遇到了同样的问题,并设法找到了一个有效的解决方案here

这有点 hacky,因为它使用反射来访问回收队列,但它工作得很好。 我所做的是用另一个类包装集合,该类拦截

Reset
并保存所有当前项目,引发
CollectionChanged
事件,然后将所有这些项目放回到(内部)回收队列中。

    class MyGridCollection : IList, INotifyCollectionChanged
    {
        IList _collection;
        swc.DataGrid _grid;
        swcp.DataGridRowsPresenter _presenter;

        public MyGridCollection(IList collection, swc.DataGrid grid)
        {
            _collection = collection;
            if (_collection is INotifyCollectionChanged collectionChanged)
                collectionChanged.CollectionChanged += OnCollectionChanged;
            _grid = grid;
        }

        public void Unregister()
        {
            if (_collection is INotifyCollectionChanged collectionChanged)
                collectionChanged.CollectionChanged -= OnCollectionChanged;
        }

        public object this[int index]
        {
            get => _collection[index];
            set => _collection[index] = value;
        }

        public bool IsReadOnly => _collection.IsReadOnly;
        public bool IsFixedSize => _collection.IsFixedSize;
        public int Count => _collection.Count;
        public object SyncRoot => _collection.SyncRoot;
        public bool IsSynchronized => _collection.IsSynchronized;

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public int Add(object value) => _collection.Add(value);

        public void Clear() => _collection.Clear();

        public bool Contains(object value) => _collection.Contains(value);

        public void CopyTo(Array array, int index) => _collection.CopyTo(array, index);

        public IEnumerator GetEnumerator() => _collection.GetEnumerator();

        public int IndexOf(object value) => _collection.IndexOf(value);

        public void Insert(int index, object value) => _collection.Insert(index, value);

        public void Remove(object value) => _collection.Remove(value);

        public void RemoveAt(int index) => _collection.RemoveAt(index);

        static readonly FieldInfo s_queueField = typeof(swc.ItemContainerGenerator).GetField("_recyclableContainers", BindingFlags.Instance | BindingFlags.NonPublic);

        void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            if (args.Action == NotifyCollectionChangedAction.Reset && s_queueField != null)
            {
                _presenter ??= _grid.FindChild<swcp.DataGridRowsPresenter>();
                if (_presenter != null)
                {
                    var existingRows = new List<sw.DependencyObject>();

                    IList list = _presenter.Children;
                    for (int i = 0; i < list.Count; i++)
                    {
                        if (list[i] is sw.DependencyObject child)
                            existingRows.Add(child);
                    }
                    CollectionChanged?.Invoke(this, args);

                    if (s_queueField.GetValue(_grid.ItemContainerGenerator) is Queue<sw.DependencyObject> queue)
                    {
                        for (int i = 0; i < existingRows.Count; i++)
                        {
                            queue.Enqueue(existingRows[i]);
                        }
                        return;
                    }
                }
            }
            CollectionChanged?.Invoke(this, args);
        }
    }

可以像这样使用:

myDataGrid.ItemsSource = new MyGridCollection(actualCollection, myDataGrid);

这可以使性能至少达到可接受的水平。 理想情况下,有一种方法可以在不反思的情况下做到这一点,但我还没有找到一种方法来做到这一点。

© www.soinside.com 2019 - 2024. All rights reserved.