【.NET MAUI】XAML DataTemplateで複数ソースにバインドするとコンパイルエラーになる理由と4つの解決策

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:DataTypeSampleItem です。そのスコープでは {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:DataTypeSampleItem のままで OK。

&lt;CollectionView x:Name="cv" ItemsSource="{Binding Items}"&gt;
  &lt;CollectionView.ItemTemplate&gt;
    &lt;DataTemplate x:DataType="local:SampleItem"&gt;
      &lt;Grid Padding="12"&gt;
        &lt;Grid.GestureRecognizers&gt;
          &lt;TapGestureRecognizer
            Command="{Binding Path=BindingContext.ItemClickCommand, Source={x:Reference cv}}"
            CommandParameter="{Binding .}" /&gt;
        &lt;/Grid.GestureRecognizers&gt;
        &lt;Label Text="{Binding Title}" /&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/CollectionView.ItemTemplate&gt;
&lt;/CollectionView&gt;
  • ポイント: 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:DataTypeSampleItem のまま。CommandCommandParameter のそれぞれに 別の 型情報を付与して、IntelliSense とビルド時検査の恩恵を保ちます。
  • 落とし穴: 補完候補で見えづらいので、チーム規約に「バインディングをプロパティ要素構文にする」基準を決めておくと吉。

③ RelativeSource を使う標準パターン(名前不要で汎用)

ビジュアルツリーの祖先要素をたどり、その BindingContext(= ViewModel)を参照します。ViewModel を AncestorType に指定するのは間違い(VM は視覚ツリー上の要素ではない)なので、Page/ContentPage/CollectionView などの UI 要素を指定します。

&lt;TapGestureRecognizer
  Command="{Binding Path=BindingContext.ItemClickCommand,
                    Source={RelativeSource AncestorType={x:Type ContentPage}}}"
  CommandParameter="{Binding .}" /&gt;

カスタムコントロール内であれば、AncestorTypeCollectionViewContentView に合わせます。

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/型不一致」を早期発見すること。利点の裏返しで、複数の型を一度に参照したい場面では“どの型でチェックするか”を明示する必要が出ます。
  • ランタイムとの整合SourceRelativeSource で実オブジェクトを指定すれば、コンパイル時の型情報と実行時のデータコンテキストが噛み合い、安定動作します。

サンプル全体(ページ 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 の内部DataTriggerEventToCommandBehavior 内もスコープは同様。コマンド側だけ Source を明示し、パラメータは {Binding .} でアイテムを渡す、という設計を崩さないのがコツ。
  • SelectionChanged を使う場合CollectionView.SelectionChangedCommandCommandParameter には SelectionChangedEventArgs 相当が来ます。単一アイテムを渡したいなら TapGestureRecognizer のほうが明快です。

パフォーマンスと可読性

Compiled Binding は XAML ロード後に通常の Binding オブジェクトを生成します。①〜④の方式間で描画/応答性能の顕著な差は基本ありません。判断軸は以下です。

  • 可読性:① > ③ > ② > ④(プロジェクト規約次第)
  • 影響範囲の小ささ:②(局所化) > ① ≒ ③ > ④
  • 設計負債リスク:④ が最大。ドメインモデルの純度を下げます。

導入〜検証のチェックリスト

  • DataTemplate の x:DataType をアイテム型に設定したか。
  • コマンド側の BindingSource または RelativeSource を指定したか。
  • パラメータは {Binding .} で現在アイテムを渡しているか。
  • IDE の警告(XAML Hot Reload/ビルド時)をゼロにしたか。
  • タップ〜コマンド〜引数の流れをデバッグ出力で一度確認したか。

補足テクニック(CommunityToolkit など)

  • AsyncRelayCommand<T> の活用:非同期処理なら AsyncRelayCommand<SampleItem> を使うと、XAML で渡した CommandParameter の型が崩れにくくなります。
  • Attached Property で共通化:テンプレートごとに同じ指定を書くのが冗長なら、添付プロパティで「親 VM のコマンドをバインドする」振る舞いをカプセル化するのも一案です(ただし過度な抽象化はデバッグコスト増)。
  • デザイン時データ(d:DataContext):デザイナ体験を良くしたい場合、d:DataContextd:DesignInstance を併用し、実行時の x:DataType とは切り離して設計できます。

まとめ

  • 原因:DataTemplate の x:DataTypeアイテム型で固定されるため、親 ViewModel のコマンドを同じスコープで解決できずコンパイルエラーになる。
  • 解決バインディングソースを明示し、必要に応じて 型情報(x:DataType)を局所的に切替える。
  • 実務推奨:まず ① x:Reference。名前付けが難しければ ③ RelativeSource。ピンポイントで型安全を維持したい箇所は ②。設計として許容できるなら ④。
  • パフォーマンス差:ほぼ皆無。可読性と保守性で選ぶ。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次