WPF:数据虚拟化,DataGrid虚拟化

[复制链接]
发表于 2025-6-26 09:25:02 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
WPF:数据虚拟化

下载源代码</p>介绍
从UI的观点上看,WPF为有效的处理大型集合提供了一些智慧的UI虚拟化特性。但那没有为数据归档虚拟化提供通用的方法。当有人在互联网论坛上发贴讨论数据虚拟化时,没有一个人提供一个解决方案(至少我没有看到)。本文提出这个的一个解决方案。
 背景
UI虚拟化
当一个WPF的ItemControl被绑定到一个大型集合的数据源时,假如可以UI虚拟化,该控件将只为那些在可以看到的项创见可视化的容器(加上面和下面的少许)。这是一个完整集合中有代表性的一小部门。用户移动滚动条时,将为那些滚动到可视区域的项创建新的可视化容器,那些不再可见的项的容器将被销毁。当容器设置为循环使用时,它将再使用可视化容器代替不断的创建和销毁可视化容器,避免对象的实例化和垃圾回收器的过分工作。
数据虚拟化
数据虚拟化是指绑定到ItemControl的真实的数据对象的归档虚拟化的时间段。数据虚拟化不是由WPF提供的。作为对比,基本数据对象的小集合对内存的消耗不是很多;但是,大集合的内存消耗是非常严重的。另外,真实的检索数据(例如,从数据库)和实例化数据对象是很耗时的,尤其当是网络数据调用时。因此,我们希望使用数据虚拟化机制来限制检索的数据的数量和在内存中天生数据对象的数量。
解决方案
总览
这个解决方案是只在ItemControl绑定到IList接口的实现时起作用,而不是IEumerable的实现,它并不枚举整个列表,而只是读取必要表现的项。它使用Count属性判断集合的大小,推测并设置滚动的范围。然后使用列表索引重新确定要在屏幕上表现的项。因此,创建一个可以报告具有大量项的,并且可以只检索必要的项的IList。
IItemsProvider

 为了利用这个解决方案,下面的数据源必须能提供集合中项的数量,并且能够提供完整集合的小块(或页)。这必要在IItemsProvider接口封装。
  1. /// <summary><br>/// Represents a provider of collection details.<br>/// </summary><br>/// <typeparam name="T">The type of items in the collection.</typeparam><br>public interface IItemsProvider<T><br>{<br>    /// <summary><br>    /// Fetches the total number of items available.<br>    /// </summary><br>    /// <returns></returns><br>    int FetchCount();<br><br>    /// <summary><br>    /// Fetches a range of items.<br>    /// </summary><br>    /// <param name="startIndex">The start index.</param><br>    /// <param name="count">The number of items to fetch.</param><br>    /// <returns></returns><br>    IList<T> FetchRange(int startIndex, int count);<br>}
复制代码
 
 假如下面的查询是一个数据库查询,它是一个利用大多数据库供应商都提供的COUNT()聚集函数和OFFSET与LIMIT表达式的一个IItemProvider接口的一个简朴实现。
VirtualizingCollection

 这是一个实行数据虚拟化的IList的实现。VirtualizingCollection(T)把整个集合分装到一定命量的页中。根据必要把页加载到内存中,在不必要时从开释。
下面讨论我们有兴趣的部门。详细信息请参考附件中的源代码项目。
IList实现的第一个方面是实现Count属性。它通常被ItemsControl用来确定集合的大小,并出现适当的滚动条。
  1. private int _count = -1;<br><br>public virtual int Count<br>{<br>    get<br>    {<br>        if (_count == -1)<br>        {<br>            LoadCount();<br>        }<br>        return _count;<br>    }<br>    protected set<br>    {<br>        _count = value;<br>    }<br>}<br><br>protected virtual void LoadCount()<br>{<br>    Count = FetchCount();<br>}<br><br>protected int FetchCount()<br>{<br>    return ItemsProvider.FetchCount();<br>}<br><br>
复制代码
 
Count属性使用延迟和懒惰加载(lazy loading)模式。它使用特别值-1作为未加载的标识。当第一次读取它时,它从ItemsProvider加载实在际的数量。
 IList接口的实现的另一个重要方面是索引的实现。
  1. public T this[int index]<br>{<br>    get<br>    {<br>        // determine which page and offset within page<br>        int pageIndex = index / PageSize;<br>        int pageOffset = index % PageSize;<br><br>        // request primary page<br>        RequestPage(pageIndex);<br><br>        // if accessing upper 50% then request next page<br>        if ( pageOffset > PageSize/2 && pageIndex < Count / PageSize)<br>            RequestPage(pageIndex + 1);<br><br>        // if accessing lower 50% then request prev page<br>        if (pageOffset < PageSize/2 && pageIndex > 0)<br>            RequestPage(pageIndex - 1);<br><br>        // remove stale pages<br>        CleanUpPages();<br><br>        // defensive check in case of async load<br>        if (_pages[pageIndex] == null)<br>            return default(T);<br><br>        // return requested item<br>        return _pages[pageIndex][pageOffset];<br>    }<br>    set { throw new NotSupportedException(); }<br>}
复制代码
 这个索引是这个解决方案的一个智慧的操作。首先,它必须确定请求的项在哪个页(pageIndex)中,在页中的位置(pageOffset),然后调用RequestPage()方法请求该页。
附加的步骤是然后根据pageOffset加载后一页或前一页。这基于一个假设,假如用户正在浏览第0页,那么他们有很高的机率接下来要滚动浏览第1页。提前把数据取来,就可以无延迟的表现。
然后调用CleanUpPages()清除(或卸载)所有不再使用的页。
末了,放置页不可用的一个防御性的查抄, 当RequestPage没有同步操作时是必要的,例如在子类AsyncVirtualizingCollection中。
  1. // ...<br>      <br>private readonly Dictionary<int, IList<T>> _pages = <br>        new Dictionary<int, IList<T>>();<br>private readonly Dictionary<int, DateTime> _pageTouchTimes = <br>        new Dictionary<int, DateTime>();<br><br>protected virtual void RequestPage(int pageIndex)<br>{<br>    if (!_pages.ContainsKey(pageIndex))<br>    {<br>        _pages.Add(pageIndex, null);<br>        _pageTouchTimes.Add(pageIndex, DateTime.Now);<br>        LoadPage(pageIndex);<br>    }<br>    else<br>    {<br>        _pageTouchTimes[pageIndex] = DateTime.Now;<br>    }<br>}<br><br>protected virtual void PopulatePage(int pageIndex, IList<T> page)<br>{<br>    if (_pages.ContainsKey(pageIndex))<br>        _pages[pageIndex] = page;<br>}<br><br>public void CleanUpPages()<br>{<br>    List<int> keys = new List<int>(_pageTouchTimes.Keys);<br>    foreach (int key in keys)<br>    {<br>        // page 0 is a special case, since the WPF ItemsControl<br>        // accesses the first item frequently<br>        if ( key != 0 && (DateTime.Now - <br>             _pageTouchTimes[key]).TotalMilliseconds > PageTimeout )<br>        {<br>            _pages.Remove(key);<br>            _pageTouchTimes.Remove(key);<br>        }<br>    }<br>}
复制代码
 页存储在以页索引为键的字典(Dictionary)中。一个附加的字典(Dictionary)记载着每个页的末了存取时间,它用于在CleanUpPages()方法中移除较长时间没有存取的页。
  1. protected virtual void LoadPage(int pageIndex)<br>{<br>    PopulatePage(pageIndex, FetchPage(pageIndex));<br>}<br><br>protected IList<T> FetchPage(int pageIndex)<br>{<br>    return ItemsProvider.FetchRange(pageIndex*PageSize, PageSize);<br>}
复制代码
 为完成该解决方案,FetchPage()实行从ItemProvider中抓取数据,LoadPage()方法完成调用PopulatePage方法获取页并把该页存储到字典(Dictionary)中的工作。
看起来好象有一些太多的不全逻辑的方法(a few too many inconsequential methods),但这样设计是有原因的:每一个方法做且只做一件事,有助于进步代码的可读性,并使在子类中进行功能扩展和维护变得容易,下面可以看到。
类VirtualizingCollection实现了数据虚拟化的基本目的。不幸的是,在使用中,它有一个严重不敷:数据抓取方法是全部同步实行的。这就是说它们要在UI线程中实行,造成一个缓慢的程序(原文是:This means they will be executed by the UI thread, resulting, potentially, in a sluggish application.)
AsyncVirtualizingCollection

类AsyncVirtualizingCollection继承自VirtualizingCollection,重载了Load方法,以实现数据的异步加载。
WPF中异步数据源的关键是在数据抓取完成后必须通知UI的数据绑定。在规则的对象中,是通过实现INotifyPropertyChanged接口实现的。对一个集合的实现,必要紧密的关系,INotifyCollectionChanged。(原文:For a collection implementation, however, it is necessary to use its close relative, INotifyCollectionChanged.)。那是ObservableCollection要使用的接口。
  1. public event NotifyCollectionChangedEventHandler CollectionChanged;<br>protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)<br>{<br>    NotifyCollectionChangedEventHandler h = CollectionChanged;<br>    if (h != null)<br>        h(this, e);<br>}<br><br>private void FireCollectionReset()<br>{<br>    NotifyCollectionChangedEventArgs e = <br>      new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);<br>    OnCollectionChanged(e);<br>}<br><br>public event PropertyChangedEventHandler PropertyChanged;<br>protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)<br>{<br>    PropertyChangedEventHandler h = PropertyChanged;<br>    if (h != null)<br>        h(this, e);<br>}<br><br>private void FirePropertyChanged(string propertyName)<br>{<br>    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);<br>    OnPropertyChanged(e);<br>}<br><br>
复制代码
 AsyncVirtualizingColliection实现了INotifyCollectionChanged接口和INotifyPropertyChanged接口。提供数据绑定弹性最大化。这个实现没有任何要注意的。
 
  1. protected override void LoadCount()<br>{<br>    Count = 0;<br>    IsLoading = true;<br>    ThreadPool.QueueUserWorkItem(LoadCountWork);<br>}<br><br>private void LoadCountWork(object args)<br>{<br>    int count = FetchCount();<br>    SynchronizationContext.Send(LoadCountCompleted, count);<br>}<br><br>private void LoadCountCompleted(object args)<br>{<br>    Count = (int)args;<br>    IsLoading = false;<br>    FireCollectionReset();<br>}
复制代码
 在重载的LoadCount()方法中,抓取是由ThreadPool(线程池)异步调用的。一旦完成,就会重置Count,UI的更新是由INotifyCollectionChanged接口调用FireCollectionReset方法实现的。注意LoadCountCompleted方法会在UI线程通过SynchronizationContext再一次被调用。假定集合的实例在UI线程中被创建,SynchronationContext属性就会被设置。
  1. protected override void LoadPage(int index)<br>{<br>    IsLoading = true;<br>    ThreadPool.QueueUserWorkItem(LoadPageWork, index);<br>}<br><br>private void LoadPageWork(object args)<br>{<br>    int pageIndex = (int)args;<br>    IList<T> page = FetchPage(pageIndex);<br>    SynchronizationContext.Send(LoadPageCompleted, new object[]{ pageIndex, page });<br>}<br><br>private void LoadPageCompleted(object args)<br>{<br>    int pageIndex = (int)((object[]) args)[0];<br>    IList<T> page = (IList<T>)((object[])args)[1];<br><br>    PopulatePage(pageIndex, page);<br>    IsLoading = false;<br>    FireCollectionReset();<br>}
复制代码
 页数据的加载遵循相同的惯例,再一次调用FireCollectionReset方法更新用户UI。
也要注意IsLoading属性是一个简朴的标识,可以用来告知UI集合正在加载。当IsLoading改变后,由INotifyPropertyChanged机制调用FirePropertyChanged方法更新UI。
 
  1. public bool IsLoading<br>{<br>    get<br>    {<br>        return _isLoading;<br>    }<br>    set<br>    {<br>        if ( value != _isLoading )<br>        {<br>            _isLoading = value;<br>            FirePropertyChanged("IsLoading");<br>        }<br>    }<br>}
复制代码
 
 演示项目
 为了演示这个解决方案,我创建了一个简朴的示例项目(包括附加的源代码项目)。
首先,创建一个IItemsProvider的一个实现,它通过使用线程休眠来模仿网络或磁盘举动的延迟提供虚拟数据。
[code]public class DemoCustomerProvider : IItemsProvider
{
    private readonly int _count;
    private readonly int _fetchDelay;

    public DemoCustomerProvider(int count, int fetchDelay)
    {
        _count = count;
        _fetchDelay = fetchDelay;
    }

    public int FetchCount()
    {
        Thread.Sleep(_fetchDelay);
        return _count;
    }

    public IList FetchRange(int startIndex, int count)
    {
        Thread.Sleep(_fetchDelay);

        List list = new List();
        for( int i=startIndex; i
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表