Windows Community Toolkit 4.0 - DataGrid - Part01
概述
在上面一篇 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 !!!