1)DataGrid的過濾過程:
用戶輸入過濾條件
調用DataGrid的CollectionViewSource的View.Refresh()功能
DataGrid控件內部調用CollectionView的RefreshOverride方法
CollectionView會調用CollectionViewSource的Filter回調函數來過濾符合自定義過濾條件的數據
CollectionView調用內部的OnCollectionChanged和OnCurrentChanged分別更新界面上的數據和當前選中的Item
2)通過分析發現(10W條數據, 實時過濾時UI非常卡,導致用戶輸入過濾字符丟失),調用CollectionViewSource的View.Refresh()的性能損耗主要集中於:
CollectionViewSource.Filter註冊的方法,以及OnCollectionChanged(每次更新都導致ItemContainerGenerator重新構造UI元素)
3)優化方向:
減少CollectionViewSource.Filter註冊的方法的耗時(在實時過濾中每個條件的更改都會調用Refresh從而調用Filter方法)
減少OnCollectionChanged調用的次數。
4)具體優化措施:
實例化3個Timer, 分別用於獲取過濾後的數組(調用Filter)、調用OnCollectionChanged、OnCurrentItemChanged。3個timer分別由前一個timer完成時啓動, 形成一個順序操作。每次調用Timer時,先停止後續Timer的執行, 這樣保證在合理的時間間隔裏只有一次Refresh完整完成。
5)實現:
下面代碼重載了ObservableCollection, 然後創建自定義的ListCollectionview.使用時只要用CustomCollection聲明列表數據,包裝爲CollectionViewSource, 綁定到DataGrid的ItemSource即可。
//聲明數組數據
public CustomCollection<StudyInfoModel> StudyList
{
get { return studyList; }
}
//包裝爲CollectionView
<CollectionViewSource Source="{Binding StudyList}" x:Key="StudyListView">
<CollectionViewSource.SortDescriptions>
<ComponentModel:SortDescription PropertyName="DateTime" Direction="Descending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
//綁定到DataGrid
<DataGrid ItemsSource="{Binding Mode=OneWay, Source={StaticResource StudyListView}}" />
public class CustomCollectionView<T> : ListCollectionView
{ private readonly DispatcherTimer _timerRefreshCalculate = new DispatcherTimer(); private readonly DispatcherTimer _timerRefreshUI = new DispatcherTimer(); private readonly DispatcherTimer _timerRefreshCurrentItem = new DispatcherTimer(); private bool _isRefreshingCalculate = false; private object _oldSelectedItem = null; public CustomCollectionView(IList list) : base(list) { _timerRefreshUI.Interval = new TimeSpan(0, 0, 0, 0, 300); _timerRefreshCurrentItem.Interval = new TimeSpan(0, 0, 0, 0, 500); _timerRefreshCalculate.Interval = new TimeSpan(0, 0, 0, 0, 200); } #region Override Method protected override void OnPropertyChanged(PropertyChangedEventArgs e) { if (_isRefreshingCalculate) { return; } base.OnPropertyChanged(e); } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args) { if (_isRefreshingCalculate) { return; } base.OnCollectionChanged(args); } protected override void OnCurrentChanged() { if (_isRefreshingCalculate) { return; } base.OnCurrentChanged(); } protected override void RefreshOverride() { CancelAllRefreshRequest(); StartRefresh(); } #endregion #region Public Method public void CancelAllRefreshRequest() { _timerRefreshCurrentItem.Stop(); _timerRefreshCurrentItem.Tick -= TimerCurrentItem; _timerRefreshUI.Stop(); _timerRefreshUI.Tick -= TimerUI; _timerRefreshCalculate.Stop(); _timerRefreshCalculate.Tick -= TimerCalculate; if (null != this.CurrentItem) { _oldSelectedItem = this.CurrentItem; } SetCurrent(null, -1); } #endregion #region Private Method private void StartRefresh() { _timerRefreshCurrentItem.Stop(); _timerRefreshCurrentItem.Tick -= TimerCurrentItem; _timerRefreshUI.Stop(); _timerRefreshUI.Tick -= TimerUI; _timerRefreshCalculate.Stop(); _timerRefreshCalculate.Tick -= TimerCalculate; //begin to refresh from calculate, so set flag by true. //and it shielded any collection action during the calculating time. //this logic will avoid items are not correct at UI during refresh. _isRefreshingCalculate = true; _timerRefreshCalculate.Tick += TimerCalculate; _timerRefreshCalculate.Start(); } private void RefreshCalculate(CancellationToken? token) { _timerRefreshCalculate.Tick -= TimerCalculate; if (null != token && null != token.Value) { token.Value.ThrowIfCancellationRequested(); } _isRefreshingCalculate = true; base.RefreshOverride(); _isRefreshingCalculate = false; if (null != token && null != token.Value) { token.Value.ThrowIfCancellationRequested(); } } private void RefreshUI() { try { //detach timer _timerRefreshUI.Tick -= TimerUI; base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); //set timer to refresh current item _timerRefreshCurrentItem.Tick -= TimerCurrentItem; _timerRefreshCurrentItem.Tick += TimerCurrentItem; _timerRefreshCurrentItem.Start(); } catch (OperationCanceledException) { return; } } private void RefreshCurrentItem() { _timerRefreshCurrentItem.Tick -= TimerCurrentItem; if (null == this.InternalList || this.InternalList.Count <= 0) { return; } if (null != _oldSelectedItem) { var index = this.InternalList.IndexOf(_oldSelectedItem); if (index != -1) { SetCurrent(_oldSelectedItem, index); } else { SetCurrent(this.InternalList[0], 0); } } else { SetCurrent(this.InternalList[0], 0); } //Set event to update UI base.OnCurrentChanged(); this.OnPropertyChanged("IsCurrentAfterLast"); this.OnPropertyChanged("IsCurrentBeforeFirst"); this.OnPropertyChanged("CurrentPosition"); this.OnPropertyChanged("CurrentItem"); } private void TimerCalculate(object sender, EventArgs e) { _timerRefreshCurrentItem.Stop(); _timerRefreshCurrentItem.Tick -= TimerCurrentItem; _timerRefreshUI.Stop(); _timerRefreshUI.Tick -= TimerUI; try { RefreshCalculate(null); } catch (OperationCanceledException) { } _timerRefreshUI.Interval = new TimeSpan(0, 0, 0, 0, 50 + Math.Min((int)(this.InternalCount / 80), 300)); _timerRefreshUI.Tick += TimerUI; _timerRefreshUI.Start(); } private void TimerUI(object sender, EventArgs e) { RefreshUI(); } private void TimerCurrentItem(object sender, EventArgs e) { RefreshCurrentItem(); } private void OnPropertyChanged(string propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion } public class CustomCollection<T> : ObservableCollection<T>, ICollectionViewFactory { public CustomCollection() { } public CustomCollection(List<T> list) : base(list) { } public CustomCollection(IEnumerable<T> collection) : base(collection) { } public ICollectionView CreateView() { return new CustomCollectionView<T>(this); } }