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); } }