次の方法で共有


ItemsRepeater

柔軟なレイアウト システム、カスタム ビュー、および仮想化を使ってカスタム コレクション エクスペリエンスを作成するには、ItemsRepeater を使用します。

ListView とは異なり、ItemsRepeater では包括的なエンド ユーザー エクスペリエンスが提供されません。既定の UI はなく、フォーカス、選択、ユーザーの操作に関するポリシーは提供されません。 代わりに、独自のコレクション ベースのエクスペリエンスとカスタム コントロールを作成するために使用できる構成要素です。 組み込みのポリシーはありませんが、必要なエクスペリエンスを構築するためのポリシーをアタッチすることができます。 たとえば、使用するレイアウト、キーボード操作ポリシー、選択ポリシーなどを定義できます。

ItemsRepeater は概念的に、ListView などの完全なコントロールとしてではなく、データドリブン パネルと考えることができます。 表示されるデータ項目のコレクション、各データ項目の UI 要素を生成する項目テンプレート、要素のサイズと位置を設定する方法を決定するレイアウトを指定します。 その後、ItemsRepeater でデータ ソースに基づいた子要素が生成され、項目テンプレートとレイアウトで指定されたとおりに表示されます。 表示される項目が同種である必要はありません。これは、データ テンプレート セレクターで指定する条件に基づいてデータ項目を表すために、ItemsRepeater でコンテンツを読み込むことができるためです。

これは適切なコントロールですか?

データ コレクション用のカスタム表示を作成するには、ItemsRepeater を使用します。 これは基本的な一連の項目を表示するために使用できますが、多くの場合、カスタム コントロールのテンプレートで表示要素として使用します。

最小限にカスタマイズされたグリッドまたはリストにデータを表示するためにすぐに使えるコントロールが必要な場合は、ListView または GridView の使用を検討してください。

ItemsRepeater には組み込みの項目コレクションはありません。 別のデータ ソースにバインドするのではなく、項目コレクションを直接提供しなければならない場合は、より高いポリシー エクスペリエンスが必要になる可能性があり、ListView または GridView を使用する必要があります。

ItemsControl と ItemsRepeater の両方でカスタマイズ可能なコレクション エクスペリエンスを有効にすることはできますが、ItemsRepeater では UI レイアウトの仮想化がサポートされるのに対して ItemsControl ではサポートされません。 単にデータのいくつかの項目を表示するためであるか、カスタム コレクション コントロールをビルドするためであるかに関係なく、ItemsControl ではなく ItemsRepeater を使用することをお勧めします。

ItemsRepeater でのスクロール

ItemsRepeaterControl から派生しないため、コントロール テンプレートはありません。 したがって、ListView やその他のコレクション コントロールのようにスクロールの操作は組み込まれていません。

ItemsRepeater を使用する場合は、ScrollViewer コントロールでラップし、スクロール機能を提供する必要があります。

ItemsRepeater を作成する

WinUI 3 ギャラリー アイコン WinUI 3 ギャラリー アプリには、WinUI コントロールと機能の対話型の例が含まれています。 Microsoft Store からアプリを取得するか、GitHub でソース コードを参照します。

ItemsRepeater を使用するには、ItemsSource プロパティを設定し、表示するデータを指定する必要があります。 その後、ItemTemplate プロパティを設定し、項目の表示方法を指示します。

アイテムソース (ItemsSource)

ビューを設定するには、ItemsSource プロパティをデータ項目のコレクションに設定します。 ここでは、ItemsSource がコードでコレクションのインスタンスに直接設定されています。

ObservableCollection<string> Items = new ObservableCollection<string>();

ItemsRepeater itemsRepeater1 = new ItemsRepeater();
itemsRepeater1.ItemsSource = Items;

ItemsSource プロパティを、XAML でコレクションにバインドすることもできます。 データ バインディングについて詳しくは、「データ バインディングの概要」をご覧ください。

<ItemsRepeater ItemsSource="{x:Bind Items}"/>

アイテムテンプレート

データ項目を視覚化する方法を指定するには、定義した DataTemplate または DataTemplateSelectorItemTemplate プロパティを設定します。 データ テンプレートで、データがどのように視覚化されるかが定義されます。 既定では、項目は、データ オブジェクトの文字列表現を使用する TextBlock でビューに表示されます。

しかし、通常は、個々の項目を表示するために使用する 1 つまたは複数のコントロールのレイアウトと外観を定義するテンプレートを使用して、より豊富な表現でデータを表示する必要があります。 テンプレートで使用するコントロールを、データ オブジェクトのプロパティにバインドすることも、その静的コンテンツをインラインで定義することもできます。

データテンプレート

この例では、データ オブジェクトはシンプルな文字列です。 DataTemplate にはテキストの左側にイメージが含まれており、青緑色で文字列を表示するために TextBlock がスタイル設定されています。

DataTemplatex:Bind markup extension を使う場合、DataTemplate に DataType (x:DataType) を指定する必要があります。

<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 で使用される要素の数がパフォーマンスに大きく影響する可能性があります。 DataTemplate を使用してリスト項目の外観を定義する詳しい方法のその例については、「項目のコンテナーとテンプレート」を参照してください。

ヒント

便宜上、静的リソースとして参照するのではなく、テンプレートをインラインで宣言する必要がある場合は、ItemsRepeater の直接の子として、DataTemplate または DataTemplateSelector を指定することができます。 これは、ItemTemplate プロパティの値として割り当てられます。 たとえば、これが有効です。

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

ヒント

ListView やその他のコレクション コントロールとは異なり、ItemsRepeater では、余白、パディング、選択ビジュアル、ビジュアル状態のポインターなど、既定のポリシーを含む追加の項目コンテナーがある DataTemplate の要素は折り返されません。 代わりに、ItemsRepeater では、DataTemplate で定義された内容のみが表示されます。 項目をリスト ビュー項目と同じ外観にする必要がある場合は、データ テンプレートで、ListViewItem などの、コンテナーを明示的に含めることができます。 ItemsRepeater では ListViewItem ビジュアルが表示されますが、選択や複数選択チェックボックスの表示など、その他の機能は自動的に利用されません。

同様に、データ コレクションが、Button (List<Button>) などの、実際のコントロールのコレクションである場合は、DataTemplate ContentPresenter を配置してコントロールを表示することができます。

データテンプレートセレクター

ビューで表示する項目が、同じ種類である必要はありません。 DataTemplateSelector を使用して ItemTemplate プロパティを指定し、指定した条件に基づいて異なる DataTemplate を選択することができます。

この例では、Large と Small の項目を表すために、2 つの異なる DataTemplate のうちどちらかを決める DataTemplateSelector が定義されていると仮定します。

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

ItemsRepeater で使用する DataTemplateSelector を定義する場合、SelectTemplateCore(Object) メソッドのオーバーライドを実装するだけで済みます。 詳しい説明と例については、DataTemplateSelector に関するページを参照してください。

より高度なシナリオで要素を作成する方法を管理する DataTemplate の代わりに、独自の IElementFactory を実装して、ItemTemplate として使用できます。 これには、要求されたときにコンテンツを生成する役割があります。

データ ソースの構成

項目のコンテンツを生成するために使用するコレクションを指定するには、ItemsSource プロパティを使用します。 IEnumerable を実装する任意の種類に ItemsSource を設定することができます。 データ ソースによって実装される追加のコレクション インターフェイスで、データを操作するために ItemsRepeater で使用できる機能が決定されます。

このリストには使用可能なインターフェイスと、それぞれの使用を検討するタイミングが示されています。

  • IEnumerable(.NET) / IIterable

    • 小規模の静的なデータ セットで使用できます。

      データ ソースでは、少なくとも IEnumerable / IIterable インターフェイスを実装する必要があります。 これがすべてサポートされている場合、コントロールはすべてを 1 回反復処理して、インデックス値を介して項目をaccessするために使用できるコピーを作成します。

  • IReadonlyList(.NET) / IVectorView

    • 静的な読み取り専用のデータ セットで使用できます。

      コントロールがインデックスによって項目にアクセスし、冗長な内部コピーを回避することを可能にします。

  • IList(.NET) / IVector

    • 静的なデータ セットで使用できます。

      コントロールがインデックスによって項目にアクセスし、冗長な内部コピーを回避することを可能にします。

      警告: INotifyCollectionChanged を実装せずに list/vector を変更した場合、UI では反映されません。

  • INotifyCollectionChanged(.NET)

    • 変更通知をサポートすることをお勧めします。

      コントロールで、データ ソース内の変更を監視して対応できるようにし、これらの変更を UI に反映できるようにします。

  • IObservableVector の

    • 変更通知がサポートされます

      INotifyCollectionChanged インターフェイスと同様、これにより、コントロールでデータ ソース内の変更を監視し、対応できるようになります。

      Warning: Windows。Foundation.IObservableVector<T> は'Move' アクションをサポートしていません。 そのため、アイテムのユーザーインターフェースがその視覚的状態を失う可能性があります。 たとえば、現在選択中の項目やフォーカスが当たっている項目が、'削除' の後に '追加' される操作によって移動されると、フォーカスが失われ、選択されなくなります。

      Platform.Collections.Vector<T> では IObservableVector<T> が使用され、これと同じ制限があります。 '移動' アクションのサポートが必要な場合は、INotifyCollectionChanged インターフェイスを使用します。 .NET ObservableCollection<T> クラスは、INotifyCollectionChanged を使用します。

  • IKeyIndexMapping の

    • 一意識別子を各項目に関連付けることができる場合。 コレクション変更アクションとして 'リセット' を使用する場合にお勧めします。

      コントロールで、INotifyCollectionChanged または IObservableVector イベントの一部としてハード 'リセット' アクションを受信した後、既存の UI を非常に効率的に回復できるようにします。 リセットを受信した後、コントロールでは、既に作成されている要素に現在のデータを関連付けるために、指定された一意の ID が使用されます。 キーとインデックスのマッピングがなければ、コントロールはデータのUIを最初からやり直さなければならないと判断するでしょう。

IKeyIndexMapping 以外の、上記のインターフェイスでは、ItemsRepeater で、ListView や GridView の場合と同じ動作が提供されます。

ItemsSource の次のインターフェイスでは、ListView および GridView コントロールで特別な機能が有効になりますが、現在、ItemsRepeater で影響はありません。

ユーザーが上下にスクロールしたときに、データを段階的に読み込む別の方法は、ScrollViewer のビューポートの位置を監視し、ビューポートがエクステントに近づいたときにさらにデータを読み込むことです。

<ScrollViewer ViewChanged="ScrollViewer_ViewChanged">
    <ItemsRepeater ItemsSource="{x:Bind MyItemsSource}" .../>
</ScrollViewer>
private async void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    if (!e.IsIntermediate)
    {
        var scroller = (ScrollViewer)sender;
        var distanceToEnd = scroller.ExtentHeight - (scroller.VerticalOffset + scroller.ViewportHeight);

        // trigger if within 2 viewports of the end
        if (distanceToEnd <= 2.0 * scroller.ViewportHeight
                && MyItemsSource.HasMore && !itemsSource.Busy)
        {
            // show an indeterminate progress UI
            myLoadingIndicator.Visibility = Visibility.Visible;

            await MyItemsSource.LoadMoreItemsAsync(/*DataFetchSize*/);

            loadingIndicator.Visibility = Visibility.Collapsed;
        }
    }
}

項目のレイアウト変更

ItemsRepeater によって表示される項目は、その子要素のサイズ設定と配置を管理する Layout オブジェクトによって配置されます。 ItemsRepeater と共に使用する場合、Layout オブジェクトによって UI の仮想化が有効になります。 指定されているレイアウトは、StackLayoutUniformGridLayout です。 既定では、ItemsRepeater で垂直方向の StackLayout が使用されます。

スタックレイアウト (StackLayout)

StackLayout では 1 行に要素が配置され、これを水平方向または垂直方向に設定することができます。

Spacing プロパティを設定することで、項目間のスペースの量を調整できます。 Spacing は、レイアウトの Orientation の方向に適用されます。

スタック レイアウトの間隔

この例では、ItemsRepeater.Layout プロパティの StackLayout を水平方向に、間隔を 8 ピクセルに設定します。

<!-- 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(ユニフォームグリッドレイアウト)

UniformGridLayout では、折り返しレイアウトで順に要素を配置します。 OrientationHorizontal の場合、項目は左から右の順にレイアウトされ、Orientation が Vertical の場合は、上から下にレイアウトされます。 すべての項目のサイズが同じように設定されます。

均一なグリッド レイアウトの間隔

水平レイアウトの各行の項目数は、項目の最小の幅の影響を受けます。 垂直レイアウトの各列の項目数は、項目の最小の高さの影響を受けます。

  • 使用する最小サイズは、MinItemHeight および MinItemWidth プロパティを設定することで、明示的に指定できます。
  • 最小サイズを指定しない場合、最初の項目の測定されたサイズが項目ごとの最小サイズと見なされます。

また、行と列の間に含めるレイアウトの最小間隔は、MinColumnSpacingMinRowSpacing プロパティを設定することで、設定できます。

均一なグリッドのサイズと間隔の設定

行または列内の項目の数が項目の最小サイズと間隔に基づいて決定された後、(前の図で示したとおり) 行または列の最後の項目の後に未使用のスペースが残る可能性があります。 余分なスペースについては、無視するか、各項目のサイズを増やすために使用するか、項目間に追加のスペースを作成するために使用するかを指定できます。 これは ItemsStretch および ItemsJustification プロパティで制御されます。

ItemsStretch プロパティを使用すれば、未使用のスペースを埋めるために項目サイズを増やす方法を指定することができます。

このリストには使用可能な値が示されています。 定義では、既定の OrientationHorizontal であることが前提となります。

  • None: 余分なスペースが行の末尾に未使用のまま残されます。 これが既定値です。
  • 塗りつぶし: 使用可能なスペースを使い切るように、項目に追加の幅 (垂直の場合は高さ) が指定されます。
  • 均一: 使用可能なスペースを使い切るように、項目に追加の幅が指定され、縦横比を維持するために追加の高さが指定されます (垂直の場合は、高さと幅が切り替わります)。

この図は、水平レイアウトでの ItemsStretch 値の効果を示しています。

均一なグリッド項目のストレッチ

ItemsStretchNone の場合は、ItemsJustification プロパティを設定し、余分なスペースを使用して項目を揃える方法を指定できます。

このリストには使用可能な値が示されています。 定義では、既定の OrientationHorizontal であることが前提となります。

  • 先頭: 項目は行の先頭に揃えられます。 余分なスペースが行の末尾に未使用のまま残されます。 これが既定値です。
  • 中央: 項目は行の中央に揃えられます。 余分なスペースは、行の先頭と末尾に均等に分割されます。
  • 末尾: 項目は行の末尾に揃えられます。 余分なスペースが行の先頭に未使用のまま残されます。
  • SpaceAround: 項目は均等に配布されます。 各項目の前後に同量のスペースが追加されます。
  • SpaceBetween: 項目は均等に配布されます。 各項目の間に同量のスペースが追加されます。 行の先頭と末尾にはスペースは追加されません。
  • SpaceEvenly: 項目は、各項目間および行の先頭と末尾の両方に、等しいスペースで均等に分散されます。

この図には、垂直レイアウトでの ItemsStretch 値の効果が示されています (行ではなく、列に適用)。

均一なグリッド項目の位置揃え

ヒント

ItemsStretch プロパティは、レイアウトの測定 パスに影響します。 ItemsJustification プロパティは、レイアウトの配置 パスに影響します。

この例では、ItemsRepeater.Layout プロパティを 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>

ライフサイクル イベント

ItemsRepeater で項目をホストするときに、コンテンツの非同期ダウンロードの開始、選択を追跡するメカニズムと要素との関連付け、あるいはバックグラウンド タスクの停止など、項目が表示されたとき、または表示が中止されたときに何らかのアクションが必要な場合があります。

仮想化コントロールでは、Loaded/Unloaded イベントに依存することはできません。これは、要素がリサイクル時にライブ ビジュアル ツリーから削除されない場合があるためです。 代わりに、その他のイベントが、要素のライフサイクルを管理するために提供されます。 この図では、ItemsRepeater での要素のライフサイクルと、関連イベントがいつ発生するかが示されています。

ライフ サイクル イベントの図

  • ElementPrepared は、要素が使用できる状態になるたびに発生します。 これは、新しく作成された要素と、既に存在していてリサイクル キューから再利用されている要素の両方で発生します。
  • ElementClearing は、認識された項目の範囲から外れた場合など、要素がリサイクル キューに送信されるたびにすぐに発生します。
  • ElementIndexChanged は、表される項目のインデックスが変更されている、認識済みの各 UIElement で発生します。 たとえば、別の項目がデータ ソースで追加または削除された場合、次の順番にある項目のインデックスでこのイベントが受信されます。

この例では、これらのイベントを使用してカスタム選択サービスをアタッチし、項目を表示するために ItemsRepeater を使用するカスタム コントロールで項目の選択を追跡する方法を示します。

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->
<UserControl ...>
    ...
    <ScrollViewer>
        <muxc:ItemsRepeater ItemsSource="{x:Bind Items}"
                            ItemTemplate="{StaticResource MyTemplate}"
                            ElementPrepared="OnElementPrepared"
                            ElementIndexChanged="OnElementIndexChanged"
                            ElementClearing="OnElementClearing">
        </muxc:ItemsRepeater>
    </ScrollViewer>
    ...
</UserControl>
interface ISelectable
{
    int SelectionIndex { get; set; }
    void UnregisterSelectionModel(SelectionModel selectionModel);
    void RegisterSelectionModel(SelectionModel selectionModel);
}

private void OnElementPrepared(ItemsRepeater sender, ElementPreparedEventArgs args)
{
    var selectable = args.Element as ISelectable;
    if (selectable != null)
    {
        // Wire up this item to recognize a 'select' and listen for programmatic
        // changes to the selection model to know when to update its visual state.
        selectable.SelectionIndex = args.Index;
        selectable.RegisterSelectionModel(this.SelectionModel);
    }
}

private void OnElementIndexChanged(ItemsRepeater sender, ElementIndexChangedEventArgs args)
{
    var selectable = args.Element as ISelectable;
    if (selectable != null)
    {
        // Sync the ID we use to notify the selection model when the item
        // we represent has changed location in the data source.
        selectable.SelectionIndex = args.NewIndex;
    }
}

private void OnElementClearing(ItemsRepeater sender, ElementClearingEventArgs args)
{
    var selectable = args.Element as ISelectable;
    if (selectable != null)
    {
        // Disconnect handlers to recognize a 'select' and stop
        // listening for programmatic changes to the selection model.
        selectable.UnregisterSelectionModel(this.SelectionModel);
        selectable.SelectionIndex = -1;
    }
}

データの並べ替え、フィルター処理、リセット

データ セットのフィルター処理や並べ替えなどのアクションを実行するときに、従来は前のデータ セットと新しいデータを比較してから、INotifyCollectionChanged を使用して詳細な変更通知を発行していたかもしれません。 しかし、多くの場合、古いデータを新しいデータに完全に置き換え、代わりにリセット アクションを使用してコレクション変更通知をトリガーするほうが簡単です。

通常、リセットにより、コントロールで既存の子要素が解放され、スクロール位置 0 で最初から UI のビルドをやり直すことになります。これは、リセット時にデータの変更方法が正確に認識されないためです。

しかし、ItemsSource として割り当てられているコレクションがIKeyIndexMapping インターフェイスを実装することにより一意の識別子をサポートする場合、ItemsRepeater は以下をすばやく識別することができます。

  • リセットの前後に存在していたデータの再利用可能なUIエレメント
  • 以前表示されていた項目のうち、削除されたもの
  • 新しく追加された項目のうち、表示されるもの

これにより、ItemsRepeater でスクロール位置 0 からのやり直しが回避されます。 また、リセットで変更されなかったデータの UIElement をすばやく復元でき、その結果、パフォーマンスが向上します。

この例では、MyItemsSource が項目の基になるリストを折り返すカスタム データ ソースである、垂直スタックで項目のリストを表示する方法を示します。 Data プロパティが公開され、これを使用して新しいリストを再度割り当て、項目ソースとして使用し、その後、リセットをトリガーすることができます。

<ScrollViewer x:Name="sv">
    <ItemsRepeater x:Name="repeater"
                ItemsSource="{x:Bind MyItemsSource}"
                ItemTemplate="{StaticResource MyTemplate}">
       <ItemsRepeater.Layout>
           <StackLayout ItemSpacing="8"/>
       </ItemsRepeater.Layout>
   </ItemsRepeater>
</ScrollViewer>
public MainPage()
{
    this.InitializeComponent();

    // Similar to an ItemsControl, a developer sets the ItemsRepeater's ItemsSource.
    // Here we provide our custom source that supports unique IDs which enables
    // ItemsRepeater to be smart about handling resets from the data.
    // Unique IDs also make it easy to do things apply sorting/filtering
    // without impacting any state (i.e. selection).
    MyItemsSource myItemsSource = new MyItemsSource(data);

    repeater.ItemsSource = myItemsSource;

    // ...

    // We can sort/filter the data using whatever mechanism makes the
    // most sense (LINQ, database query, etc.) and then reassign
    // it, which in our implementation triggers a reset.
    myItemsSource.Data = someNewData;
}

// ...


public class MyItemsSource : IReadOnlyList<ItemBase>, IKeyIndexMapping, INotifyCollectionChanged
{
    private IList<ItemBase> _data;

    public MyItemsSource(IEnumerable<ItemBase> data)
    {
        if (data == null) throw new ArgumentNullException();

        this._data = data.ToList();
    }

    public IList<ItemBase> Data
    {
        get { return _data; }
        set
        {
            _data = value;

            // Instead of tossing out existing elements and re-creating them,
            // ItemsRepeater will reuse the existing elements and match them up
            // with the data again.
            this.CollectionChanged?.Invoke(
                this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

    #region IReadOnlyList<T>

    public ItemBase this[int index] => this.Data != null
        ? this.Data[index]
        : throw new IndexOutOfRangeException();

    public int Count => this.Data != null ? this.Data.Count : 0;
    public IEnumerator<ItemBase> GetEnumerator() => this.Data.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();

    #endregion

    #region INotifyCollectionChanged

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    #endregion

    #region IKeyIndexMapping

    private int lastRequestedIndex = IndexNotFound;
    private const int IndexNotFound = -1;

    // When UniqueIDs are supported, the ItemsRepeater caches the unique ID for each item
    // with the matching UIElement that represents the item.  When a reset occurs the
    // ItemsRepeater pairs up the already generated UIElements with items in the data
    // source.
    // ItemsRepeater uses IndexForUniqueId after a reset to probe the data and identify
    // the new index of an item to use as the anchor.  If that item no
    // longer exists in the data source it may try using another cached unique ID until
    // either a match is found or it determines that all the previously visible items
    // no longer exist.
    public int IndexForUniqueId(string uniqueId)
    {
        // We'll try to increase our odds of finding a match sooner by starting from the
        // position that we know was last requested and search forward.
        var start = lastRequestedIndex;
        for (int i = start; i < this.Count; i++)
        {
            if (this[i].PrimaryKey.Equals(uniqueId))
                return i;
        }

        // Then try searching backward.
        start = Math.Min(this.Count - 1, lastRequestedIndex);
        for (int i = start; i >= 0; i--)
        {
            if (this[i].PrimaryKey.Equals(uniqueId))
                return i;
        }

        return IndexNotFound;
    }

    public string UniqueIdForIndex(int index)
    {
        var key = this[index].PrimaryKey;
        lastRequestedIndex = index;
        return key;
    }

    #endregion
}

カスタム コレクション コントロールを作成する

ItemsRepeater を使用すれば、各項目を表示する独自の種類のコントロールを備えたカスタム コレクション コントロールを作成することができます。

これは ItemsControl を使用する場合と似ていますが、ItemsControl から派生させ、コントロール テンプレートに ItemsPresenter を配置するのではなく、Control から派生させ、コントロール テンプレートに ItemsRepeater を挿入します。 ItemsRepeater はカスタム コレクション コントロールに "含まれ" ているのに対して、ItemsControl はカスタム コレクション コントロール "である" ことになります。 これは、サポートしない継承済みのプロパティではなく、公開するプロパティを明示的に選ぶ必要もあることを意味します。

この例では、MediaCollectionView という名前のカスタム コントロールのテンプレートで ItemsRepeater を配置し、そのプロパティを公開する方法を示します。

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->
<Style TargetType="local:MediaCollectionView">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MediaCollectionView">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <ScrollViewer x:Name="ScrollViewer">
                        <muxc:ItemsRepeater x:Name="ItemsRepeater"
                                            ItemsSource="{TemplateBinding ItemsSource}"
                                            ItemTemplate="{TemplateBinding ItemTemplate}"
                                            Layout="{TemplateBinding Layout}"
                                            TabFocusNavigation="{TemplateBinding TabFocusNavigation}"/>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
public sealed class MediaCollectionView : Control
{
    public object ItemsSource
    {
        get { return (object)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ItemsSource.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register(nameof(ItemsSource), typeof(object), typeof(MediaCollectionView), new PropertyMetadata(0));

    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)GetValue(ItemTemplateProperty); }
        set { SetValue(ItemTemplateProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ItemTemplate.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ItemTemplateProperty =
        DependencyProperty.Register(nameof(ItemTemplate), typeof(DataTemplate), typeof(MediaCollectionView), new PropertyMetadata(0));

    public Layout Layout
    {
        get { return (Layout)GetValue(LayoutProperty); }
        set { SetValue(LayoutProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Layout.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty LayoutProperty =
        DependencyProperty.Register(nameof(Layout), typeof(Layout), typeof(MediaCollectionView), new PropertyMetadata(0));

    public MediaCollectionView()
    {
        this.DefaultStyleKey = typeof(MediaCollectionView);
    }
}

グループ化された項目を表示する

ItemsRepeater を別の ItemsRepeater の ItemTemplate で入れ子にすることで、入れ子になった仮想化レイアウトを作成できます。 フレームワークでは、表示されていないか、現在のビューポートの近くにない要素の不要な認識を最小限に抑えることで、リソースが効率的に利用されます。

この例では、垂直スタックでグループ化された項目のリストを表示する方法を示します。 外部の ItemsRepeater で各グループが生成されます。 各グループのテンプレートでは、別の ItemsRepeater で項目が生成されます。

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->

<Page.Resources>
    <muxc:StackLayout x:Key="MyGroupLayout"/>
    <muxc:StackLayout x:Key="MyItemLayout" Orientation="Horizontal"/>
</Page.Resources>

<ScrollViewer>
  <muxc:ItemsRepeater ItemsSource="{x:Bind AppNotifications}"
                      Layout="{StaticResource MyGroupLayout}">
    <muxc:ItemsRepeater.ItemTemplate>
      <DataTemplate x:DataType="ExampleApp:AppNotifications">
        <!-- Group -->
        <StackPanel>
          <!-- Header -->
          <TextBlock Text="{x:Bind AppTitle}"/>
          <!-- Items -->
          <muxc:ItemsRepeater ItemsSource="{x:Bind Notifications}"
                              Layout="{StaticResource MyItemLayout}"
                              ItemTemplate="{StaticResource MyTemplate}"/>
          <!-- Footer -->
          <Button Content="{x:Bind FooterText}"/>
        </StackPanel>
      </DataTemplate>
    </muxc:ItemsRepeater.ItemTemplate>
  </muxc:ItemsRepeater>
</ScrollViewer>

以下の図は、上のサンプルをガイドラインとして使用して作成された基本的なレイアウトを示しています。

アイテムリピーターを使用した入れ子レイアウト

次の例では、ユーザー設定で変更でき、水平スクロール リストとして表示される、さまざまなカテゴリを含むアプリのレイアウトを示します。 この例のレイアウトは、上の図によっても示されています。

<!-- xmlns:muxc="using:Microsoft.UI.Xaml.Controls" -->
<!-- Include the <muxc:ItemsRepeaterScrollHost> if targeting Windows 10 versions earlier than 1809. -->
<ScrollViewer>
  <muxc:ItemsRepeater ItemsSource="{x:Bind Categories}"
                      Background="LightGreen">
    <muxc:ItemsRepeater.ItemTemplate>
      <DataTemplate x:DataType="local:Category">
        <StackPanel Margin="12,0">
          <TextBlock Text="{x:Bind Name}" Style="{ThemeResource TitleTextBlockStyle}"/>
          <!-- Include the <muxc:ItemsRepeaterScrollHost> if targeting Windows 10 versions earlier than 1809. -->
          <ScrollViewer HorizontalScrollMode="Enabled"
                                          VerticalScrollMode="Disabled"
                                          HorizontalScrollBarVisibility="Auto" >
            <muxc:ItemsRepeater ItemsSource="{x:Bind Items}"
                                Background="Orange">
              <muxc:ItemsRepeater.ItemTemplate>
                <DataTemplate x:DataType="local:CategoryItem">
                  <Grid Margin="10"
                        Height="60" Width="120"
                        Background="LightBlue">
                    <TextBlock Text="{x:Bind Name}"
                               Style="{StaticResource SubtitleTextBlockStyle}"
                               Margin="4"/>
                  </Grid>
                </DataTemplate>
              </muxc:ItemsRepeater.ItemTemplate>
              <muxc:ItemsRepeater.Layout>
                <muxc:StackLayout Orientation="Horizontal"/>
              </muxc:ItemsRepeater.Layout>
            </muxc:ItemsRepeater>
          </ScrollViewer>
        </StackPanel>
      </DataTemplate>
    </muxc:ItemsRepeater.ItemTemplate>
  </muxc:ItemsRepeater>
</ScrollViewer>

要素をビューで表示する

XAML フレームワークでは既に、1) キーボードのフォーカスを受け取ったとき、または 2) ナレーターのフォーカスを受け取ったときにビューでの FrameworkElement の表示が処理されています。 要素を明示的にビューで表示する必要があるケースが他にもある場合があります。 たとえば、ユーザー アクションに応答する場合や、ページ ナビゲーション後に UI の状態を復元する場合です。

仮想化された要素をビューで表示するには、以下の操作が必要です。

  1. 項目の UIElement を認識する
  2. レイアウトを実行し、確実に要素に有効な位置が指定されるようにする
  3. 認識された要素を表示する要求を開始する

以下の例では、ページ ナビゲーション後のフラットな垂直リストでの項目のスクロール位置の復元の一環として、これらの手順を示します。 入れ子になった ItemsRepeater を使用する階層データの場合、方法は同じですが、階層レベルごとに実行する必要があります。

<ScrollViewer x:Name="scrollviewer">
  <ItemsRepeater x:Name="repeater" .../>
</ScrollViewer>
public class MyPage : Page
{
    // ...

     protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        // retrieve saved offset + index(es) of the tracked element and then bring it into view.
        // ... 
        
        var element = repeater.GetOrCreateElement(index);

        // ensure the item is given a valid position
        element.UpdateLayout();

        element.StartBringIntoView(new BringIntoViewOptions()
        {
            VerticalOffset = relativeVerticalOffset
        });
    }

    protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        base.OnNavigatingFrom(e);

        // retrieve and save the relative offset and index(es) of the scrollviewer's current anchor element ...
        var anchor = this.scrollviewer.CurrentAnchor;
        var index = this.repeater.GetElementIndex(anchor);
        var anchorBounds = anchor.TransformToVisual(this.scrollviewer).TransformBounds(new Rect(0, 0, anchor.ActualSize.X, anchor.ActualSize.Y));
        relativeVerticalOffset = this.scrollviewer.VerticalOffset - anchorBounds.Top;
    }
}

アクセシビリティを有効にする

ItemsRepeater では、既定のアクセシビリティ エクスペリエンスは提供されません。 Windows アプリの使用可能性に関するドキュメントには、アプリが包括的なユーザー エクスペリエンスを提供するのに役立つ豊富な情報が記載されています。 ItemsRepeater を使ってカスタム コントロールを作成する場合は、必ず、カスタム オートメーション ピアに関するドキュメントを参照してください。

キーボード操作

ItemsRepeater で提供されるフォーカス移動のためのキーボード操作の最小限のサポートは、XAML のキーボード操作の 2D 方向ナビゲーションに関する記述に基づいています。

方向ナビゲーション

ItemsRepeater の XYFocusKeyboardNavigation モードは、既定で Enabled になっています。 目的のエクスペリエンスに応じて、Home、End、PageUp、PageDown などの一般的なキーボード操作のサポートの追加を検討してください。

ItemsRepeater では、項目の既定のタブ順序が (仮想化されているかどうかに関わらず)、データ内に項目が指定されているのと同じ順序に従っていることが自動的に確保されます。 ItemsRepeater では、既定でその TabFocusNavigation プロパティが、一般的な既定値である Local ではなく、Once に設定されます。

ItemsRepeater では、最後にフォーカスが置かれた項目が自動的に記憶されません。 これは、ユーザーが Shift + Tab キーを使用したときに、最後に認識された項目に移動される可能性があることを意味します。

スクリーン リーダーでの "YX 項目" の読み上げ

PositionInSetSizeOfSet の値などの、適切なオートメーション プロパティの設定を管理する必要があり、項目の追加、移動、削除などが行われた場合、確実に最新の状態が保たれているようにする必要があります。

一部のカスタム レイアウトでは、表示順序が明白でない場合があります。 ユーザーは最低でも、スクリーン リーダーによって使用される PositionInSet および SizeOfSet プロパティの値が、データでの項目の表示順序と一致することを期待します (0 ベースではなく、自然計数と一致させるために 1 単位のオフセット)。

これを実現する最善の方法は、項目コントロールのオートメーション ピアで GetPositionInSetCore および GetSizeOfSetCore メソッドを実装し、コントロールによって表されるデータ セットの項目の位置をレポートすることです。 支援技術によるアクセスの実行時にのみ、値が計算され、それを最新の状態に保つことが大変なことではなくなります。 値はデータの順序と一致します。

この例では、CardControl と呼ばれるカスタム コントロールを表示するときに、これをどのように実行するかを示します。

<ScrollViewer >
    <ItemsRepeater x:Name="repeater" ItemsSource="{x:Bind MyItemsSource}">
       <ItemsRepeater.ItemTemplate>
           <DataTemplate x:DataType="local:CardViewModel">
               <local:CardControl Item="{x:Bind}"/>
           </DataTemplate>
       </ItemsRepeater.ItemTemplate>
   </ItemsRepeater>
</ScrollViewer>
internal sealed class CardControl : CardControlBase
{
    protected override AutomationPeer OnCreateAutomationPeer() => new CardControlAutomationPeer(this);

    private sealed class CardControlAutomationPeer : FrameworkElementAutomationPeer
    {
        private readonly CardControl owner;

        public CardControlAutomationPeer(CardControl owner) : base(owner) => this.owner = owner;

        protected override int GetPositionInSetCore()
          => ((ItemsRepeater)owner.Parent)?.GetElementIndex(this.owner) + 1 ?? base.GetPositionInSetCore();

        protected override int GetSizeOfSetCore()
          => ((ItemsRepeater)owner.Parent)?.ItemsSourceView?.Count ?? base.GetSizeOfSetCore();
    }
}