首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

WPF 數據虛擬化完整實戰:VirtualizingCollection 與 AsyncVirtualizingCollection 深度解析

     編輯:關於.NET

背景:UI 虛擬化 ≠ 數據虛擬化

類型機制解決問題
UI 虛擬化僅為可視區域建立視覺容器 (Container),滾動時回收/重建視覺樹節點過多導致渲染/佈局效能下降
數據虛擬化**按需從數據源分頁加載實體物件**,未訪問頁釋放記憶體大數據集實體物件佔用記憶體、數據庫/網絡查詢延遲

WPF 內建 UI 虛擬化,**無內建數據虛擬化**。需自行實現 IList 介面的虛擬化集合。

核心介面:IItemsProvider<T>


public interface IItemsProvider {
    int FetchCount();                    // 總筆數 (對應 SQL COUNT(*))
    IList FetchRange(int startIndex, int count); // 分頁查詢 (OFFSET/LIMIT)
}

資料庫實現極簡:COUNT(*) + OFFSET @start ROWS FETCH NEXT @count ROWS ONLY。

基石實現:VirtualizingCollection<T> (同步版)

核心結構

  • 內部 **頁面字典** : Dictionary> _pages
  • **頁面存取時間戳** : Dictionary _pageTouchTimes
  • 配置參數:PageSize (預設 100)、PageTimeout (預設 30 秒)

關鍵屬性:Count — 延遲載入總數


private int _count = -1;
public virtual int Count {
    get {
        if (_count == -1) LoadCount(); // 首次訪問觸發
        return _count;
    }
    protected set { _count = value; }
}
protected virtual void LoadCount() => Count = FetchCount();
protected int FetchCount() => ItemsProvider.FetchCount();

核心索引器:this[int index] — 智慧分頁與預取


public T this[int index] {
    get {
        int pageIndex = index / PageSize;
        int pageOffset = index % PageSize;
        
        RequestPage(pageIndex); // 確保當前頁載入
        
        // ★ 預取策略:訪問頁面上半部 → 預取下一頁;下半部 → 預取上一頁
        if (pageOffset > PageSize/2 && pageIndex < Count/PageSize)
            RequestPage(pageIndex + 1);
        if (pageOffset < PageSize/2 && pageIndex > 0)
            RequestPage(pageIndex - 1);
        
        CleanUpPages(); // 清理過期頁
        
        // 防禦性檢查:異步載入時頁面可能尚未就緒
        if (_pages[pageIndex] == null) return default(T);
        
        return _pages[pageIndex][pageOffset];
    }
    set { throw new NotSupportedException(); }
}

頁面管理核心邏輯


protected virtual void RequestPage(int pageIndex) {
    if (!_pages.ContainsKey(pageIndex)) {
        _pages.Add(pageIndex, null);
        _pageTouchTimes.Add(pageIndex, DateTime.Now);
        LoadPage(pageIndex); // 同步載入
    } else {
        _pageTouchTimes[pageIndex] = DateTime.Now; // 更新存取時間
    }
}

protected virtual void LoadPage(int pageIndex) {
    PopulatePage(pageIndex, FetchPage(pageIndex));
}

protected virtual void PopulatePage(int pageIndex, IList page) {
    if (_pages.ContainsKey(pageIndex)) _pages[pageIndex] = page;
}

protected IList FetchPage(int pageIndex) {
    return ItemsProvider.FetchRange(pageIndex * PageSize, PageSize);
}

public void CleanUpPages() {
    var keys = new List(_pageTouchTimes.Keys);
    foreach (int key in keys) {
        // 第 0 頁特殊保護:WPF ItemsControl 高頻訪問首項
        if (key != 0 && (DateTime.Now - _pageTouchTimes[key]).TotalMilliseconds > PageTimeout) {
            _pages.Remove(key);
            _pageTouchTimes.Remove(key);
        }
    }
}

同步版缺陷

所有數據獲取在 UI 執行緒同步執行 → 大數據/高延遲源導致 UI 凍結、動畫卡頓。

進階實現:AsyncVirtualizingCollection<T> (異步版)

核心增強:實作 INotifyCollectionChanged + INotifyPropertyChanged


public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;

private void FireCollectionReset() {
    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void FirePropertyChanged(string name) {
    OnPropertyChanged(new PropertyChangedEventArgs(name));
}

異步載入總數


protected override void LoadCount() {
    Count = 0;
    IsLoading = true;
    ThreadPool.QueueUserWorkItem(LoadCountWork);
}

private void LoadCountWork(object args) {
    int count = FetchCount();
    SynchronizationContext.Send(LoadCountCompleted, count); // 回 UI 執行緒
}

private void LoadCountCompleted(object args) {
    Count = (int)args;
    IsLoading = false;
    FireCollectionReset(); // 通知 UI 重新查詢 Count
}

異步載入頁面


protected override void LoadPage(int index) {
    IsLoading = true;
    ThreadPool.QueueUserWorkItem(LoadPageWork, index);
}

private void LoadPageWork(object args) {
    int pageIndex = (int)args;
    IList page = FetchPage(pageIndex);
    SynchronizationContext.Send(LoadPageCompleted, new object[] { pageIndex, page });
}

private void LoadPageCompleted(object args) {
    int pageIndex = (int)((object[])args)[0];
    IList page = (IList)((object[])args)[1];
    
    PopulatePage(pageIndex, page);
    IsLoading = false;
    FireCollectionReset(); // 觸發 UI 重新綁定
}

IsLoading 屬性 — UI 回饋支援


public bool IsLoading {
    get => _isLoading;
    set {
        if (value != _isLoading) {
            _isLoading = value;
            FirePropertyChanged("IsLoading");
        }
    }
}

演示專案:百萬級資料流暢體驗

模擬資料源


public class DemoCustomerProvider : IItemsProvider {
    private readonly int _count, _fetchDelay;
    public DemoCustomerProvider(int count, int fetchDelay) { _count=count; _fetchDelay=fetchDelay; }
    public int FetchCount() { Thread.Sleep(_fetchDelay); return _count; }
    public IList FetchRange(int start, int count) {
        Thread.Sleep(_fetchDelay);
        return Enumerable.Range(start+1, count).Select(i => new Customer { Id=i, Name="Customer "+i }).ToList();
    }
}

XAML 關鍵配置



運行效果

演示截圖
百萬筆資料、1秒模擬延遲下,異步虛擬化集合保持 UI 流暢,記憶體佔用極低

原文來源

WPF Data Virtualization - Paul McClean (CodeProject)

總結與最佳實踐

  1. UI 虛擬化 + 數據虛擬化 = 完整解決方案,缺一不可
  2. AsyncVirtualizingCollection 為生產環境首選,**同步版僅適用極低延遲本地源**
  3. PageSize 建議 50-200,**過大增加延遲、過小增加查詢頻率**
  4. PageTimeout 平衡記憶體與重複查詢,建議 30-60 秒
  5. **第 0 頁永不清理** — WPF 高頻訪問首項導致閃爍
  6. 資料源端**必須支援高效分頁查詢** (索引、游標、Keyset Pagination)
  7. 異步實現關鍵:**SynchronizationContext.Send 回 UI 執行緒觸發通知**,確保綁定系統接收更新
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved