ObservableCollection<T>
中有一个 Move
方法,而这个方法在其他类型的集合中是很少见的。由于 ObservableCollection<T>
主要用于绑定,涉及到 UI 更新,而 UI 更新普遍比普通的集合修改慢了不止一个数量级,所以可以大胆猜想,Move
的存在是为了提升 UI 刷新性能。
然而事实真是这样的吗?
试验
将 ObservableCollection<T>
用于 UI 绑定的目前只有 UWP 和 WPF,于是我写了两个 App 来验证这个问题。代码已上传 GitHub walterlv/ListViewBindingDemo for ItemsMove。
验证方式主要看两个点:
- UI 元素的 Hash 值有没有更改,以便了解 UWP 或 WPF 框架是否有为此移动的数据创建新的 UI。
- UI 元素的焦点有没有变化,以便了解 UWP 或 WPF 是否将此 UI 元素移出过视觉树。
结果如下图:
在 UWP 中,移动数据的元素焦点没有改变,Hash 值也没有改变。
在 UWP 中,未被移动数据的元素 Hash 值没有改变。
在 WPF 中,移动数据的元素焦点丢失,Hash 值已经改变。
在 WPF 中,未被移动数据的元素 Hash 值没有改变。
猜想
- UWP 真的对
ObservableCollection<T>
的Move
操作有优化,根本就没有将移动数据的元素移除视觉树。 - WPF 似乎并没有对
ObservableCollection<T>
的Move
操作进行优化,因为 Hash 值都变了,直接就是创建了个新的。几乎等同于将原来的 UI 元素移除之后再创建了一个新的。
调查
.Net Standard 统一了 ObservableCollection<T>
的 API,所以 UWP 和 WPF 这些基本的 API 是一样的。由于 .NET Framework 发布了源代码,.Net Core 直接开源,所以这两者的代码我们都能翻出来。
这是 [Net Framework 版的 ObservableCollection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Called by base class ObservableCollection<T> when an item is to be moved within the list;
/// raises a CollectionChanged event to any listeners.
/// </summary>
protected virtual void MoveItem(int oldIndex, int newIndex)
{
CheckReentrancy();
T removedItem = this[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, removedItem);
OnPropertyChanged(IndexerName);
OnCollectionChanged(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex);
}
这是 [.Net Core 版的 ObservableCollection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Called by base class ObservableCollection<T> when an item is to be moved within the list;
/// raises a CollectionChanged event to any listeners.
/// </summary>
protected virtual void MoveItem(int oldIndex, int newIndex)
{
CheckReentrancy();
T removedItem = this[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, removedItem);
OnIndexerPropertyChanged();
OnCollectionChanged(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex);
}
好吧,微软真省事儿,不止代码中的每个字母都相同,就连注释都一样……
MoveItem
所做的就是在旧的位置移除元素,并将其插入到新的位置。于是,优化的重心就在于引发 CollectionChanged
事件时传入的参数了,都是传入 NotifyCollectionChangedAction.Move
。
由于 UWP 没有开源,从源码级别我们只能分析 WPF 为此枚举所做的事情。在 WPF 中,ListView
为此所做的判断仅一处,就是其基类 ItemsControl
类的 AdjustItemInfos
方法。然而此方法内部对 Move
的实现几乎就是 Remove
和 Add
的叠加。
但是 UWP 中我们可以做更多的试验。比如我们直接移除掉原来的一项,然后延迟再添加一个新的:
1
2
3
4
var item = EditableCollection.FirstOrDefault(x => x.EditingText == "E");
EditableCollection.Remove(item);
await Task.Delay(2000);
EditableCollection.Insert(random.Next(EditableCollection.Count), item);
或者我们直接添加一个跟原来不同的项:
1
2
3
4
var item = EditableCollection.FirstOrDefault(x => x.EditingText == "E");
EditableCollection.Remove(item);
await Task.Delay(2000);
EditableCollection.Insert(random.Next(EditableCollection.Count), new EditableModel("X"));
这时运行发现,焦点确实移除了,但 HashCode 依然是原来的 HashCode。基本可以确定,UWP 的 ListBox
做了更多的优化,在根据 DataTemplate
生成控件时,一直在重用之前已经生成好的控件。
结论
UWP 比 WPF 对 ObservableCollection<T>
的集合操作进行了更好的性能优化,在添加、删除、移动时会重用之前创建好的控件。而在 WPF 中,则简单地创建和销毁这些控件——即便调用了 ObservableCollection<T>
专有的 Move
方法也没有做更多的优化。
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/binded-items-move-behavior-in-listview.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。