.NET MAUI DataTemplateのx:DataTypeにTupleを指定できない理由と解決策(Compiled Binding対応)

.NET MAUI の CollectionView や BindableLayout の DataTemplate で x:DataType を指定すると、Compiled Binding(コンパイル時バインディング)になり、パフォーマンスと保守性が一気に上がります。ところが ItemsSource を Tuple や ValueTuple にすると x:DataType が書けず、警告(XC0022 など)と格闘することになりがちです。この記事では「なぜ詰まるのか」と「実務で一番ラクに解決する方法」を具体例つきで整理します。

目次

Compiled Binding を使うと何が嬉しいのか

.NET MAUI の XAML バインディングは、通常は実行時にパスを解決します(いわゆる classic binding / reflection-based binding)。一方で Compiled Binding は、ビルド時(XAML コンパイル時)にバインディングを解析してコード生成するため、次のメリットがあります。

  • 実行時のオーバーヘッドが減る(リスト表示のように大量のバインディングがある場面ほど効きやすい)
  • プロパティ名のタイプミスをビルド時に検出できる(「動かして初めて気づく」を減らせる)
  • IntelliSense が効きやすい(XAML 側で補完が効き、開発体験が改善する)

この Compiled Binding を使う鍵が x:DataType です。Microsoft Learn の公式ドキュメントでも、Compiled Binding を使うには x:DataType を指定して型を明示することが前提、と説明されています。

DataTemplate では x:DataType を「テンプレート側」に書く必要がある

DataTemplate の内部は、ページ全体の BindingContext ではなく「行(アイテム)そのもの」をコンテキストとして評価されます。そのため、ページのルートに x:DataType を書いていても、DataTemplate に正しい型を書かないと「外側の型で解釈される」などの問題が起きます。公式ドキュメントでも、DataTemplate には正しい x:DataType を付けるべきと明記されています。

問題:DataTemplate の x:DataType を Tuple にしたいが、書けない

例えば、こんなデータを CollectionView に出したい状況を想定します。

やりたいこと(Tuple で 1 行ぶんのデータを持つ)

public class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

// 例:Id, Message, Person をまとめて返したい
public ObservableCollection> Items { get; } = new()
{
new Tuple(1, "Hello", new Person { Name = "Alice", Age = 30 }),
new Tuple(2, "World", new Person { Name = "Bob", Age = 25 }),
};

そして XAML 側で、Compiled Binding を効かせるために DataTemplate へ x:DataType を付けたい。

詰まりポイント:Tuple を「型名」として x:DataType に書けない

結論から言うと、Tuple 型(例:Tuple<int, string, Person>)は XAML 上で “名前付き型(named type)” として扱えず、DataTemplate の x:DataType に指定できません。Microsoft Q&A でも、Tuple<int, string, Person> は XAML の named type として指定できないため、Compiled Binding を使うなら強い型のラッパークラスを作るべき、という回答が採択されています。

この時点で「じゃあどう書けばいい?」の答えは、残念ながら “Tuple をそのまま x:DataType に指定する書き方” は用意されていない、になります。

なぜダメなのか:x:DataType が要求するのは “XAML で解決できる型名”

x:DataType は「C# の型表記をそのまま解釈する」仕組みではなく、XAML の型解決ルールに従って型を見つけます。したがって、次のような点で Tuple は引っかかりやすいです。

ポイント何が起きるか実務上の影響
Tuple は “1行ぶんのデータ構造” として便利だが、XAML の型名指定と相性が悪いTuple<...> のような C# の型表記を x:DataType に持ち込めないDataTemplate の Compiled Binding を諦める or 型を変える必要が出る
x:DataType を付けないと XC0022 が出ることがある「この Binding はコンパイル可能だが、x:DataType が無い」旨の警告警告をエラー扱いにしているプロジェクトではビルドが止まる

なお、.NET MAUI の x:DataType 自体は、特定の書式でジェネリック型も指定できます(括弧で型引数を与える記法)。ただし、Tuple をそのまま “素直に” 使うより、次に紹介する 表示用の型を作る設計 のほうが結果的に堅牢で、チーム開発でも揉めません。

解決策:Tuple をやめて “強い型” のラッパークラス(または record)を作る

ここからが本題です。UI に渡す 1 行ぶんのデータは、Tuple で雑にまとめるのではなく「表示用 DTO(View 用モデル)」として名前付きの型を作るのが定石です。

おすすめの型:record / class どちらでもOK

表示専用で「生成後に値が変わらない」なら record が相性良いです。値が変わり得る(更新される)なら class + INotifyPropertyChanged(CommunityToolkit.Mvvm の ObservableObject 等)を検討すると安全です。

例:record(表示用 DTO)

public record PersonRecord(int Id, string Message, Person Person);

例:class(必要なら変更通知を載せやすい)

public class PersonRecord
{
    public int Id { get; set; }
    public string Message { get; set; } = "";
    public Person Person { get; set; } = new();
}

この「名前付き型」を作るのがポイントで、XAML 側では x:DataType="local:PersonRecord" のように指定できるようになります。Microsoft Q&A の採択回答でも、この方針(ラッパークラスを作る)が解決策として示されています。

ViewModel 側:ObservableCollection<PersonRecord> にする

public class MainViewModel
{
    public ObservableCollection<PersonRecord> Items { get; } = new()
    {
        new PersonRecord(1, "Hello", new Person { Name = "Alice", Age = 30 }),
        new PersonRecord(2, "World", new Person { Name = "Bob", Age = 25 }),
    };
}

XAML 側:DataTemplate に “名前付き型” を指定する

ポイントは 2 つです。

  • ページ全体の x:DataType は ViewModel にする(ページ内の一般バインディングがコンパイルされる)
  • DataTemplate の x:DataType は行アイテムの型(ここでは PersonRecord)にする
<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:YourApp"
    x:Class="YourApp.MainPage"
    x:DataType="local:MainViewModel">

    <ContentPage.BindingContext>
        <local:MainViewModel />
    </ContentPage.BindingContext>

    <CollectionView ItemsSource="{Binding Items}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="local:PersonRecord">
                <HorizontalStackLayout Spacing="12">
                    <Label Text="{Binding Id}" />
                    <Label Text="{Binding Message}" />
                    <Label Text="{Binding Person.Name}" />
                    <Label Text="{Binding Person.Age}" />
                </HorizontalStackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

</ContentPage>

これで {Binding Id} / {Binding Message} / {Binding Person.Name} のようなバインディングが Compiled Binding になり、XC0022 などの警告も根本的に回避できます。

ラッパー型を作ると何が改善するのか(Tuple と比較)

観点Tuple / ValueTuple表示用ラッパー型(record / class)
DataTemplate の x:DataType指定が難しく、Compiled Binding を成立させにくい型名をそのまま指定できる
XAML 側の可読性Item1 / Item2 … になりやすいId / Message / Person のように意図が明確
保守性(要素追加)要素順に依存しやすい(順番が変わると地獄)プロパティを追加しても影響範囲が読みやすい
チーム開発レビューで意味が伝わりにくい名前がドキュメントになる

補足:どうしても Tuple のままにしたい場合の「現実的」な選択肢

要件や既存コードの都合で、すぐに型を変えられないケースもあります。その場合の落としどころを整理します。

選択肢A:Compiled Binding を諦めて classic binding で運用する

もっとも単純です。DataTemplate から x:DataType を外し、実行時解決で回します。公式ドキュメントでも、x:DataType が指定された範囲だけがコンパイルされ、指定が無い範囲は classic binding になることが説明されています。

また、親階層に x:DataType があるせいでテンプレート内が誤解釈される場合は、テンプレート内で x:DataType を {x:Null} に再定義して「ここから先は classic binding」と明示する手もあります(ただし Compiled Binding の恩恵はその範囲で消えます)。

選択肢B:Tuple を “名前付き型” にしてしまう(継承・ラップ)

「Tuple を捨てるのは嫌だが XAML で型名として扱えるようにしたい」という場合、Tuple を継承したクラスを作って “名前付き型” にする方法もあります。実際に、Tuple をそのままバインドして Item1 が見つからない等の問題が出た際に、カスタム型を作って x:DataType に指定する回避策が紹介されています。

ただしこの方法は、結局「独自型を作る」点では同じなので、どうせ作るなら Id や Message のような意味のあるプロパティ名を持つラッパー型にするほうが、後々ラクです。

選択肢C:MultiBinding で合成…は要注意

「Tuple を作る代わりに XAML 側で合成すればいいのでは?」と考える人もいますが、Compiled Binding は現状 MultiBinding ではサポートされない旨がドキュメントに明記されています。リストで多用するなら、結局は表示用の型を作るほうが無難です。

ValueTuple((int, string, Person))でも同じようにハマる理由

ValueTuple は C# のタプル構文((int, string) や (double Sum, int Count) のような名前付き要素)で便利に扱えますが、基本的に要素アクセスは Item1 / Item2 … が土台です。C# のリファレンスでも、タプル要素は Item1 などでアクセスできることが示されています。

また、ValueTuple<T1,T2> 自体も Item1 / Item2 を持つことが API ドキュメントに明記されています。

そのため UI 側(XAML バインディング)で「id」「message」のような “名前付き要素” で直感的にバインドしたいという期待は外れやすく、やはり表示用の型を作るのが安定策になります。

よくある警告・エラー(XC0022 / XC0024 / XFC0045)と対処の考え方

Tuple まわりで起きがちな警告・エラーを、原因と対処の観点で整理します。

コード起きやすい状況意味(ざっくり)現実的な対処
XC0022x:DataType を付けずに Binding している「x:DataType を付ければコンパイルできる」テンプレート/スコープが変わる場所に x:DataType を付与する(付けられないなら classic binding にする)
XC0024DataTemplate が外側の x:DataType を誤って引き継いだ「テンプレートの外側の型で解釈されそう」DataTemplate に正しい x:DataType を明示(または {x:Null} で classic binding 化)
XFC0045Tuple に対して Item1 等をバインドしたが解決できない「そのプロパティが見つからない」表示用の名前付き型へ置換(または Tuple 継承型/ラッパーを作る)

XC0022 / XC0024 などの意味と修正方針は Microsoft Learn の compiled bindings ドキュメントにまとまっています。

また、Tuple バインディングで Item1 が見つからない等のトラブル例(XFC0045)や、回避として「カスタム型を用意する」方針はコミュニティでも共有されています。

実務で効く型設計のコツ:ドメインと「表示用 DTO」を分ける

Tuple を使いたくなる背景には、だいたい次のような事情があります。

  • API/DB から取得した値を一時的に束ねたい
  • 画面に出すために複数モデルを合成したい
  • ViewModel の中でサクッと LINQ で組み立てたい

このとき、Tuple で “その場しのぎ” をすると、XAML 側で型が立たず Compiled Binding を捨てる羽目になりがちです。なのでおすすめは次のパターンです。

層置くもの目的
ドメイン/モデル層Person など本質的なデータ構造アプリの意味を表す
ViewModel/表示用層PersonRecord のような “画面1行分” の DTOUI バインディングを安定させる(Compiled Binding を成立させる)

最初は「型を増やすのが面倒」に見えますが、DataTemplate を 1 つでも使う画面だと回収が早いです。後から列(表示項目)が増えたとき、Tuple の “順番依存” よりも、プロパティ追加のほうが圧倒的に安全だからです。

まとめ

  • DataTemplate の Compiled Binding を成立させるには、x:DataType に XAML で解決できる “名前付き型” を指定する必要がある。
  • Tuple をそのまま x:DataType に書いて Compiled Binding を効かせるのは難しいため、表示用のラッパー型(class / record)を作るのが最も実務的。
  • どうしても Tuple を残すなら、テンプレート部分だけ classic binding に割り切るか、Tuple を名前付き型にラップして妥協する。

「UI で扱う 1 行ぶんのデータは、Tuple ではなく “名前付きの DTO” にする」。このルールを徹底すると、.NET MAUI の x:DataType と DataTemplate 周りのトラブルはかなり減ります。

この記事を書いた人

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

コメント

コメントする

目次