| 類型 | 機制 | 解決問題 |
|---|---|---|
| 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();
}
}
WPF Data Virtualization - Paul McClean (CodeProject)
AsyncVirtualizingCollection 為生產環境首選,**同步版僅適用極低延遲本地源**PageSize 建議 50-200,**過大增加延遲、過小增加查詢頻率**PageTimeout 平衡記憶體與重複查詢,建議 30-60 秒SynchronizationContext.Send 回 UI 執行緒觸發通知**,確保綁定系統接收更新