.NET MAUIのDataTemplateでCommandが発火しない原因と解決策(XC0045・x:DataType・コンパイル時バインディング対応)

.NET MAUI の CollectionView + DataTemplate を コンパイル時バインディング(x:DataType)で組んだとき、行内の Delete ボタンや TapGestureRecognizer から ViewModel の Command が発火しない/XamlC 警告 XC0045 が出る問題に遭遇することがあります。原因は「DataTemplate の型固定」と「コマンドの参照先の型の食い違い」です。型安全を維持したまま解決する実装パターンを具体例付きで解説します。

目次

発生する症状(XC0045 と Command 不発)

典型的には、次のような状態になります。

  • CollectionView の DataTemplate に x:DataType=”models:EmployeeModel” を付けている
  • DataTemplate 内の Delete ボタン/TapGestureRecognizer から ViewModel の DeleteEmployeeCommand/ViewEmployeeCommand を呼びたい
  • ビルド時に XamlC 警告 XC0045 が出る(例:Property “DeleteEmployeeCommand” not found on “EmployeeModel”)
  • 実行しても Command がトリガーされない

一方で、DataTemplate の x:DataType を外すとコマンドだけは動くものの、

  • CommandParameter が null になったり
  • 表示が崩れたり(バインディングのミスが潜って気付きにくい)

という状況になり、「コンパイル時バインディングは維持したいのに、Command だけが邪魔をする」状態に陥りがちです。

まず押さえるべき前提:x:DataType は“実行時の BindingContext を変えない”

ここが理解の肝です。

項目何が起きる?今回のトラブルとの関係
x:DataType主に ビルド時に「この Binding はこの型に対して正しいか?」をチェックし、最適化するDataTemplate で EmployeeModel に固定されると、ViewModel の Command が“存在しない”と判断されやすい
BindingContext主に 実行時に「Binding の解決先になるオブジェクト」DataTemplate の行は通常、ItemsSource の 1 要素(EmployeeModel)になる
XamlCXAML コンパイル(警告やエラーを出す)型がズレると XC0045 が出て、結果的に期待どおり動かない/気付きにくい不具合になる

DataTemplate で x:DataType を EmployeeModel にすると、そのテンプレート内のバインディングは「EmployeeModel に対するもの」として型チェックされる、というのが基本の挙動です。

原因:DataTemplate の x:DataType により“テンプレート内の型が EmployeeModel として固定”される

DataTemplate は「1 行=1 アイテム」の世界です。したがって、

  • 氏名や部署名などの表示 → EmployeeModel(行データ)から取る
  • 削除・詳細表示などの操作 → 親 ViewModel(画面のロジック)が持つ Command を呼ぶ

という 型が混在しやすい構造になります。

しかし DataTemplate に x:DataType を付けると、テンプレート内のバインディングは基本的に EmployeeModel 型として扱われます。その状態で次のように書くと、

<Button Text="Delete"
        Command="{Binding DeleteEmployeeCommand}"
        CommandParameter="{Binding .}" />

XamlC は「EmployeeModel に DeleteEmployeeCommand は存在しない」と判断し、XC0045 を出します。これが “Command が発火しない” 状態の根本原因です。

解決策:Command の Binding だけ “ViewModel 型” を明示して XamlC に正しい型を教える

結論はシンプルです。

表示用の Binding は EmployeeModel(行データ)のまま型安全に保ち、Command 参照だけは「親 ViewModel を参照する」ことを明示します。

今回のポイントは次の 2 つです。

  • Command の Source を、親の ViewModel(または親要素の BindingContext)へ向ける
  • その Command バインディングに対して x:DataType=ViewModel を付け、XamlC の型推論を正す

最小修正版(回答で提示された形)

Delete ボタンと、行タップ(TapGestureRecognizer)で ViewModel の Command を呼ぶ例です。DataTemplate の x:DataType は EmployeeModel のまま維持します。

<Button Text="Delete"
        Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:EmployeeViewModel}},
                         Path=DeleteEmployeeCommand,
                         x:DataType=viewmodels:EmployeeViewModel}"
        CommandParameter="{Binding .}" />

<TapGestureRecognizer
        Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:EmployeeViewModel}},
                         Path=ViewEmployeeCommand,
                         x:DataType=viewmodels:EmployeeViewModel}"
        CommandParameter="{Binding .}" />

これで、

  • DataTemplate 内の表示は EmployeeModel のコンパイル時バインディングを維持
  • Command だけは「親の ViewModel の Command」として解決
  • XC0045 を潰し、Command が正しく発火

という状態を作れます。

より堅牢にするなら:Mode=FindAncestorBindingContext を明示する

プロジェクトや構成によっては、RelativeSource の探索意図をより明確にした方が読みやすく、将来の保守もしやすくなります。たとえば「親要素の BindingContext(ViewModel)を探す」ことをはっきり書く形です。

<Button Text="Delete"
        Command="{Binding Source={RelativeSource Mode=FindAncestorBindingContext,
                                          AncestorType={x:Type viewModels:EmployeeViewModel}},
                         Path=DeleteEmployeeCommand,
                         x:DataType=viewmodels:EmployeeViewModel}"
        CommandParameter="{Binding .}" />

どちらでも動作は同じ方向性になりますが、チーム開発では「何を探しているか」が読み取れる書き方が有利です。

実装全体像:CollectionView + DataTemplate(EmployeeModel)と、操作 Command(EmployeeViewModel)を両立する

ここでは “現場でそのまま使える” 形に寄せたサンプル構成を提示します。

XAML 例(表示は EmployeeModel、操作は ViewModel)

<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:models="clr-namespace:YourApp.Models"
    xmlns:viewModels="clr-namespace:YourApp.ViewModels"
    x:Class="YourApp.Pages.EmployeePage">


<CollectionView ItemsSource="{Binding Employees}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:EmployeeModel">

            <Grid Padding="12" ColumnDefinitions="*,Auto">

                <!-- 行データ(EmployeeModel)を表示:型安全のまま -->
                <StackLayout Spacing="2">
                    <Label Text="{Binding FullName}" FontAttributes="Bold" />
                    <Label Text="{Binding Department}" FontSize="12" />
                </StackLayout>

                <!-- 行操作(ViewModel の Command を呼ぶ) -->
                <Button Grid.Column="1"
                        Text="Delete"
                        Command="{Binding Source={RelativeSource Mode=FindAncestorBindingContext,
                                                          AncestorType={x:Type viewModels:EmployeeViewModel}},
                                         Path=DeleteEmployeeCommand,
                                         x:DataType=viewModels:EmployeeViewModel}"
                        CommandParameter="{Binding .}" />

                <!-- 行タップ:詳細表示 -->
                <Grid.GestureRecognizers>
                    <TapGestureRecognizer
                        Command="{Binding Source={RelativeSource Mode=FindAncestorBindingContext,
                                                          AncestorType={x:Type viewModels:EmployeeViewModel}},
                                         Path=ViewEmployeeCommand,
                                         x:DataType=viewModels:EmployeeViewModel}"
                        CommandParameter="{Binding .}" />
                </Grid.GestureRecognizers>

            </Grid>

        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>


ポイントは次のとおりです。

  • DataTemplate は x:DataType=”EmployeeModel” のまま(表示の型安全を守る)
  • Command だけは Source を親 ViewModel へ向ける
  • Command の Binding だけに x:DataType=”EmployeeViewModel” を付与(XC0045 を回避)
  • CommandParameter は {Binding .}(行の EmployeeModel 自体)を渡す

ViewModel 側(CommunityToolkit.MVVM の RelayCommand 例)

Command は CommunityToolkit.MVVM(CommunityToolkit.Mvvm)で生成するケースが多いので、よくある形を載せます。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

namespace YourApp.ViewModels;

public partial class EmployeeViewModel : ObservableObject
{
    public ObservableCollection<EmployeeModel> Employees { get; } = new();

    [RelayCommand]
    private void DeleteEmployee(EmployeeModel employee)
    {
        if (employee is null) return;

        // 例:一覧から削除(実際はDB/API連携や確認ダイアログを挟むことが多い)
        Employees.Remove(employee);
    }

    [RelayCommand]
    private async Task ViewEmployeeAsync(EmployeeModel employee)
    {
        if (employee is null) return;

        // 例:詳細画面へ遷移
        // await Shell.Current.GoToAsync($"employee/detail?id={employee.Id}");
        await Task.CompletedTask;
    }
}

ここで重要なのは、XAML から参照するコマンド名です。

  • [RelayCommand] private void DeleteEmployee(...) → DeleteEmployeeCommand
  • [RelayCommand] private Task ViewEmployeeAsync(...) → ViewEmployeeCommand(Async が付いていても Command 名は ViewEmployeeCommand になることが多い)

「XAML で参照している Command 名」と「生成される Command プロパティ名」がズレると、別の意味で動きません。XC0045 の原因が型だけなのか、Command 名もズレているのかは、早い段階で切り分けるのが安全です。

Model 側(EmployeeModel の例)

namespace YourApp.Models;

public class EmployeeModel
{
public int Id { get; set; }
public string FullName { get; set; } = "";
public string Department { get; set; } = "";
}

なぜ「x:DataType を外すと動く」のに、別の問題(null や表示崩れ)が起きやすいのか

x:DataType を外すと、テンプレート内のバインディングは 実行時のリフレクション解決に寄ります。結果として、

  • Command のように本来 ViewModel を参照したいバインディングが「たまたま」動くことがある
  • 一方で、表示系バインディングのミス(プロパティ名の誤字、型の不一致)がコンパイル時に検出されず、実行時に崩れる
  • CommandParameter も同様に、Binding の意図が曖昧だと null になって気付きにくい

という状態になりがちです。

つまり、今回の目的(コンパイル時バインディングを維持して品質を上げたい)に照らすと、x:DataType を外すのは “その場しのぎ” になりやすく、長期運用でコストが跳ねます。

CommandParameter を null にしない実戦テクニック

DataTemplate 内で「行のデータ(EmployeeModel)」を渡したい場合、基本は次のどれかで OK です。

書き方意味おすすめ度
CommandParameter="{Binding .}"現在の BindingContext(行の EmployeeModel)そのもの高(意図が明確)
CommandParameter="{Binding}"上とほぼ同義(書き味の好み)中
CommandParameter="{Binding Id}"特定プロパティだけ渡す中(受け側の型設計とセットで)

「コマンドは ViewModel、パラメータは EmployeeModel」という “混在” を成立させるのが今回の狙いなので、Command と CommandParameter を同じ Source に寄せない(寄せる必要がある場合は明示する)ことが大切です。

よくある失敗とチェックポイント(XC0045 の再発防止)

同じ現象に見えても、原因が複合していることがあります。下のチェックで早めに切り分けるとハマりづらいです。

症状ありがちな原因対処
XC0045 が出るDataTemplate の x:DataType が行データ型のため、Command が見つからないCommand Binding に x:DataType=ViewModel を付け、Source を親 ViewModel に向ける
警告は消えたが Command が発火しないRelativeSource が想定の祖先を見つけられていない(画面構造が違う)Mode=FindAncestorBindingContext を明示し、AncestorType が ViewModel に合っているか確認
CommandParameter が nullCommandParameter の Binding が壊れている/BindingContext が想定と違う{Binding .} を基本にし、DataTemplate の x:DataType を維持したまま検証
Command 名が見つからない[RelayCommand] の生成名と XAML の参照名がズレている生成される 〜Command の名前を再確認(Async の有無も含めて)
TapGestureRecognizer だけ動かないGestureRecognizers の付け先が意図した要素ではない/上に別要素が被っているタップを受けたい要素(Grid など)に設定し、レイアウトの重なりも確認

別解:プロジェクト事情で RelativeSource が使いづらいときの代替パターン

チームの XAML 規約や画面構造によっては、RelativeSource の祖先探索が読みにくい・壊れやすいケースもあります。そんなときの代替案をいくつか紹介します(どれも “表示は EmployeeModel、操作は ViewModel” を守るための手段です)。

x:Reference でページ(またはルート要素)を参照する

ページやルートレイアウトに x:Name を付けて、BindingContext 経由で ViewModel の Command を引く方法です。祖先探索より「どこを参照しているか」が明確になりやすいメリットがあります。

<ContentPage x:Name="RootPage" ...>

    <CollectionView ItemsSource="{Binding Employees}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="models:EmployeeModel">
                <Button Text="Delete"
                        Command="{Binding Source={x:Reference RootPage},
                                         Path=BindingContext.DeleteEmployeeCommand,
                                         x:DataType=viewModels:EmployeeViewModel}"
                        CommandParameter="{Binding .}" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

</ContentPage>

注意点としては、ページが入れ子になったり、テンプレート化された画面構造(Shell や独自レイアウト)で x:Reference の参照先が変わる場合があるので、画面設計に合わせて採用します。

“Command だけ” コンパイル時バインディングを無効化する(最終手段)

どうしても型指定が難しい場合、Command のところだけコンパイル時バインディングを外す選択肢もあります。ただし、型安全と警告検出の恩恵が減るので、最終手段として考えるのが無難です。

  • 表示や重要な Binding は x:DataType を維持
  • どうしても混在が解けない箇所だけ、局所的に “動けばOK” にする

今回のケースでは、x:DataType を ViewModel に明示できるなら、そちらを優先する方がトラブルが減ります。

設計のコツ:DataTemplate は「表示(Model)」と「操作(ViewModel)」が混ざる前提で設計する

今回の問題は、.NET MAUI に限らず “リスト行の MVVM” で頻出です。長期的に破綻しないための設計上のコツをまとめます。

コマンドは「行アイテムを引数に取る」形に寄せる

DataTemplate 内からは、行アイテム(EmployeeModel)が自然に取れます。なので、ViewModel のコマンドは次の設計が扱いやすいです。

  • DeleteEmployee(EmployeeModel employee) のように、行アイテムを受け取る
  • コマンド内部で ID を使う/API 呼び出しに渡す/選択状態を変える

これにより、XAML 側は CommandParameter="{Binding .}" で済み、バインディング事故が減ります。

表示用 Binding と Command 用 Binding の責務を分ける

  • 表示(Label.Text など)は DataTemplate の x:DataType(EmployeeModel)で固める
  • 操作(Button.Command など)は Source を親 ViewModel に寄せ、必要なら x:DataType を上書く

この分離ができると、XC0045 のような「型の食い違い」警告に悩まされにくくなります。

まとめ:XC0045 と Command 不発は“型の混在”が原因。Command だけ ViewModel 型を明示して解決する

  • DataTemplate に x:DataType=EmployeeModel を付けると、テンプレート内は基本 EmployeeModel として型チェックされる
  • そのまま ViewModel の Command を参照すると、XC0045(プロパティが見つからない)になりやすい
  • Command の Binding だけ、Source を親 ViewModel に向け、さらに x:DataType=ViewModel を明示すると、型安全を維持したまま動く
  • CommandParameter は {Binding .} を基本にして、行アイテムを確実に渡す

コンパイル時バインディング(x:DataType)は、開発体験と品質を大きく引き上げます。だからこそ「DataTemplate 内で型が混ざる場所(Command など)をどう扱うか」を押さえておくと、.NET MAUI + CommunityToolkit.MVVM の開発が一気に安定します。

この記事を書いた人

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

コメント

コメントする

目次