maui-collectionview について
maui-collectionviewは、.NET MAUIのCollectionViewでスクロール可能なリストやグリッドを作る開発者向けのリファレンススキルです。APIの選択やバインディング・レイアウト上の問題の回避、必要に応じたListViewからの移行を扱います。
主な機能
- 01リストとグリッドのレイアウト指針
- 02選択・グループ化・更新のパターン
- 03インクリメンタル読み込みとスワイプ操作
- 04バインディングと性能の注意点
- 05ListViewからの移行ガイド
活用シーン
- 01データバインドされたリストやグリッドの作成
- 02選択・グループ化・プル更新の追加
- 03インクリメンタル読み込みやスワイプ操作の実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-collectionviewインストール後、Claude Code で/maui-collectionviewを入力して読み込まれたか確認します。
同梱ファイルreferences/collectionview-api.md
詳細
CollectionView — .NET MAUI
CollectionView は、.NET MAUI でスクロール可能なデータのリストやグリッドを表示するための主要なコントロールです。パフォーマンスが向上し、柔軟なレイアウトに対応しており、ViewCell も不要なため、ListView に代わるコントロールです。
使用する場面
- スクロール可能なデータ項目のリストまたはグリッドを表示する
- オブジェクトのコレクションを、テンプレート化された項目レイアウトにバインドする
- 選択(単一または複数)、グループ化、またはプルツーリフレッシュを追加する
- 無限スクロール/インクリメンタル読み込みを実装する
- リスト項目にスワイプ操作を表示する
- データがない場合に空の状態を表示する
使用しない場面
- 項目数が固定された静的なレイアウト —
GridまたはStackLayoutを直接使用してください - 地図のピンのリスト —
Microsoft.Maui.Controls.MapsNuGet パッケージを使用してください - テーブル形式のデータ入力フォーム — 標準のフォームコントロールを使用してください
- 操作のない、テキストのみのシンプルなリスト —
StackLayout上のBindableLayoutを検討してください
スコープの制御 — 質問されたことだけに答える
このスキルは参照資料であり、すべてを適用するチェックリストではありません。ほとんどの質問では、ここにあるセクションのうち1つか2つだけが必要です。残りまで持ち出すと、回答の質が下がります。
中止条件 — 次の場合は対応しないでください:
- ユーザーの質問が限定的な場合。 その質問だけに答えてください。質問されていないグループ化、スワイプ操作、空のビュー、スナップポイント、パフォーマンスのヒントを付け加えないでください。
- ユーザーの既存コードがすでに動作している場合。 ここにある例に合わせるためだけに、動作するマークアップを書き換えないでください。具体的な問題があれば指摘し、なければ問題がないことを伝えて、質問された内容に答えてください。
- 変更がスタイル上のものにすぎない場合。 名前の変更、属性の並べ替え、すでに正しく動作するテンプレートの構成変更は、修正ではなく無用な変更です。
- 対象のコントロールが
CollectionViewではない場合。CarouselView、BindableLayout、および保守中のコードにあるListViewには、それぞれ異なるルールがあります。ユーザーが求めていないListViewのコードを書き換えないでください。ただし、どのコントロールを使うべきか尋ねられた場合や、Xamarin.Forms からの移行の場合は、CollectionViewを推奨してください(ListView からの移行を参照)。 - 問題の本質がバインディング、DI、またはナビゲーションの問題で、リストが関係しているにすぎない場合。
maui-data-binding、maui-dependency-injection、またはmaui-shell-navigationを参照してください。
以下の API セクションは参照資料であり、チェックリストではありません。関連する場合にのみ提示してください。 違反すると動作しないコードになったり、コンパイル時チェックがひそかに失われたりするため、譲れないルールが4つあります:
CollectionViewでDataTemplateのルートにViewCellを使用しないでください。- 初回レンダリング後にリストを変更する場合は、
ObservableCollection<T>を使用してください。 - バインドされたコレクションは UI スレッド上で変更してください。
- コンパイル済みバインディングを使用するには、すべての
DataTemplateとページのルートにx:DataTypeを設定してください。
それ以外のすべて(サイズ設定方法、スナップポイント、ヘッダー/フッター、空のビューなど)は任意であり、ユーザーの実際の問題に対応する場合にのみ提示してください。
入力
ItemsSourceにバインドするデータソース(通常はObservableCollection<T>)- 各項目の表示方法を定義する
DataTemplate - 任意:レイアウト設定、選択モード、グループ化モデル、空のビュー
基本セットアップ
そのままコピーして貼り付けられる完全なページです。重要なのは 2 点です。このスキル内のすべての x:DataType="models:Item" が前提とする xmlns:models 宣言と、ルートの x:DataType です。これがないと、外側の ItemsSource バインディングがコンパイルされません。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ItemsViewModel"
x:Class="MyApp.ItemsPage">
<ContentPage.BindingContext>
<vm:ItemsViewModel />
</ContentPage.BindingContext>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
以降のスニペットでは、CollectionView 要素のみを示します。スニペットをユーザーに渡す際は、使用する各プレフィックスに対応する xmlns: 宣言を含めてください。宣言がないと XAML がコンパイルされません。
上記のインラインの <ContentPage.BindingContext> により、例が単独で完結します。依存性注入を使用するアプリでは、代わりに ViewModel を登録し、コンストラクターインジェクションを通じて割り当ててください(BindingContext = vm;)。詳しくは maui-dependency-injection スキルを参照してください。
主なルール:
- 追加や削除時に UI が更新されるよう、
ItemsSourceはObservableCollection<T>にバインドします。 - 各アイテムテンプレートのルートは
LayoutまたはViewにします。ViewCellは使用しないでください。 - コンパイル済みバインディングを使用するには、
DataTemplateに必ずx:DataTypeを設定します。
レイアウト
配置を制御するには ItemsLayout を設定します。既定値は VerticalList です。
| レイアウト | XAML 値 |
|---|---|
| 垂直リスト | VerticalList(既定) |
| 水平リスト | HorizontalList |
| 垂直グリッド | Orientation="Vertical" を指定した GridItemsLayout |
| 水平グリッド | Orientation="Horizontal" を指定した GridItemsLayout |
グリッドレイアウト
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
VerticalItemSpacing="8"
HorizontalItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Border Padding="8" StrokeThickness="0">
<VerticalStackLayout>
<Image Source="{Binding Image}" HeightRequest="120" Aspect="AspectFill" />
<Label Text="{Binding Name}" FontAttributes="Bold" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
水平リスト
<CollectionView ItemsSource="{Binding Items}"
ItemsLayout="HorizontalList" />
選択
選択モード
| モード | バインドするプロパティ | バインディングモード |
|---|---|---|
None | — | — |
Single | SelectedItem | TwoWay |
Multiple | SelectedItems | OneWay |
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
SelectionChangedCommand="{Binding ItemSelectedCommand}" />
Multiple 選択では、SelectedItems(型は IList<object>)にバインドします。
<CollectionView SelectionMode="Multiple"
SelectedItems="{Binding ChosenItems, Mode=OneWay}" />
選択時のビジュアル状態
VisualStateManager を使って選択したアイテムをハイライト表示します。
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Grid Padding="8">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
グループ化
List<T>を継承するグループクラスを作成します。
public class AnimalGroup : List<Animal>
{
public string Name { get; }
public AnimalGroup(string name, List<Animal> animals) : base(animals)
{
Name = name;
}
}
ObservableCollection<AnimalGroup>にバインドし、IsGrouped="True"を設定します。
<CollectionView ItemsSource="{Binding AnimalGroups}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="models:AnimalGroup">
<Label Text="{Binding Name}"
FontAttributes="Bold"
BackgroundColor="{StaticResource Gray100}"
Padding="8" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Animal">
<Label Text="{Binding Name}" Padding="16,4" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
プルして更新
CollectionView を RefreshView で囲みます。処理が完了したら IsRefreshing を false に戻します。
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView ItemsSource="{Binding Items}" />
</RefreshView>
段階的な読み込み(無限スクロール)
<CollectionView ItemsSource="{Binding Items}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}" />
⚠️ 仮想化しないレイアウトでは使用しないでください。
LinearItemsLayoutとGridItemsLayoutは仮想化をサポートしています。CollectionViewの代わりにStackLayout上でBindableLayoutを使用すると仮想化されず、しきい値到達イベントが無限に発生します。
SwipeView — DataTemplate 内からのバインディング
DataTemplate 内のコマンドから ViewModel に直接アクセスすることはできません。RelativeSource AncestorType を使用します。
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="Red"
Command="{Binding BindingContext.DeleteCommand, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="8">
<Label Text="{Binding Name}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
空のビュー
ItemsSource が空または null の場合に表示されます。
<CollectionView ItemsSource="{Binding SearchResults}"
EmptyView="No items found." />
カスタムの空ビューを表示するには、ContentView で囲みます。
<CollectionView ItemsSource="{Binding SearchResults}">
<CollectionView.EmptyView>
<ContentView>
<VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Image Source="empty_state.png" WidthRequest="120" />
<Label Text="Nothing here yet" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</ContentView>
</CollectionView.EmptyView>
</CollectionView>
ヘッダーとフッター
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.Header>
<Label Text="Header" FontAttributes="Bold" Padding="8" />
</CollectionView.Header>
<CollectionView.Footer>
<Label Text="Footer" FontAttributes="Italic" Padding="8" />
</CollectionView.Footer>
</CollectionView>
ヘッダーまたはフッターがデータバインドされている場合は、HeaderTemplate / FooterTemplate を使用します。
スクロール
ScrollTo
インデックスまたはアイテムを指定して、プログラムからスクロールします。
// Scroll to index
collectionView.ScrollTo(index: 10, position: ScrollToPosition.Center, animate: true);
// Scroll to item
collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, animate: true);
| ScrollToPosition | 動作 |
|---|---|
MakeVisible | アイテムが表示されるよう、必要な分だけスクロールします |
Start | アイテムをビューポートの先頭に移動します |
Center | アイテムをビューポートの中央に移動します |
End | アイテムをビューポートの末尾に移動します |
スナップポイント
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>
SnapPointsType:None、Mandatory、MandatorySingleSnapPointsAlignment:Start、Center、End
ListView からの移行
ListView は引き続きコンパイルされますが、.NET 10 以降では [Obsolete] とマークされています
(「ListView is deprecated. Please use CollectionView instead.」)。.NET 9 以前では非推奨ではないため、
非推奨として説明する前に、プロジェクトのターゲットフレームワークを確認してください。ユーザーから
使用するコントロールについて尋ねられた場合、または Xamarin.Forms から移行する場合は、
CollectionView を推奨してください。こちらは高速で、ViewCell が不要なうえ、柔軟な
レイアウトをサポートします。避けるべきなのは、ユーザーが変更を求めていない ListView のコードを
黙って書き換えることです。
ListView | CollectionView での相当機能 |
|---|---|
ViewCell テンプレートのルート | 任意の View/Layout ルート — ViewCell はサポートされません |
ItemSelected イベント | SelectionChanged イベント、または SelectionChangedCommand |
ItemTapped イベント | 項目テンプレート内の TapGestureRecognizer — SelectionChanged は選択が変わったときにのみ発生するため、すでに選択されている項目をタップしても再発生しません |
IsPullToRefreshEnabled + Refreshing | CollectionView を RefreshView でラップします |
IsGroupingEnabled | IsGrouped |
HasUnevenRows="True" | デフォルトの ItemSizingStrategy="MeasureAllItems" |
RowHeight(固定の高さ) | 項目テンプレートで高さを設定します。MeasureFirstItem は最初の項目の測定サイズを再利用するだけで、明示的な行の高さではありません |
SeparatorVisibility/SeparatorColor | 相当機能はありません — 項目テンプレート内に BoxView/Border を描画します |
区切り線の API がないことは、移行時によくある驚きのひとつです。CollectionView には組み込みの区切り線がないため、テンプレートに自分で追加してください。
パフォーマンスのヒント
ユーザーからパフォーマンスの問題が報告された場合、またはパフォーマンスについて明示的に質問された場合にのみ適用してください。デフォルトのチェックリストではありません。
- 項目のサイズが均一なら
MeasureFirstItemを使用します。すべての項目を個別に測定するデフォルトのMeasureAllItemsよりも大幅に高速です。LinearItemsLayout/GridItemsLayoutではなく、CollectionView自体に設定してください(StructuredItemsViewで宣言されています)。<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="models:Item"> <Grid Padding="8" ColumnDefinitions="44,*" ColumnSpacing="8"> <Image WidthRequest="44" HeightRequest="44" /> <Label Grid.Column="1" Text="{Binding Name}" VerticalOptions="Center" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView>MeasureFirstItemが適切でない場合 — 次の条件では、デフォルトのMeasureAllItemsを維持してください。- 項目ごとに高さが異なる場合(折り返しテキスト、任意の行、アスペクト比が異なる画像など)。最初の項目のサイズがすべてに適用されるため、ほかの項目が切り取られたり引き伸ばされたりします。
DataTemplateSelectorが異なるテンプレートを返す場合。最初の項目はほかの項目を代表しません。- 最初の項目が標準的でない場合(ヘッダー風の行や「注目」行など)。すべての項目がそのサイズを引き継ぎます。データの並べ替えで対処するのは不適切です。代わりに
MeasureAllItemsを使用してください。 - 最初の項目の測定時に実行時データがまだ読み込まれておらず、そのデータによって項目のサイズが決まる場合。
- 最初の描画後にリストが変更される場合は
ObservableCollection<T>を使用します。INotifyCollectionChangedを実装しているため、インプレースのAdd/Remove/Insertで UI が段階的に更新されます。バインド後に変更されないリストならList<T>で問題ありません。なお、コレクションの種類にかかわらず、ItemsSourceを置き換えるとすべてが再描画されます。そのため、再割り当てではなく、バインド済みコレクションをインプレースで変更してください。 - UI スレッドでコレクションを更新します —
MainThread.BeginInvokeOnMainThread(() => Items.Add(item))。
よくある落とし穴
| 問題 | 解決方法 |
|---|---|
| 項目を変更しても UI が更新されない | List<T> ではなく ObservableCollection<T> を使用します。 |
| アプリがクラッシュする、または項目が空白になる | ViewCell は絶対に使用しないでください — テンプレートのルートには Grid、StackLayout、または任意の View を使用します。 |
| 項目が消える、またはレイアウトが崩れる | ItemsSource とコレクションは必ず UI スレッド(MainThread.BeginInvokeOnMainThread)で更新します。 |
| 増分読み込みが際限なく実行される | レイアウトに StackLayout を使用しないでください。LinearItemsLayout または GridItemsLayout を使用します。 |
| EmptyView が正しく描画されない | カスタムの空のビューは ContentView でラップします。 |
| スクロールのパフォーマンスが低い | 項目のサイズが均一な場合は、MeasureFirstItem サイズ設定戦略を使用します。 |
ItemSizingStrategy がコンパイルできない | ItemSizingStrategy は StructuredItemsView で宣言されています — <LinearItemsLayout> / <GridItemsLayout> ではなく、<CollectionView> に設定します。 |
| 項目が切り取られる、または引き伸ばされる | MeasureFirstItem は項目のサイズが均一であることを前提とします。高さが異なる項目には、既定の MeasureAllItems を使用します。 |
| 選択状態が表示されない | 項目テンプレートのルート要素に VisualState Name="Selected" を追加します。 |
| SwipeView のコマンドでバインディングエラーが発生する | 項目テンプレート内から ViewModel にアクセスするには、RelativeSource AncestorType を使用します。 |
検証
作成または編集した CollectionView のマークアップを返す前に、次の点を確認してください。
-
DataTemplateのルートがView/Layoutであり、ViewCellではない。 - コンパイル済みバインディングのために、
DataTemplateでx:DataTypeを宣言している。 - リストが変更される場合、
ItemsSourceがObservableCollection<T>にバインドされている。 -
ItemSizingStrategyを使用する場合、レイアウトではなく<CollectionView>に設定している。 -
Multiple選択ではSelectedItemsにバインドし、Single選択ではSelectedItem(TwoWay)にバインドしている。 - 更新が完了したときに、
RefreshView.IsRefreshingがfalseに戻されている。 - 回答が、ユーザーの依頼内容 のみ を扱っている — 求められていないセクションを追加していない。