概述 在上面一篇 Windows Community Toolkit 4.0 - DataGrid - Overview 中,我們對 DataGrid 控制項做了一個概覽的介紹,今天開始我們會做進一步的詳細分享。 按照概述中分析代碼結構的順序,今天我們先對 CollectionViews 文件夾中的類 ...
概述
在上面一篇 Windows Community Toolkit 4.0 - DataGrid - Overview 中,我們對 DataGrid 控制項做了一個概覽的介紹,今天開始我們會做進一步的詳細分享。
按照概述中分析代碼結構的順序,今天我們先對 CollectionViews 文件夾中的類做詳細的分析。
下麵是 Windows Community Toolkit Sample App 的示例截圖和 code/doc 地址:
Windows Community Toolkit Doc - DataGrid
Windows Community Toolkit Source Code - DataGrid
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls.DataGrid;
開發過程
首先再來看一下 CollectionViews 文件夾的代碼結構:
4 個類中,CollectionView 是 EnumerableCollectionView 和 ListCollectionView 的基類,而 CollectionViewsError 是和 DataGrid 數據源中錯誤的處理類,接下來我們來分別看一下:
1. CollectionView
CollectionView 類是 DataGrid 數據相關處理的基類,這個類里的處理方法和屬性設置很多,同時還針對 FILTER,SORT 和 GROUP 特性做了處理,下麵先來看看類中定義的屬性:
- Count - 表示 DataGrid 控制項數據的數量,在 OnCollectionChanged 事件處理中,非 Replace 情況下觸發;
- IsEmpty - 表示 DataGrid 控制項中數據是否為空,同樣在 OnCollectionChanged 事件處理中,空和非空狀態切換時觸發;
- Culture - 表示 DataGrid 控制項的區域性信息,在 Culture 變化時,包括名稱,日曆系統,字元排序等會發生變化;
- CurrentPosition - 表示 DataGrid 控制項的當前位置,在子類的 RaiseCurrencyChanges 和 LoadSnapshot 事件中被使用;
- CurrentItem - 表示 DataGrid 控制項當前選中的元素,同樣在子類的 RaiseCurrencyChanges 和 LoadSnapshot 事件中被使用;
- IsCurrentBeforeFirst - 表示 DataGrid 控制項中當前選中是否在首個元素之前;
- IsCurrentAfterLast - 表示 DataGrid 控制項中當前選中是否在最後一個元素之後;
接下來看幾個重要的方法:
1). CollectionView()
CollectionView 類的構造方法,可以看到方法中創建了監聽器,對時間的 Action 調用和卸載做了定義,對於集合改變事件做了綁定,並對布爾類型的屬性做了初始設置;
public CollectionView(IEnumerable collection) { _sourceCollection = collection ?? throw new ArgumentNullException("collection"); // forward collection change events from underlying collection to our listeners. INotifyCollectionChanged incc = collection as INotifyCollectionChanged; if (incc != null) { _sourceWeakEventListener = new WeakEventListener<CollectionView, object, NotifyCollectionChangedEventArgs>(this) { // Call the actual collection changed event OnEventAction = (source, changed, arg) => OnCollectionChanged(source, arg), // The source doesn't exist anymore OnDetachAction = (listener) => incc.CollectionChanged -= _sourceWeakEventListener.OnEvent }; incc.CollectionChanged += _sourceWeakEventListener.OnEvent; } _currentItem = null; _currentPosition = -1; SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, _currentPosition < 0); SetFlag(CollectionViewFlags.IsCurrentAfterLast, _currentPosition < 0); SetFlag(CollectionViewFlags.CachedIsEmpty, _currentPosition < 0); }
2). OnCollectionChanged()
集合變化的處理,包括對變化動畫的判斷,當變化不是替換時,觸發 count 屬性變化;以及對於集合空的判斷,空和為空切換時,觸發 isEmpty 屬性變化,前面在屬性說明中我們提提到了;
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) { if (args == null) { throw new ArgumentNullException("args"); } unchecked { // invalidate enumerators because of a change ++_timestamp; } CollectionChanged?.Invoke(this, args); // Collection changes change the count unless an item is being // replaced or moved within the collection. if (args.Action != NotifyCollectionChangedAction.Replace) { OnPropertyChanged(CountPropertyName); } bool isEmpty = IsEmpty; if (isEmpty != CheckFlag(CollectionViewFlags.CachedIsEmpty)) { SetFlag(CollectionViewFlags.CachedIsEmpty, isEmpty); OnPropertyChanged(IsEmptyPropertyName); } }
3). SetCurrent()
根據當前選擇的元素,當前位置和元素數量設置當前選中;新元素不為空時,設置 IsCurrentBeforeFirst 和 IsCurrentAfterLast 屬性為 false;當集合為空時,設置兩個屬性為 true,設置新的選中位置為 -1;否則,根據 newPosition 的值來設置這兩個屬性;
protected void SetCurrent(object newItem, int newPosition, int count) { if (newItem != null) { // non-null item implies position is within range. // We ignore count - it's just a placeholder SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, false); SetFlag(CollectionViewFlags.IsCurrentAfterLast, false); } else if (count == 0) { // empty collection - by convention both flags are true and position is -1 SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); newPosition = -1; } else { // null item, possibly within range. SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, newPosition < 0); SetFlag(CollectionViewFlags.IsCurrentAfterLast, newPosition >= count); } _currentItem = newItem; _currentPosition = newPosition; }
2. EnumerableCollectionView
該類是 CollectionView 類的子類,支持枚舉類型的數據集合。下麵我們主要分享它基於 CollectionView 的特殊實現部分:
1). EnumerableCollectionView()
先看看構造方法,首先根據數據源設置當前元素和位置等,綁定集合改變,屬性改變和當前的改變和改變後事件;重點說一下 OnCurrentChanging 和 OnCurrentChanged 事件,分別可以在改變前做干預處理,改變後做對應處理;
internal EnumerableCollectionView(IEnumerable source) : base(source) { _snapshot = new ObservableCollection<object>(); LoadSnapshotCore(source); if (_snapshot.Count > 0) { SetCurrent(_snapshot[0], 0, 1); } else { SetCurrent(null, -1, 0); } // If the source doesn't raise collection change events, try to detect changes by polling the enumerator. _pollForChanges = !(source is INotifyCollectionChanged); _view = new ListCollectionView(_snapshot); INotifyCollectionChanged incc = _view as INotifyCollectionChanged; incc.CollectionChanged += new NotifyCollectionChangedEventHandler(EnumerableCollectionView_OnViewChanged); INotifyPropertyChanged ipc = _view as INotifyPropertyChanged; ipc.PropertyChanged += new PropertyChangedEventHandler(EnumerableCollectionView_OnPropertyChanged); _view.CurrentChanging += new CurrentChangingEventHandler(EnumerableCollectionView_OnCurrentChanging); _view.CurrentChanged += new EventHandler<object>(EnumerableCollectionView_OnCurrentChanged); }
2). ProcessCollectionChanged()
處理集合變化事件的方法,主要對改變做了 Add,Remove,Replace 和 Reset 四種情況的處理;分別看一下處理內容:
- Add - Add 操作後,對 snapshot 集合做對應變化,當新增索引 < 0 或小於當前開始索引時,加到集合開始位置,否則插入對應位置;
- Remove - Remove 操作後,在 snapshot 集合中刪除對應位置的元素;
- Replace - Replace 操作後,在 snapshot 集合中替換對應位置的元素;
- Reset - Reset 操作後,對應重置 snapshot 集合;
protected override void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) { // Apply the change to the snapshot switch (args.Action) { case NotifyCollectionChangedAction.Add: if (args.NewStartingIndex < 0 || _snapshot.Count <= args.NewStartingIndex) { // Append for (int i = 0; i < args.NewItems.Count; ++i) { _snapshot.Add(args.NewItems[i]); } } else { // Insert for (int i = args.NewItems.Count - 1; i >= 0; --i) { _snapshot.Insert(args.NewStartingIndex, args.NewItems[i]); } } break; case NotifyCollectionChangedAction.Remove: if (args.OldStartingIndex < 0) { throw CollectionViewsError.EnumerableCollectionView.RemovedItemNotFound(); } for (int i = args.OldItems.Count - 1, index = args.OldStartingIndex + i; i >= 0; --i, --index) { if (!object.Equals(args.OldItems[i], _snapshot[index])) { throw CollectionViewsError.CollectionView.ItemNotAtIndex("removed"); } _snapshot.RemoveAt(index); } break; case NotifyCollectionChangedAction.Replace: for (int i = args.NewItems.Count - 1, index = args.NewStartingIndex + i; i >= 0; --i, --index) { if (!object.Equals(args.OldItems[i], _snapshot[index])) { throw CollectionViewsError.CollectionView.ItemNotAtIndex("replaced"); } _snapshot[index] = args.NewItems[i]; } break; case NotifyCollectionChangedAction.Reset: LoadSnapshot(SourceCollection); break; } }
3). LoadSnapshot()
載入 snapshot 方法,根據重新載入的元素集合,判斷以下屬性是否需要響應變化:IsCurrentAfterLast,IsCurrentBeforeFirst,CurrentPosition 和 CurrentItem。
private void LoadSnapshot(IEnumerable source) { // Force currency off the collection (gives user a chance to save dirty information). OnCurrentChanging(); // Remember the values of the scalar properties, so that we can restore // them and raise events after reloading the data object oldCurrentItem = CurrentItem; int oldCurrentPosition = CurrentPosition; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; bool oldIsCurrentAfterLast = IsCurrentAfterLast; // Reload the data LoadSnapshotCore(source); // Tell listeners everything has changed OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); OnCurrentChanged(); if (IsCurrentAfterLast != oldIsCurrentAfterLast) { OnPropertyChanged(new PropertyChangedEventArgs(IsCurrentAfterLastPropertyName)); } if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) { OnPropertyChanged(new PropertyChangedEventArgs(IsCurrentBeforeFirstPropertyName)); } if (oldCurrentPosition != CurrentPosition) { OnPropertyChanged(new PropertyChangedEventArgs(CurrentPositionPropertyName)); } if (oldCurrentItem != CurrentItem) { OnPropertyChanged(new PropertyChangedEventArgs(CurrentItemPropertyName)); } }
3. ListCollectionView
該類是 CollectionView 類的子類,支持列表類型的數據集合。下麵我們也會主要分享它基於 CollectionView 的特殊實現部分:
1). ListCollectionView()
ListCollectionView 類的構造方法,當支持編輯行為時,需要刷新可增加,可刪除,可取消編輯的判斷;然後設置當前位置和元素;當支持分組時,註冊分組描述,分組改變和分組依據的變化處理事件;
public ListCollectionView(IList list) : base(list) { _internalList = list; #if FEATURE_IEDITABLECOLLECTIONVIEW RefreshCanAddNew(); RefreshCanRemove(); RefreshCanCancelEdit(); #endif if (InternalList.Count == 0) { // don't call virtual IsEmpty in ctor SetCurrent(null, -1, 0); } else { SetCurrent(InternalList[0], 0, 1); } #if FEATURE_ICOLLECTIONVIEW_GROUP _group = new CollectionViewGroupRoot(this); _group.GroupDescriptionChanged += new EventHandler(OnGroupDescriptionChanged); ((INotifyCollectionChanged)_group).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupChanged); ((INotifyCollectionChanged)_group.GroupDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupByChanged); #endif }
2). ProcessCollectionChangedWithAdjustedIndex()
處於集合變化和索引調整的方法,首先判斷當前動作的類型:Add,Remove 或 Replace,並針對每種不同類型的操作,進行分別的處理;再對 afterLastHasChanged,beforeFirstHasChanged,currentPositionHasChanged 和 currentItemHasChanged 屬性進行設置;
private void ProcessCollectionChangedWithAdjustedIndex(EffectiveNotifyCollectionChangedAction action, object oldItem, object newItem, int adjustedOldIndex, int adjustedNewIndex) { EffectiveNotifyCollectionChangedAction effectiveAction = action; if (adjustedOldIndex == adjustedNewIndex && adjustedOldIndex >= 0) { effectiveAction = EffectiveNotifyCollectionChangedAction.Replace; } else if (adjustedOldIndex == -1) { if (adjustedNewIndex < 0) { if (action == EffectiveNotifyCollectionChangedAction.Add) { return; } effectiveAction = EffectiveNotifyCollectionChangedAction.Remove; } } else if (adjustedOldIndex < -1) { ... } else { ... } int originalCurrentPosition = CurrentPosition; int oldCurrentPosition = CurrentPosition; object oldCurrentItem = CurrentItem; bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; NotifyCollectionChangedEventArgs args = null, args2 = null; switch (effectiveAction) { case EffectiveNotifyCollectionChangedAction.Add: // insert into private view #if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER #if FEATURE_IEDITABLECOLLECTIONVIEW // (unless it's a special item (i.e. new item)) if (UsesLocalArray && (!IsAddingNew || !object.Equals(_newItem, newItem))) #else if (UsesLocalArray) #endif { InternalList.Insert(adjustedNewIndex, newItem); } #endif if (!IsGrouping) { AdjustCurrencyForAdd(adjustedNewIndex); args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItem, adjustedNewIndex); } #if FEATURE_ICOLLECTIONVIEW_GROUP else { AddItemToGroups(newItem); } #endif break; case EffectiveNotifyCollectionChangedAction.Remove: ... case EffectiveNotifyCollectionChangedAction.Replace: ... case EffectiveNotifyCollectionChangedAction.Move: ... default: Debug.Assert(false, "Unexpected Effective Collection Change Action"); break; } bool afterLastHasChanged = IsCurrentAfterLast != oldIsCurrentAfterLast; bool beforeFirstHasChanged = IsCurrentBeforeFirst != oldIsCurrentBeforeFirst; bool currentPositionHasChanged = CurrentPosition != oldCurrentPosition; bool currentItemHasChanged = CurrentItem != oldCurrentItem; oldIsCurrentAfterLast = IsCurrentAfterLast; oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; oldCurrentPosition = CurrentPosition; oldCurrentItem = CurrentItem; // base class will raise an event to our listeners if (!IsGrouping) { Debug.Assert(!CurrentChangedMonitor.Busy, "Expected _currentChangedMonitor.Busy is false."); CurrentChangedMonitor.Enter(); using (CurrentChangedMonitor) { OnCollectionChanged(args); if (args2 != null) { OnCollectionChanged(args2); } } // Any scalar properties that changed don't need a further notification, // but do need a new snapshot ... } // currency has to change after firing the deletion event, // so event handlers have the right picture if (_currentElementWasRemoved) { int oldCurPos = originalCurrentPosition; #if FEATURE_ICOLLECTIONVIEW_GROUP if (_newGroupedItem != null) { oldCurPos = IndexOf(_newGroupedItem); } #endif MoveCurrencyOffDeletedElement(oldCurPos); // changes to the scalar properties need notification afterLastHasChanged = afterLastHasChanged || (IsCurrentAfterLast != oldIsCurrentAfterLast); beforeFirstHasChanged = beforeFirstHasChanged || (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); currentPositionHasChanged = currentPositionHasChanged || (CurrentPosition != oldCurrentPosition); currentItemHasChanged = currentItemHasChanged || (CurrentItem != oldCurrentItem); } RaiseCurrencyChanges(false, currentItemHasChanged, currentPositionHasChanged, beforeFirstHasChanged, afterLastHasChanged); }
4. CollectionViewsError
CollectionViewsError 類中主要定義了 DataGrid 控制項數據,就是 CollectionView 中的錯誤,我們來看一下都定義了哪些錯誤:
- EnumeratorVersionChanged - InvalidOperationException,“Collection was modified; enumeration operation cannot execute.”
- MemberNotAllowedDuringAddOrEdit - InvalidOperationException,"'{0}' is not allowed during an AddNew or EditItem transaction."
- NoAccessWhileChangesAreDeferred - InvalidOperationException,"This value cannot be accessed while changes are deferred."
- ItemNotAtIndex - InvalidOperationException,"The {0} item is not in the collection."
- RemovedItemNotFound - InvalidOperationException,"The removed item is not found in the source collection."
- CollectionChangedOutOfRange - InvalidOperationException,"The collection change is out of bounds of the original collection."
- AddedItemNotInCollection - InvalidOperationException,"The added item is not in the collection."
- CancelEditNotSupported - InvalidOperationException,"CancelEdit is not supported for the current edit item."
- MemberNotAllowedDuringTransaction - InvalidOperationException,"'{0}' is not allowed during a transaction started by '{1}'."
- MemberNotAllowedForView - InvalidOperationException,"'{0}' is not allowed for this view."
總結
這裡我們把 DataGrid 的 CollectionView 相關類介紹完成了,作為 DataGrid 相關分享的第一篇,後面我們會繼續分享 Utilities 和最重要的 DataGrid 的相關重點。
最後,再跟大家安利一下 WindowsCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。
衷心感謝 WindowsCommunityToolkit 的作者們傑出的工作,感謝每一位貢獻者,Thank you so much, ALL WindowsCommunityToolkit AUTHORS !!!