MAUI の XAML で「表示はアイテム」「コマンドは親 ViewModel」という典型パターンを取ると、Compiled Binding(x:DataType)によりビルド時エラーにぶつかりがちです。本記事では、CollectionView の DataTemplate 内から複数ソースに型安全にバインドするための“実務で使える 4 つの解決策”を、再現コード・ポイント・落とし穴まで含めて体系的に解説します。
XAML DataTemplate で複数ソースにバインドするときのコンパイルエラー
質問の背景
.NET MAUI の CollectionView でアイテムを表示しつつ、タップ時にはページ側の ViewModel のコマンドを呼びたい――現場で最もよくある UI 要件です。DataTemplate の x:DataType にアイテム型(例:SampleItem)を設定し、TapGestureRecognizer.Command にはページの ViewModel(例:SampleViewModel)の ItemClickCommand をバインド、CommandParameter には「現在のアイテム({Binding .})」を渡します。
しかし Compiled Binding を有効にしていると、DataTemplate の x:DataType とコマンド側の型が一致せずビルド時に警告/エラー、さらには実行時不具合(コマンドが発火しない/意図しないデータ参照)が発生します。
よくあるエラーパターン(再現コード)
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Pages.SamplePage">
<ContentPage.BindingContext>
<vm:SampleViewModel />
</ContentPage.BindingContext>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:SampleItem">
<Grid Padding="12">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding ItemClickCommand}"
CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Label Text="{Binding Title}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
DataTemplate の x:DataType は SampleItem です。そのスコープでは {Binding ...} は「SampleItem に存在するプロパティ」として解析されるため、ItemClickCommand が見つからずコンパイルエラー/警告になります。
解決の全体像
解はシンプルです。「コマンドだけはページの ViewModel をソースにする」と 明示 します。以下の 4 つのアプローチから、可読性・保守性・制約に応じて選びます。
| 方法 | 実装ポイント | メリット | デメリット |
|---|---|---|---|
| ① x:Reference で親 ViewModel にアクセス | <CollectionView x:Name="cv"> を付け、Command="{Binding Path=BindingContext.ItemClickCommand, Source={x:Reference cv}}" | 直感的で短い。DataTemplate はそのまま。 | 参照先に x:Name が必要。 |
| ② スコープを局所的に切替(x:DataType を局所化) | TapGestureRecognizer のプロパティ要素構文を使い、コマンド側バインディングだけ x:DataType を VM に切替。 | DataTemplate の型宣言を崩さない。 型安全の恩恵を保ちつつ“必要箇所のみ”例外に。 | 記述がやや冗長。IDE の補完で気づきにくい。 |
| ③ RelativeSource を使う標準パターン | Source={RelativeSource AncestorType={x:Type ContentPage}} などで親要素の BindingContext を参照。 | 名前不要。XAML の定番パターンで応用範囲が広い。 | パス指定(BindingContext.***)がやや長い。 |
| ④ アイテムに親 VM 参照(Owner)を持たせる | モデルに Owner(親 VM)を持たせ、Owner.ItemClickCommand にバインド。 | DataTemplate 内で完結し型安全。 補完が効く。 | モデルが ViewModel に依存し設計が重くなる。 |
解決策の詳細と完全サンプル
前提コード(ViewModel / Model)
// Models/SampleItem.cs
namespace MyApp.Models;
public class SampleItem
{
public string Title { get; set; } = "";
// ④を使うなら:
public MyApp.ViewModels.SampleViewModel? Owner { get; set; }
}
// ViewModels/SampleViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace MyApp.ViewModels;
public class SampleViewModel
{
public ObservableCollection Items { get; } = new();
public ICommand ItemClickCommand { get; }
public SampleViewModel()
{
ItemClickCommand = new Command<MyApp.Models.SampleItem>(OnItemClicked);
// ダミーデータ
for (int i = 1; i <= 20; i++)
{
Items.Add(new MyApp.Models.SampleItem {
Title = $"Item {i}",
// ④を使う場合のみ:
Owner = this
});
}
}
private void OnItemClicked(MyApp.Models.SampleItem item)
{
// TODO: 遷移や処理
System.Diagnostics.Debug.WriteLine($"Clicked: {item.Title}");
}
}
補足:Command<T>(または AsyncRelayCommand<T>)のようにジェネリック型を使うと、XAML 側で CommandParameter に目的の型(ここでは SampleItem)を渡せます。
① x:Reference で親 ViewModel にアクセス(最小コードで強力)
ビュー要素(ここでは CollectionView)に名前を付け、その要素の BindingContext をたどります。DataTemplate の x:DataType は SampleItem のままで OK。
<CollectionView x:Name="cv" ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:SampleItem">
<Grid Padding="12">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Path=BindingContext.ItemClickCommand, Source={x:Reference cv}}"
CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Label Text="{Binding Title}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
- ポイント:
Sourceを指定しているのは コマンドのバインディングだけ です。要素全体のBindingContextはアイテムのままなので、CommandParameter="{Binding .}"で現在のアイテムが安全に渡せます。 - 落とし穴: 参照先の要素名を変更したら、XAML でも忘れずに修正。
② 局所的に x:DataType を切り替える(型安全を保ちつつ最小範囲で例外)
コンパイル時型情報(x:DataType)を“必要箇所だけ”切り替えるテクニックです。プロパティ要素構文の <Binding ... /> を用いると、コマンドのバインディングだけ に ViewModel の型を宣言できます。
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:SampleItem">
<Grid Padding="12">
<Grid.GestureRecognizers>
<TapGestureRecognizer>
<TapGestureRecognizer.Command>
<Binding
x:DataType="vm:SampleViewModel"
Path="ItemClickCommand"
Source="{RelativeSource AncestorType={x:Type ContentPage}}" />
</TapGestureRecognizer.Command>
<TapGestureRecognizer.CommandParameter>
<Binding x:DataType="local:SampleItem" Path="." />
</TapGestureRecognizer.CommandParameter>
</TapGestureRecognizer>
</Grid.GestureRecognizers>
<Label Text="{Binding Title}" />
</Grid>
</DataTemplate>
- ポイント: DataTemplate の
x:DataTypeはSampleItemのまま。CommandとCommandParameterのそれぞれに 別の 型情報を付与して、IntelliSense とビルド時検査の恩恵を保ちます。 - 落とし穴: 補完候補で見えづらいので、チーム規約に「バインディングをプロパティ要素構文にする」基準を決めておくと吉。
③ RelativeSource を使う標準パターン(名前不要で汎用)
ビジュアルツリーの祖先要素をたどり、その BindingContext(= ViewModel)を参照します。ViewModel を AncestorType に指定するのは間違い(VM は視覚ツリー上の要素ではない)なので、Page/ContentPage/CollectionView などの UI 要素を指定します。
<TapGestureRecognizer
Command="{Binding Path=BindingContext.ItemClickCommand,
Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding .}" />
カスタムコントロール内であれば、AncestorType を CollectionView や ContentView に合わせます。
Command="{Binding Path=BindingContext.ItemClickCommand,
Source={RelativeSource AncestorType={x:Type CollectionView}}}"
④ アイテムに親 ViewModel 参照(Owner)を持たせる(設計として許容できる場合のみ)
モデルが ViewModel を知る設計になりますが、DataTemplate 内での型整合性は最も高く保てます。
<DataTemplate x:DataType="local:SampleItem">
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Owner.ItemClickCommand}"
CommandParameter="{Binding .}" />
</Grid.GestureRecognizers>
<Label Text="{Binding Title}" />
ただしモデルが UI 層に依存するため、ドメインモデルの純度を重視するプロジェクトでは不適です。DTO/ViewModel 専用アイテム(例:SampleItemView)として割り切るのは現実解です。
エラーの正体と XAML スコープの理解
- DataTemplate の
x:DataTypeはスコープを固定:その要素配下での{Binding ...}は「その型に存在するメンバ」としてコンパイル時に解析されます。 - Compiled Binding の狙い:IntelliSense とビルド時チェックで「typo/型不一致」を早期発見すること。利点の裏返しで、複数の型を一度に参照したい場面では“どの型でチェックするか”を明示する必要が出ます。
- ランタイムとの整合:
SourceやRelativeSourceで実オブジェクトを指定すれば、コンパイル時の型情報と実行時のデータコンテキストが噛み合い、安定動作します。
サンプル全体(ページ XAML まとめ)
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Pages.SamplePage">
<CollectionView x:Name="cv" Grid.Row="1" ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="local:SampleItem">
<Frame Padding="12" Margin="0,8" HasShadow="True">
<Frame.GestureRecognizers>
<!-- ★① x:Reference 版(推奨) -->
<TapGestureRecognizer
Command="{Binding Path=BindingContext.ItemClickCommand, Source={x:Reference cv}}"
CommandParameter="{Binding .}" />
</Frame.GestureRecognizers>
<VerticalStackLayout>
<Label Text="{Binding Title}" FontSize="18" />
<Label Text="タップで ViewModel のコマンドへ" FontSize="12" />
</VerticalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
どれを選ぶ?実務の判断基準
| 状況 | 最初の一手 | 代替 | 避けたい選択 |
|---|---|---|---|
対象要素に x:Name を付けられる | ① x:Reference | ③ RelativeSource | ④ Owner(設計を汚す場合) |
| 名前を付けづらい/再利用テンプレート | ③ RelativeSource | ② 局所的切替 | ④ Owner |
| 特定 1 箇所だけ型を切替えたい | ② 局所的切替 | ① or ③ | ④ Owner |
| View 専用アイテム(DTO)で割り切れる | ④ Owner | ① or ③ | なし |
ありがちな落とし穴と対処
- AncestorType に ViewModel を指定する:
RelativeSourceは視覚ツリー上の祖先要素を探すため、VM そのものは指定できません。ContentPageなどの UI 要素を指定し、BindingContext.***と辿るのが正解。 - Source を指定し忘れる:② のように型情報だけ VM に切り替えると、実体はアイテムのままでランタイムに齟齬が出ます。必ず
Source/RelativeSourceを併用。 - Trigger/Behavior の内部:
DataTriggerやEventToCommandBehavior内もスコープは同様。コマンド側だけSourceを明示し、パラメータは{Binding .}でアイテムを渡す、という設計を崩さないのがコツ。 - SelectionChanged を使う場合:
CollectionView.SelectionChangedCommandのCommandParameterにはSelectionChangedEventArgs相当が来ます。単一アイテムを渡したいならTapGestureRecognizerのほうが明快です。
パフォーマンスと可読性
Compiled Binding は XAML ロード後に通常の Binding オブジェクトを生成します。①〜④の方式間で描画/応答性能の顕著な差は基本ありません。判断軸は以下です。
- 可読性:① > ③ > ② > ④(プロジェクト規約次第)
- 影響範囲の小ささ:②(局所化) > ① ≒ ③ > ④
- 設計負債リスク:④ が最大。ドメインモデルの純度を下げます。
導入〜検証のチェックリスト
- DataTemplate の
x:DataTypeをアイテム型に設定したか。 - コマンド側の
BindingにSourceまたはRelativeSourceを指定したか。 - パラメータは
{Binding .}で現在アイテムを渡しているか。 - IDE の警告(XAML Hot Reload/ビルド時)をゼロにしたか。
- タップ〜コマンド〜引数の流れをデバッグ出力で一度確認したか。
補足テクニック(CommunityToolkit など)
- AsyncRelayCommand<T> の活用:非同期処理なら
AsyncRelayCommand<SampleItem>を使うと、XAML で渡したCommandParameterの型が崩れにくくなります。 - Attached Property で共通化:テンプレートごとに同じ指定を書くのが冗長なら、添付プロパティで「親 VM のコマンドをバインドする」振る舞いをカプセル化するのも一案です(ただし過度な抽象化はデバッグコスト増)。
- デザイン時データ(d:DataContext):デザイナ体験を良くしたい場合、
d:DataContextとd:DesignInstanceを併用し、実行時のx:DataTypeとは切り離して設計できます。
まとめ
- 原因:DataTemplate の
x:DataTypeが アイテム型で固定されるため、親 ViewModel のコマンドを同じスコープで解決できずコンパイルエラーになる。 - 解決:バインディングソースを明示し、必要に応じて 型情報(x:DataType)を局所的に切替える。
- 実務推奨:まず ① x:Reference。名前付けが難しければ ③ RelativeSource。ピンポイントで型安全を維持したい箇所は ②。設計として許容できるなら ④。
- パフォーマンス差:ほぼ皆無。可読性と保守性で選ぶ。

コメント