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 !!!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员的SOD蜜

.net访问PostgreSQL数据库发生“找不到函数名”的问题追踪

    PostgreSQL是一个使用广泛的免费开源的数据库,与MySQL比较,它更适合复杂的企业计算任务,而MySQL在互联网领域应用更为广泛,究其原因,可能...

3667
来自专栏TechBox

一份走心的iOS开发规范前言约定(一)命名规范(二)编码规范2.14 内存管理规范本文参考文章其他有价值的文章

6178
来自专栏林德熙的博客

WPF 开发

如果使用NamedPipeServerStream、Mutex做单实例,需要传入字符串,这时如果传入一个固定的字符串,会在多用户的时候无法使用。

2381
来自专栏我杨某人的青春满是悔恨

走进 RxSwift 之观察者模式

RxSwift 是 ReactiveX 系列的 Swift 版本,如果你之前用过 ReactiveCocoa(RAC) 的话,想必对 Functional Re...

2072
来自专栏冰霜之地

高效的序列化/反序列化数据方式 Protobuf

上篇文章中其实已经讲过了 encode 的过程,这篇文章以 golang 为例,从代码实现的层面讲讲序列化和反序列化的过程。

5955
来自专栏跟着阿笨一起玩NET

C# Eval在aspx页面中的用法及作用

2732
来自专栏守候书阁

编写自己的代码库(javascript常用实例的实现与封装--续)

这个系列的上一篇文章(编写自己的代码库(javascript常用实例的实现与封装))总结了34个常见的操作。但是在开发中,常见的实例又何止这么多个,经过这些日子...

1473
来自专栏iOS开发

iOS开发之 Method Swizzling 深入浅出

如果产品经理突然说:"在所有页面添加统计功能,也就是用户进入这个页面就统计一次"。我们会想到下面的一些方法:

4857
来自专栏大内老A

集成EntLib实现ASP.NET MVC的异常处理[续篇]

在《集成EntLib实现ASP.NET MVC的异常处理》我们实现采用EntLib的Exception Handling Application Block(E...

2029
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

7915

扫码关注云+社区

领取腾讯云代金券