【问题标题】:Virtualization on the ViewModel layer in WinRTWinRT 中 ViewModel 层的虚拟化
【发布时间】:2014-09-11 10:14:54
【问题描述】:

WPF 和 WinRT (C# + XAML) 都使用支持 UI 虚拟化的面板(例如 VirtualizingStackPanel 等)来支持 UI 虚拟化。使用 MVVM 时,它是使用某种ItemsControl 来完成的(ListBoxGridView 等...),它绑定到视图模型上的可枚举属性(通常是 ObservableCollection)。 items 控件仅为可见的项目创建 UI。之所以称为 UI 虚拟化,是因为只有 UI 是虚拟化的。只有未呈现的项目的视图不会被创建,并且会延迟到用户实际滚动到项目的那一刻。列表中的视图模型对象都是预先创建的。因此,如果我有一个 100,000 人的列表要呈现,ObservableCollection 将必须包含 100,000 个视图模型,无论用户何时将它们滚动到视图中。

在我们的应用程序中,我们希望实现它,以便视图模型层成为这种虚拟化的一部分。我们希望 items 控件呈现一个适合可能加载的项目总数的滚动条(因此 observable 集合应该使 items 控件认为它已经包含 100,000 个项目,因此滚动条视图端口位于合适的大小),但我们希望在新项目即将进入视野时通知可观察集合,以便它可以从服务器加载实际对象。我们希望能够在加载的项目中显示某种进度指示器,然后在项目加载到可观察集合后立即将其替换为项目的实际数据模板。

我们希望尽可能地保持 MVVM 准则,但性能和响应能力是重中之重。如果可能,我们也更喜欢可重复使用的解决方案。

解决这个问题的最佳方法是什么?

【问题讨论】:

  • 这个问题太宽泛了,无法回答。当然,您已经对应该做什么有了一些想法。请把它分解成可以回答的小问题。
  • 我不明白为什么它太宽泛了......我描述了一个非常具体的场景,我想知道是否已经有最佳实践。肯定有人以前尝试过。
  • 您想要:a) 能够虚拟化其数据的集合,b) 知道如何处理虚拟化数据源的自定义 ItemsControl,c) ItemsControl 和集合之间的通知机制 d) a表示 ItemsControl 中的项目的自定义控件,它知道它是否已加载,并且 e) (a)、(b)、(c) 和 (d) 应该是高性能的。除了听起来很宽泛(至少对我来说,YMMV),我几乎可以保证没有“最好的方法”来解决它。
  • 我想在视图模型层允许虚拟化。那是一个)。我认为不需要特殊的项目控件,或者超出正常属性更改和集合更改机制的通知机制。我认为不需要自定义控件,因为数据模板可以实现我编写的所有内容。但是,如果有一个解决方案确实涉及您所写的所有内容,那么它也会被接受。是的,性能是一个因素。我不是要一个完整的解决方案的源代码,只是关于如何开始解决这个问题的指南。为什么这么困扰你?
  • 它并没有特别困扰我。我只是认为这个问题太宽泛了,因此给了它一个近距离的投票。其他人可能会也可能不会效仿(需要 5 票接近才能结束问题)。这不是为了惹恼你;这样做是为了保持问题的质量水平,并且至少是为了向发布者发出一个信号,即可能需要改进或附加细节才能回答问题,我认为这也是你的目的。

标签: c# xaml mvvm winrt-xaml ui-virtualization


【解决方案1】:

实际上,WinRT ItemsControls 已经能够使用两种方式处理数据虚拟化: 1) 在实现 IList 或 IObservableVector 的自定义类中或通过从 ObservableCollection 继承来实现 ISumportIncrementalLoading。这个方法真的很简单,但是它只支持线性滚动(你不能跳过数据从第一个元素立即滚动到第 1000000 个元素),并且每次加载新页面时滚动条都会自动调整大小

2) 自己实现IObservableVector,第一次访问一个item,直接返回null,开始加载过程。加载后,您可以引发 VectorChanged 事件,指示该项目不再为空。这实现起来相当复杂(很难依赖现有的 ObservableVector 实现),但它支持非线性滚动,您甚至可以添加逻辑以在控件长时间未访问时卸载项目(从而节省内存并仅在需要时重新加载它们)。

【讨论】:

  • 谢谢,非常详细。在我们的案例中,我认为选项 2 可能最符合要求。除了 IObservableVector 和 IList 还需要实现 INotifyCollectionChanged 还是在 WinRT 中已过时?
  • 你只需要实现 IObservableVector。 INotifyCollectionChanged 不是强制性的(它是仅为复古兼容性而设计的语言)
  • 原来我特别需要实现 IObservableVector (其他类型参数不起作用)。接口有很多与本场景无关的方法,我只需要处理Count和This[](get)即可。
  • 你会如何“卸载项目,当他们没有被访问......很长一段时间”?当用户快速滚动时,我认为虚拟化应该与 UI 与虚拟化一样快。它需要根据视口清除它们。我认为 IObservableVector 不会提供该信息。如果是这样的话,当内存是一个严重的问题时,我认为这个 IObservableVector 是一个糟糕的选择。
  • 我同意,您需要在视图和视图模型之间进行更好的合作。滚动操作应该以某种方式报告给视图模型,然后虚拟化向量应该清除项目。在我们的例子中,要求不是为了节省内存,只是为了支持视图模型项的延迟加载
【解决方案2】:

我最终根据 Simon Ferques 的指南制作了 POC。我在这里添加代码以供将来参考。

        public class VirtualizaingVector<T> : ObservableObject, IObservableVector<object>
        {

        public event VectorChangedEventHandler<object> VectorChanged;

        private Dictionary<int, T> _items;

        private int _count;
        private bool _countCalculated;

        private IItemSupplier<T> _itemSuplier;

        public VirtualizaingVector(IItemSupplier<T> itemSupplier)
        {
            _itemSuplier = itemSupplier;
            _items = new Dictionary<int, T>();
        }

        #region Notifications

        private void _notifyVectorChanged(VectorChangedEventArgs args)
        {
            if (VectorChanged != null)
            {
                VectorChanged(this, args);
            }
        }

        private void _notifyReset()
        {
            var args = new VectorChangedEventArgs(CollectionChange.Reset, 0);
            _notifyVectorChanged(args);
        }

        private void _notifyReplace(int index)
        {
            var args = new VectorChangedEventArgs(CollectionChange.ItemChanged, (uint)index);
            _notifyVectorChanged(args);
        }

        #endregion

        #region Private

        private void _calculateCount()
        {
            _itemSuplier.GetCount().ContinueWith(task =>
            {
                lock (this)
                {
                    _count = task.Result;
                    _countCalculated = true;
                }

                NotifyPropertyChanged(() => this.Count);
                _notifyReset();
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void _startRefreshItemAsync(T item)
        {
            var t = new Task(() =>
            {
                _itemSuplier.RefreshItem(item);
            });

            t.Start(TaskScheduler.FromCurrentSynchronizationContext());
        }

        private void _startCreateItemAsync(int index)
        {
            var t = new Task<T>(() =>
            {
                return _itemSuplier.CreateItem(index);
            });

            t.ContinueWith(task =>
            {
                lock (this)
                {
                    _items[index] = task.Result;
                }
                _notifyReplace(index);
            }, TaskScheduler.FromCurrentSynchronizationContext());

            t.Start(TaskScheduler.FromCurrentSynchronizationContext());
        }


        #endregion

        public object this[int index]
        {
            get
            {
                T item = default(T);
                bool hasItem;

                lock (this)
                {
                    hasItem = _items.ContainsKey(index);
                    if (hasItem) item = _items[index];
                }

                if (hasItem)
                {
                    _startRefreshItemAsync(item);
                }
                else
                {
                    _startCreateItemAsync(index);
                }

                return item;
            }
            set
            {
            }
        }

        public int Count
        {
            get
            {
                var res = 0;
                lock (this)
                {
                    if (_countCalculated)
                    {
                        return res = _count;
                    }
                    else
                    {
                        _calculateCount();
                    }
                }

                return res;
            }
        }

    #region Implemenetation of other IObservableVector<object> interface - not relevant
    ...
    #endregion
}
    public interface IItemSupplier<T>
    {
        Task<int> GetCount();

        T CreateItem(int index);

        void RefreshItem(T item);
    }

几点说明:

  1. 虽然向量是 T 的可枚举,但它实现的接口是 IObservableVector。原因是由于某种原因,WinRt 项目控件不听任何IObservableVector&lt;T&gt;,只听IObservableVector&lt;object&gt;。悲伤但真实...
  2. 虚拟化vercor以物品供应商为参数并使用 它为了查询虚拟列表中的项目数, 以及物品本身。它还允许项目供应商 删除项目后再次访问时刷新项目 从缓存
    1. 此类是为特定场景编写的,其中内存不是问题,但时间是问题。它确实保留了一个已经创建的缓存项目列表,但它会延迟它们的创建,直到它们被第一次访问。

【讨论】:

  • “不听任何 IObservableVector,只听 IObservableVector。悲伤但真实……” - 你的意思是“IObserableVector,只听 IObserableVector”?
  • urgh - IObservableVector 是的,这使得平台调用“公共对象 this[int index]”。否则,使用 IObserableVector 平台仅调用 GetEnumerator() ,这与使用 IList 实现没有任何不同
  • 嗨,kobi,也许你能帮我解决我的类似问题? stackoverflow.com/questions/27361856/…
  • 嗨,这里似乎有 2 个问题 ..1)如果 IObservableVector 和 T 是项目中声明的类,则会引发运行时异常,并且 2)如果 T 被声明为Windows 运行时组件项目中兼容的 WinRT 组件,则可以使用它,但由于某种原因需要枚举器....我需要进一步研究,但任何人都可以阐明为什么在 T 对象时使用枚举器?
【解决方案3】:

如果您正在处理远程和/或异步后端,可能会有不同的分页安排,那么这是我对这个问题的回答: CodeProject article on Rx and IObservableVector

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-01
    • 2017-06-15
    • 1970-01-01
    • 1970-01-01
    • 2012-03-11
    相关资源
    最近更新 更多