Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

24 Jan 2021

UWP中的XAML控件ItemsRepeater

ItemsRepeater的阅读笔记。

ItemsRepeater和ItemsControl有点类似,都提供了用于处理元素集合的逻辑。和ItemsControl相比,ItemsRepeater支持UI布局的虚拟化。但是ItemsRepeater必须使用外部的数据源,无法像ItemsControl那样,可以有拥有自己的数据集合。

ItemsRepeater对需要处理的元素不做预设,需要用户自己指定一个item模板来展示元素。此外还需要指定一个布局来决定元素的大小和位置。通过data template选择器,可以对不同类型的元素设置不同的模板。ItemsRepeater可以看作定制化更强的ListView和GridView。

ItemsRepeater不是从Control派生出来的,所以没有Control Template。自己是不带任何内置的滚动控制的。如果需要滚动控制,需要在ItemsRepeater外面套一层ScrollViewer。如果1809的Win10之前,需要在外面再套一个ItemsRepeaterScrollHost:

<muxc:ItemsRepeaterScrollHost>
    <ScrollViewer>
        <muxc:ItemsRepeater ... />
    </ScrollViewer>
</muxc:ItemsRepeaterScrollHost>

主要是因为1809之前ScrollViewer没有实现ItemsRepeater所需的IScrollAnchorProvider接口。

ItemsRepeater的使用者需要指定ItemsSource、ItemTemplate来指挥元素的显示。ItemsSource可以绑定到一个Observable容器,而ItemTemplate可以是一个DataTemplate或者DataTemplateSelector。

下面是一个作用于String元素的DataTemplate的例子:

<DataTemplate x:DataType="x:String">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="47"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Image Source="Assets/placeholder.png" Width="32" Height="32"
               HorizontalAlignment="Left"/>
        <TextBlock Text="{x:Bind}" Foreground="Teal"
                   FontSize="15" Grid.Column="1"/>
    </Grid>
</DataTemplate>

由于指定DataTemplate是经常性行为,所以你可以直接DataTemplate作为ItemsRepeater的子元素:

<ItemsRepeater ItemsSource="{x:Bind Items}">
    <DataTemplate>
        <!-- ... -->
    </DataTemplate>
</ItemsRepeater>

不像ListView或者其他容器,ItemsRepeater不会在一个封装元素的DataTemplate之外再套一层容器,比如ListViewItem。这层额外的容器主要是涵盖默认的策略,比如内外间距,选项的视觉展示,指针悬停的视觉展示等等。 不过你可以再DataTemplate里面显示使用ListViewItem等容器。如果你的Collection是控件,比如Button,那么可以使用ContentPresenter来显示这些控件。

如果要显示的元素不是同一种类型,那么需要采用 DataTemplateSelector:

<ItemsRepeater ...>
    <ItemsRepeater.ItemTemplate>
        <local:VariableSizeTemplateSelector Large="{StaticResource LargeItemTemplate}" 
                                            Small="{StaticResource SmallItemTemplate}"/>
    </ItemsRepeater.ItemTemplate>
</ItemsRepeater>

通过自定义DataTemplateSelector的SelectTemplateCore(Object)来插入选择相关的逻辑。

也可以通过让ItemTemplate实现Windows.UI.Xaml.Controls.IElementFactory来按需创建元素。这是更高级的用法。

Configure the data source

那么,可以给ItemsRepeater的ItemsSource注射什么样类型的数据源呢?

  • IEnumerable/IIterable (默认必须有)
  • IReadonlyList/IVectorView (允许通过Index访问数据)
  • IList/IVector
  • INotifyCollectionChanged (允许观察数据源改动)
  • IObservableVector (同上,但是不支持Move操作,会导致移动的时候丢失焦点)
  • IKeyIndexMapping (支持在Reset的情况下,减少对元素的重新获取操作)

下面三个接口在ListView中用到,但是对ItemsRepeater没有影响:

  • ISupportIncrementalLoading
  • IIemsRangeInfo
  • ISelectionInfo

对于ISupportIncrementalLoading,一个变通的做法是通过对ScrollViewer的滚动进行观察,然后加载所需的元素。例子在文档中。

Change the layout of items

ItemsRepeater选哟一个Layout对象,来管理所有元素的UI呈现。可以使用StackLayout或者UniformGridLayout。默认情况下,ItemsRepeater使用的是竖排的StackLayout。

StackLayout把元素排布成一行或者一列。可以设置Spacing辖属来指定元素间隔;设置Orientation辖属来指定元素的走向。下面是一个例子:

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->
<muxc:ItemsRepeater ItemsSource="{x:Bind Items}" ItemTemplate="{StaticResource MyTemplate}">
    <muxc:ItemsRepeater.Layout>
        <muxc:StackLayout Orientation="Horizontal" Spacing="8"/>
    </muxc:ItemsRepeater.Layout>
</muxc:ItemsRepeater>

UniformGridLayout可以让元素朝某个方向排布,但是可以折行。折行的标准是最小宽度,或者最小高度。你可以指定最小的MinItemHeight或者MinItemWidth,如果不指定,那么第一个条目的测量而来的尺寸会被作为最小尺寸。当然也可以使用MinColumnSpacing和MinRowSpacing来指定间距。通过ItemsStretch和ItemsJustification可以指定如何使用多余的空白。

ItemsStretch可以设置为以下内容:

  • None,不适用剩余空白
  • Fill,在一个方向使用剩余空白
  • Uniform,在连个方向使用剩余空白,保持比例

ItemsStretch通过放大元素来填充空白,如果只是想移动位置的话,可以使用 ItemsJustification:

  • Start,起始位置对齐,空白在折行位置
  • Center,居中,空白在两边
  • End,折行位置对齐,空白在起始位置
  • SpaceAround,剩余空白平均添加到每个元素之前和之后
  • SpaceBetween,剩余空白评价添加到每个元素之间
  • SpaceEvenly,同SpaceBetween,但是开始和折行位置也能获得空白

ItemsStretch辖属影响的是measure过程,而ItemsJustification影响的是arrange过程。

下面是使用UniformGridLayout的一个例子:

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->
<muxc:ItemsRepeater ItemsSource="{x:Bind Items}"
                    ItemTemplate="{StaticResource MyTemplate}">
    <muxc:ItemsRepeater.Layout>
        <muxc:UniformGridLayout MinItemWidth="200"
                                MinColumnSpacing="28"
                                ItemsJustification="SpaceAround"/>
    </muxc:ItemsRepeater.Layout>
</muxc:ItemsRepeater>

Lifecycle events

ItemsRepeater是一个虚拟化的控件。不能只依赖于Loaded/Unloaded事件来判断元素是否从视觉树中移除。因为有可能元素只是被回收了。所以ItemsRepeater提供了其他事件:

  • ElementPrepared,在一个元素将被使用的时候触发。
  • ElementClearing,当元素被回收的时候触发。
  • ElementIndexChanged,在元素索引发生变化的时候触发。

Sorting, Filtering and Resetting the Data

在ItemsSource进行了Reset之后,ItemsRepeater需要从0开始构建UI。但是如果ItemsSource支持IKeyIndexMapping,ItemsRepeater就可以快速检测:

  • Reset之前和之后都使用到的UIElements
  • 需要被删除的元素
  • 需要被添加的元素

这样就可以避免Reset之后从0开始构建UI了。

Create a custom collection control

这个章节讲如何创建一个包含ItemsRepeater的自定义控件。采用的手段是在自定义控件的ControlTemplate中指定ItemsRepeater。

也可以派生ItemsControl来达到相似的功能,跟包含ItemsRepeater的方法相比,这是继承优先还是组合优先的问题。

Display grouped items

说的是如何嵌套ItemsRepeater,也就是在一个ItemsRepeater的ItemTemplate中指定其他ItemsRepeater。

Bringing an Element Into View

让一个元素进入显示范围。XAML在 1)收到键盘焦点 2)收到讲述者焦点的时候会自动将让目标元素进入显示范围。其他情况下,需要做一些额外的动作:

  • 实例化目标元素对应的UIElement
  • 执行布局操作,保证元素有一个有效的位置
  • 发送一个请求,把已实例化的元素带入显示范围

Enable Accessibility

默认情况下ItemsRepeater不提供辅助访问支持。关于辅助访问,可以参考Usability for Windows apps。如果你通过组合的方式,从ItemsRepeater创建了一个新的控件,那么可以参考Custom automation peers来提供此控件的辅助访问性。

默认情况下,ItemsRepeater提供键盘操作,基于2D Directional Navigation for Keyboarding。ItemsRepeater的XYFocusKeyboardNavigation模式默认是开启的。

ItemsRepeater会确保它的tab order是正确的。默认情况下TabFocusNavigation属性设置是Once。

如果要支持Announcing "Item X of Y" in Screen Readers,那么要确保PositionInSet和SizeOfSet在条目增删的时候保持最新。文档中有一个对应的例子。

(完)