65.9K
CodeProject 正在变化。 阅读更多。
Home

在Windows Store应用中使用XAML、C#和Rx进行随机存取数据虚拟化的实验

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015年5月26日

CPOL

10分钟阅读

viewsIcon

14072

downloadIcon

144

在这里,我分享了一个研发实验的代码,该实验旨在探索数据虚拟化的替代方法,在Windows Store应用中使用Rx和XAML ListView,我将IObservableVector视为一个“观察”集合,它对UI事件流作出反应。

引言

这段代码是一个实验和一些研究,关于如何为Windows Store应用实现随机存取数据虚拟化,以允许显示来自网络服务等异步访问源的潜在庞大数据列表。

这最初是我自己的一次学习练习,旨在一次性掌握多个概念。我想学习Windows Store应用开发和WinRT API,同时学习Reactive Extensions(即Rx)。

请注意,这不旨在解决数据虚拟化和流畅UI的所有细节,例如占位符等。

我分享这个有两个原因。一个可能是为任何可能需要它的人提供一种解决方案,另一个是吸引一些关于这个话题的反馈,以帮助进一步学习。欢迎对替代方法、框架、错误或做法的任何评论!

下载 VirtualizingCollection-noexe.zip

背景

我最初的目标是使用电影数据库网络服务来显示一个可导航的列表,其中电影的数量不确定且可能非常多。我希望用户能够轻松地访问列表中的数万甚至更多项。这个场景相当人为。通常用户不希望有如此大量的数据可用,但我可以想象在某些情况下这种灵活性可能是有益的,并且想为了学习目的探索这个技术挑战。我想在Windows Store应用中,使用C#和XAML来完成这个。

为了实现流畅、高性能且不占用大量内存的列表,可以遵循Microsoft的建议,例如这些:

https://msdn.microsoft.com/en-us/library/windows/apps/hh994637.aspx

其中的两个关键概念是:

  • UI虚拟化
  • 数据虚拟化

一个包含数万或数十万条记录的电影描述列表,将导致生成数十万个UI元素,消耗内存和其他资源,可能导致应用程序崩溃。

UI虚拟化通过确保只在列表中创建一定数量的UI元素来解决这个问题,新的元素在滚动时创建,旧的元素被移除。

在Windows 8.1 XAML中实现这一点似乎相当简单,只需确保ListView的ItemsPanel是支持UI虚拟化的(ItemsWrapGrid)。

<ListView.ItemsPanel>
	<ItemsPanelTemplate>
		<ItemsWrapGrid  Orientation="Horizontal" />
	</ItemsPanelTemplate>
</ListView.ItemsPanel>

更具挑战性的问题是如何处理可能具有以下一个或多个属性的后端数据源:

  • 记录数量非常庞大
  • 只能通过指定页码和页面大小进行访问(例如,一个不允许一次下载所有记录集的私有网络服务)
  • 项目/页面异步加载

上述MS文档描述了两种处理此问题的方法,通常都称为数据虚拟化。

  • 增量数据虚拟化
  • 随机存取数据虚拟化

增量数据虚拟化是一种随着用户向列表末尾滚动而增加记录数量的方法。这类似于网页开发中常见的技术,向下滚动会导致新项目添加到HTML页面。在这两种情况下,如果存在滚动条,滚动条会随着新项目逐步加载而改变大小。

随机存取数据虚拟化是指用户可以跳转到集合中的任何位置。用户应该能够快速跳到列表的末尾、中间或任何其他位置,并且集合会提供UI所需的一些记录。从UI的角度来看,集合是一个通过索引访问的预填充列表,但集合只是虚拟的。它“假装”拥有这些记录,但实际上是代理其他源。

我选择使用第二种方案,微软对此的声明如下:

乐趣就从这里开始了。

理解代码

对我而言,微软建议背后的概念大概是这样的:

 

当UI虚拟化的ListView请求一些元素时,集合会通过索引被访问,集合应该用数据进行响应。如果存在延迟或更改,ListView会观察到相应的通知。

对于我这样一个新手来说,在这个领域,第一个令人困惑的地方是以下一点:

  • ObservableCollectionIObservableVector毫无关系。

IObservableVector是WinRT类型。网上有很多容易混淆的文档可能会让人认为ObservableCollectionIObservableVector somehow是相关概念,但据我所知,它们不是。我花了好长时间才意识到,那些搜索结果中与ObservableCollection相关的信息在这种情况下毫无用处。

撇开这一点不谈,在考虑了这些概念并研究了该主题之后,我得出结论,“真相”可能更接近以下几点(或者至少,我更喜欢这种不同的概念安排):

为了解释我为什么更喜欢这种观点,请考虑以下步骤序列:

  1. 启用了UI虚拟化的ListView被跳过,显示记录1000到1020。
  2. ListView使用[1000]...[1019]索引访问集合。
  3. 集合尚未拥有该数据。它计算出后端源(例如,可能具有不同的分页方案)应该提供第25页(例如),最小页面大小为50。它向ListView返回空项或占位符项。
  4. 集合向后端源发出请求以获取正确的数据。
  5. 集合稍后异步接收数据。
  6. 集合通过VectorChanged事件通知其观察者(ListView),项目975到1025已更新。

如果例如有两个ListView正在观察同一个Collection,那么就会出现问题。两个ListView可能正在访问虚拟Collection的不同区域,但两者都会在步骤6中收到对方异步接收到的数据的通知。我进行的测试表明,来自IObservableVector的通知可能会导致创建相应的UI元素。似乎Collection和ListView之间最好是一对一的关系。第二个ListView不需要被告知它没有请求的数据。

对我来说,在撰写本文时,将IObservableVector视为一个“ObservingVector”似乎更有意义,它通过其索引器响应其ListView。换句话说,索引器可以被视为ListView调用的事件源,触发异步请求流和响应流,通过后端,通过通知(VectorChanged)流向ListView。

就在这时,我决定尝试将 Reactive Extensions 引入其中。当时我对 Rx 的全部了解是它擅长处理异步流。这是一个学习的机会。对于尚未了解 Rx 的人,我强烈推荐它。它的本质如下:

在.NET中,我们有IEnumerable和IEnumerator。可以使用Linq查询来过滤记录序列。

  • IEnumerable接口公开了一个方法GetEnumerator(),它返回一个IEnumerator来遍历此集合。IEnumerator允许我们获取当前项(通过返回Current属性),并确定是否还有更多项可遍历(通过调用MoveNext方法)。

Rx 认为这是一种“拉取模型”。例如,您的 Linq 查询是使用 IEnumerators 从 IEnumerable(s) 中拉取数据。Rx 所做的就是提供一组扩展,提供一种“推送模型”,允许您通过另外两个接口,即 IObserver 和 IObservable,做完全相同的事情。同样来自 Rx 文档:

  • Rx实现的推模型由IObservable/IObserver的可观察模式表示。IObservable接口是熟悉的IEnumerable接口的对偶。它抽象了一个数据序列,并维护了一个对数据序列感兴趣的IObserver实现列表。IObservable将自动通知所有观察者任何状态更改。要通过订阅注册兴趣,您可以使用IObservable的Subscribe方法,该方法接受一个IObserver并返回一个IDisposable。这使您能够跟踪和处置订阅。此外,Rx在可观察序列上的LINQ实现允许开发人员对基于推送的序列(如.NET事件、基于APM(“IAsyncResult”)的计算、基于Task的计算、Windows 7传感器和位置API、SQL StreamInsight时态事件流、F#一级事件和异步工作流)编写复杂的事件处理查询。有关IObservable/IObserver接口的更多信息,请参阅探索Rx中的主要接口。有关使用Rx不同功能的教程,请参阅使用Rx

这允许将不同的世界观或编程模型应用于问题。例如,我们可以将所有鼠标移动事件视为一个IObservable,将屏幕的一个区域视为一组可枚举的点,并通过定义一个LINQ查询来描述两者的交集为黑点,您将得到一个响应鼠标事件并在屏幕的过滤区域绘制点的程序。有了Rx和LINQ,这种事情几乎就像魔术一样。

因此,结合 Rx 和上述事件序列,我以以下方式定义了我的 VirtualisingCollection

首先,有一个`Subject`的概念。这有点超出范围,所以如果你对Rx感兴趣,请查看相关的讨论和文档,但足以说明它代表了一个特定的“主题”,既可以被观察,也可以被程序通知。在代码中,我将`VirtualisingCollection`索引器视为一个`Subject`。它是一个通过索引访问“调用”的东西,也代表了一个事件流。

private Subject<int> indexAccesses=new Subject<int>(); 

public object this[int index]
        {
            get
            {
                indexAccesses.OnNext(index);
                int pageIndex = index / pageSize;
                int pageOffset = index % pageSize;
                if (_pages.ContainsKey(pageIndex))
                {
                    if (_pages[pageIndex] == null)
                        return default(T);
                    if (_pages[pageIndex].Items == null)
                        return default(T);
                    if (_pages[pageIndex].Items.Count <= pageOffset)
                        return default(T);
                    return _pages[pageIndex].Items[pageOffset];
                }
                else
                    return default(T);
            }
            set
            {
                throw new NotSupportedException();
            }
        }

上面显示存在一个名为indexAccessesSubject,并且每当访问索引时,它都被视为索引访问事件序列中的下一个项,通过引发OnNext。索引访问器现在是一种事件。

我们还有一个“页面提供程序”,它将异步数据和计数加载逻辑从集合中分离出来。在示例代码中,它看起来像这样:

  public interface IPageProvider
        {
            Task<IList<T>> RefreshPage(int pageNum,int pageSize);
            Task<int> GetCount();
        }

RefreshPage异步执行,代表上述序列中的步骤4和5。

使用上述代码,所需的行为体现在构造函数中的一个简短Rx Linq查询中:

Func<int, IObservable<IList<T>>> pageRefresh = page => Observable.FromAsync(() => pageProvider.RefreshPage(page,pageSize));
            var pageRefreshes = (from access in indexAccesses                    //whenever the UI tries to access the virtual collection at a certain index          
                                 where PageIsNew(access)                         //and the requested page is not yet loaded
                                 let pageNumber = (access / pageSize)            //calculate the page number
                                 from refreshedPage in pageRefresh(pageNumber)   //and for every corresponding page returned by the provider
                                 select new Page()
                                 {                             //specify a new page
                                     Items = refreshedPage,
                                     Touched = DateTime.Now,
                                     PageNumber = (pageNumber)
                                 });

            pageAdditionSubscription = pageRefreshes.ObserveOnDispatcher().Subscribe(
                page =>
                {
                    UpdatePage(page);  //for every new page specified by the above query update the page and notify the UI
                },
                ex =>
                {
                    Debug.WriteLine(ex.ToString());
                }
            );

 

`indexAccesses`被视为可查询序列,如果请求的页面尚不存在(其中`PageIsNew`...),则进行过滤,然后`SelectMany`与后端异步响应流(来自`pageRefresh`中的`refreshedPage`)上的查询结合,其中`pageRefresh`是从异步调用(`Observable.FromAsync()`)创建的`Observable`。

UI调度程序通过订阅查询(`pageRefreshes.ObserveOnDispatcher().Subscribe`.....)进行观察,该查询通过引发VectorChanged事件来更新页面。

结论

代码还有其他方面,例如清理页面和更改计数,但那是它的核心部分。

基本上,每当UI需要新数据时,索引器就会被访问,这被视为一个事件流,表示为Rx可观察对象,它在Linq查询中与异步后端请求响应流(作为Observable)的交集结合,以调用UpdatePage方法,该方法使用VectorChanged事件通知UI重新绘制。

我确实想知道随机存取数据虚拟化应该如何解决。上面的方法似乎与微软提出的概念模型相悖(所附代码也绝不是一个完整的解决方案!),所以我很想知道其他人会如何解决这个问题。

关注点

在上述过程中,我注意到了以下几个值得关注的点:

  • 数据虚拟化有两种类型:增量和随机访问。
  • ObservableCollectionIObservableVector毫无关系。
  • Rx可以用于使用LINQ描述异步事件逻辑。
  • IObservableVector 不适用于数据虚拟化。提供给ListView或GridView数据源的类型必须是IObservableVector
    © . All rights reserved.