Develop > Debugging, testing, and performance > Performance and XAML UI 文档笔记。
ListView and GridView UI optimization
使用UI虚拟化、元素缩减以及渐进式更新来改善LitView和GridView的性能。
UI虚拟化的意思就是不用一次为所有的数据创建UI元素。对于ListView来说,支持虚拟化的UI面板包括ItemsWrapGrid以及ItemsStackPanel。其他的,比如VariableSizedWrapGrid, WrapGrid, 或者StackPanel不支持虚拟化。
ListView的这些事件ChoosingGroupHeaderContainer, ChoosingItemContainer,以及ContainerContentChanging只会为ItemsWrapGride或者ItemsStackPanel触发。
为了确定哪些UI需要绘制,哪些不需要绘制,要首先确定UI的视界(ViewPort),即可以用来绘制子元素的预取。注意有一些容器,比如ScrollViewer还有Grid,不会限制子元素的大小,所以子元素的大小并不取决于视界的大小。把ItemsControl放置在这样的容器中,虚拟化不会默认起作用,除非为ItemsControl设置一个明确的大小。
优化性能的另一个思路是减少需要显示的元素,可以参考 Optimize your XAML markup获取一些额外的信息。
ListViewItem和GridViewItem默认都包含一个为UI显示优化过的ListViewItemPresenter元素,可以支持聚焦、选中,以及其他UI状态。如果你自定义了容器样式(ItemContainerStyle))或者自定义了容器模板,那么推荐在自定义中使用ListViewItemPresenter,可以通过ListViewItemPresenter的辖属来对其进行自定义。下面是一个例子:
...
<ListView>
...
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<ListViewItemPresenter SelectionCheckMarkVisualEnabled="False" SelectedBackground="Orange"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
</ListView>
<!-- ... -->
如果ListViewItemPresenter的25个辖属还不够你用,那么只好自定义ListViewItemExpanded和GridViewItemExpanded这两个控件模板(来自于\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<version>\Generic\generic.xaml
)。
分段加载可以在用户快速滚动的时候保持UI的响应性。除非在ShowsScrollingPlaceholders设置了false,默认情况下,会显示占位符。可以指定一个元素的x:Phase以及{x:Bind},如下所示:
<TextBlock Text="{x:Bind Title}"/>
<TextBlock Text="{x:Bind Subtitle}" x:Phase="1"/>
<TextBlock Text="{x:Bind Description}" x:Phase="2"/>
快速滚动的时候,会先绘制由ShowsScrollingPlaceholders控制的占位符,然后是Title,接着是SubTitle,最后是description。
渐进式的data template更新是在 ContainerContentChanging里面设置不需要元素的Opacity来将其隐藏。当元素被回收的时候会保留旧有值。通过事件参数的Phase属性来决定哪些元素需要显示或者隐藏。如果有额外的阶段,则需要注册一个回调。这些回调就是不同的Phase了。
如果不同的条目对应不同的显示界面(即数据模板)的情况下,可以使用ChoosingItemContainer事件应对。ChoosingItemContainer可以返回所需的LIstViewItem或者GridViewItem供列表使用。ChoosingGroupHeaderContainer提供相似的功能,不过针对的是群组标题。另一个办法是使用DataTemplateSelector,不过没有ChoosingItemContainer高效。
ListView and GridView data virtualization
虚拟化有两种,一种是惰性加载,另一种是随机加载。
要实现惰性加载,数据源必须实现下面的接口
- IList
INotifyCollectionChanged
(C#) 或IObservableVector<T>
(C++)- ISupportIncrementalLoading
一个数据源是一个驻留内存的列表。ItemsControl会通过标准的IList接口来像数据源要数据,以及数据的个数。这里的数据个数表示的是驻留列表的大小而不是数据集的大小。当控件快划到头的时候,会调用ISupportIncrementalLoading.HasMoreItems来问是否有更多数据。如果有的话可以通过ISupportIncrementalLoading.LoadMoreItemsAsync来返回更多数据。你可以加载比所要数据更多的数据。然后通过INotifyCollectionChanged or IObservableVector<T>
来通知数据加载的完成。
一个例子是Windows8的XAML data binding sample
要实现随机加载,则要实现以下接口
- IList
INotifyCollectionChanged
(C#/VB) orIObservableVector<T>
(C++/CX)- (Optionally) IItemsRangeInfo
- (Optionally) ISelectionInfo
IItemsRangeInfo给出以下信息:
- 在视界中显示的信息
- ItemsControl已拥有的数据条目,还有当前条目,以及第一个条目
你的数据源可以根据IItemsRangeInfo给出的信息加载或者卸载某些条目。单个IItemsRangeInfo只能给单个ItemsControl使用。
一些基本的策略
- 当数据源被要求返回条目的时候
- 如果条目在内存中,则直接返回条目
- 如果没有,则返回null或者占位符
- 去数据集获取条目,获取完通过通知告诉ItemsControl
- (可选),但视界变化时,决定哪些条目需要留在内存
其他注意事项:
- 使用异步操作来获取数据,不要阻断UI
- 获取数据的时候不要计算太精细,prefer chunky to chatty。
- 决定多少请求可以同时进行,一次一个可能会降低响应度
- 可以取消数据获取吗?
- 考虑每次获取数据的代价。
- 数据会从远端改变吗?比如说在33的位置插入了一个元素。
- 是否支持预先获取,比如从滚动的方向和速率上做一个预判
- 什么时候从不要的条目从内存中删除呢?