.NET MAUIのx:Referenceが別ページ・ContentViewで使えない理由とMVVMでの解決策

.NET MAUIでStepper/Sliderの値を使ってLabelやEditorのフォントサイズを連動させるとき、x:Referenceは同一XAMLでは便利です。しかし別ContentViewや別ページへ広げようとすると「名前が見つからない」コンパイルエラーや、BindingContext設定の迷子に陥りやすくなります。原因と、MVVM(共有ViewModel)で安全に値を共有する設計を具体例で整理します。

目次

同じページでは動くのに、別ContentView/別ページで壊れる現象

たとえば、同一ページ内にStepperとLabelがあり、Stepperの値でLabelのフォントサイズを変えたい場合、次のように書くと直感的に動きます。

<VerticalStackLayout>
  <Stepper x:Name="fontStepper"
           Minimum="10"
           Maximum="40"
           Increment="1" />


 

ところが、この仕組みを次のように「別のContentViewに分割したい」「別ページでも同じ値を使いたい」と思った途端に問題が表面化します。

  • MainPageに置いた fontStepper を、埋め込んだContentView側から x:Reference で参照したい
  • PageAのStepperで設定したフォントサイズを、PageBのLabelにも反映したい

このとき多くの場合、XAMLコンパイル時(XamlC)に「参照先の要素が見つからない」系のエラーになります。さらに、ContentViewを <local:MyTraceCalculations /> のようにXAMLから生成している構成で、ViewModelを引数付きコンストラクタで渡そうとすると「引数なしコンストラクタが必要」「オブジェクトとして使えない」系のエラーが出ます。

結論:x:Reference は “同じXAML名前スコープ” の要素しか参照できない

ポイントは「XAML名前スコープ(XAML NameScope)」です。 x:Name で付けた名前は、どこからでもグローバルに見えるわけではなく、一定の境界の中だけで有効になります。

.NET MAUIでは、一般的に次のような単位で名前スコープが分かれます。

  • Page(例:MainPage.xaml)
  • ContentView(例:MyTraceCalculations.xaml)
  • ControlTemplate / DataTemplate(テンプレート内はさらに別スコープになりやすい)

x:Reference は「同じ名前スコープ内で、x:Name が付いた要素を解決する」ための仕組みです。つまり、別ファイルのXAML(別クラス)にある fontStepper を、別XAMLから直接参照することはできません。 ここが「同じページならOK、別ContentView/別ページだとNG」になる根本原因です。

なお、ElementName=... バインディングも考え方はほぼ同じで、名前スコープの境界をまたぐと解決できません。UI要素同士を直接参照する発想自体が、画面が大きくなるほど苦しくなります。

やりたい参照例x:Referenceの可否理由
同一XAML内で参照MainPage.xamlのLabelがMainPage.xamlのStepperを参照可能同じ名前スコープ
別ContentViewから参照MyTraceCalculations.xamlのLabelがMainPage.xamlのfontStepperを参照不可ContentViewは別XAML・別名前スコープ
別ページから参照PageB.xamlのLabelがPageA.xamlのfontStepperを参照不可ページは別クラス・別名前スコープ
テンプレート内から外を参照DataTemplate内の要素が外のx:Nameを参照状況により不可テンプレートは独立したスコープになりやすい

なぜ「UI同士を参照」する設計がスケールしないのか

同じページ内で完結する小さなUIなら x:Reference は手軽です。一方、ページをまたいだり、部品化(ContentView化)したりすると、次の問題が一気に表面化します。

  • 参照の境界(名前スコープ)に引っかかる
  • UIの寿命と状態の寿命が一致しなくなる(ページ遷移、再生成、表示/非表示など)
  • テストしにくい(UI要素が前提になる)
  • 部品化したContentViewが「特定のページに依存する部品」になって再利用できない

フォントサイズのような「画面状態(State)」は、Stepperそのものではなく、ViewModelのプロパティとして持たせた方が長期的に安全です。そこで次が正攻法になります。

正攻法:共有ViewModelのプロパティで値を共有する(MVVM)

目標はシンプルです。

  • フォントサイズという「状態」を FontSizeValue というプロパティで表す
  • Stepper/Sliderはその状態を変更するUI(入力)
  • Label/Editorはその状態を表示に反映するUI(出力)
  • ページやContentViewが違っても、同じViewModelインスタンスを見ていれば同期する

ViewModel例:INotifyPropertyChangedでFontSizeValueを公開

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class SharedFontViewModel : INotifyPropertyChanged
{
double _fontSizeValue = 16;


public double FontSizeValue
{
    get => _fontSizeValue;
    set
    {
        if (_fontSizeValue == value) return;
        _fontSizeValue = value;
        OnPropertyChanged();
    }
}

public event PropertyChangedEventHandler? PropertyChanged;

void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));


} 

ボイラープレートを減らしたい場合は、CommunityToolkit.Mvvm(ObservableObject / [ObservableProperty])を使う方法もあります。ただし依存を増やしたくないなら、上のような素の INotifyPropertyChanged でも十分です。

入力側(Stepper/Slider):ValueをViewModelへTwoWayバインド

入力側は「UI → ViewModel」に値が流れる必要があるので、TwoWay(双方向)を明示しておくと安心です。

<Stepper Minimum="10"
         Maximum="40"
         Increment="1"
         Value="{Binding FontSizeValue, Mode=TwoWay}" />

 

StepperとSliderを同時に置いても、同じプロパティにTwoWayで繋がっているため「どちらを動かしても同じ値」に揃います。

出力側(Label/Editor):FontSizeを同じプロパティにバインド

<Label Text="フォントサイズが連動します"
       FontSize="{Binding FontSizeValue}" />

 

複数ContentViewでも同期する:親ページのBindingContextをそのまま使う

ContentViewを部品化するときに大切なのは、ContentView側で勝手にBindingContextを差し替えないことです。親ページがViewModelをBindingContextに設定していれば、ContentViewはそれをそのまま利用できます。

MainPage:BindingContextにSharedFontViewModelを設定する

public partial class MainPage : ContentPage
{
    public MainPage(SharedFontViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm; // ここが基準
    }
}

MainPage.xaml:ContentViewを並べても同じプロパティにバインドできる

<ContentPage ...>
  <VerticalStackLayout Spacing="16">


<Stepper Minimum="10"
         Maximum="40"
         Value="{Binding FontSizeValue, Mode=TwoWay}" />

<local:MyTraceCalculations />
<local:MyAnotherContentView />

<Label Text="ページ直下のLabel"
       FontSize="{Binding FontSizeValue}" />



 

MyTraceCalculations.xaml:x:Referenceは使わず、ViewModelを参照する

&lt;ContentView ...&gt;
  &lt;VerticalStackLayout&gt;
    &lt;Label Text="ContentView内のLabel"
           FontSize="{Binding FontSizeValue}" /&gt;
  &lt;/VerticalStackLayout&gt;
&lt;/ContentView&gt;

これで、MainPage内のStepperを動かすと、ページ直下のLabelだけでなく、ContentView内のLabelも同時に更新されます。x:Referenceに依存していないため、名前スコープの壁に当たりません。

別ページでも同期したい:同じViewModelインスタンスをDIで共有する

「ページをまたいで同じフォントサイズを共有したい」場合の肝は、ページごとに別インスタンスのViewModelを作らないことです。代表的な方法がDI(依存性注入)でのSingleton登録です。

MauiProgram.cs:SharedFontViewModelをSingleton登録

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();


    // 省略:UseMauiAppなど

    builder.Services.AddSingleton<SharedFontViewModel>();

    // ページは必要に応じてTransient(都度生成)にすることが多い
    builder.Services.AddTransient<MainPage>();
    builder.Services.AddTransient<SecondPage>();

    return builder.Build();
}


} 

SecondPage:同じSharedFontViewModelが注入される

public partial class SecondPage : ContentPage
{
    public SecondPage(SharedFontViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm; // MainPageと同じインスタンス
    }
}

この構成にすると、MainPageでFontSizeValueを変えるとSecondPageでも同じ値が使われます(ページ遷移後も状態が残る)。逆に、ページごとに new SharedFontViewModel() してしまうと、当然ながら別物になり同期しません。

ContentViewをXAMLで生成するなら「引数なしコンストラクタ」が基本

<local:MyTraceCalculations /> のようにXAMLに書いたContentViewは、XAMLパーサーが内部でインスタンス化します。ここで多くの場合、引数なしコンストラクタ(デフォルトコンストラクタ)が必要です。引数付きコンストラクタだけを用意すると、生成できずにエラーになります。

やりたいことよくある発想現実的な解法
ContentViewにViewModelを渡したいContentViewのコンストラクタで受け取る親ページのBindingContextを継承する(基本)
ContentViewを部品として再利用したい内部で勝手にBindingContextを設定するBindablePropertyで入力を定義し、外からバインドする
ContentView独自のVMが欲しい引数付きコンストラクタで注入ページ側でVMを解決し、BindingContextを明示設定(コード側で生成する等)

BindingContextの落とし穴:BindingContext = this; を最後にやると全部ずれる

MVVM化の途中で特に多い詰まりどころが、MainPage側のBindingContext設定です。たとえば次のように書くと、意図と違う状態になりやすいです。

よくあるNG例

public partial class MainPage : ContentPage
{
    public SharedFontViewModel SharedViewModel { get; } = new();


public MainPage()
{
    InitializeComponent();

    // いったんVMを作ったのに…
    // BindingContextをページ自身にしてしまう
    BindingContext = this;
}


} 

これだとXAMLの {Binding FontSizeValue} は「ページ(this)」を見に行きます。ページに FontSizeValue が無ければバインドできず、結果としてUIが更新されません。ページ自身をBindingContextにしたい場合は、XAML側で {Binding SharedViewModel.FontSizeValue} のように長く書く必要が出てきます。

推奨例:素直にViewModelをBindingContextにする

public partial class MainPage : ContentPage
{
    public MainPage(SharedFontViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm;
    }
}

これが最も事故が少ない形です。ページコードビハインドにイベント処理が残る場合でも、データと状態はViewModelに寄せ、UIイベントはCommand化していくと綺麗に収まります。

それでもBindingContextを分けたい場合:RelativeSourceで親のBindingContextへ戻る

設計上、ContentView側に独自のBindingContext(独自VM)を持たせたいケースもあります。その場合、ContentView内の一部だけ「親ページのVM」を参照したい、という要望が出がちです。

このときは、親要素(ContentPageなど)をAncestorTypeで辿って、そのBindingContextを参照する方法が使えます。

&lt;Label Text="親ページのVMを参照したい"
       FontSize="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
                          Path=BindingContext.FontSizeValue}" /&gt;

この書き方は「名前スコープの外にあるx:Nameを参照」しているわけではなく、「視覚ツリー上の祖先を辿ってBindingContextを取る」ため、x:Reference の制約とは別物です。ただし、レイアウト構造に依存しやすいので、乱用するよりは「共有状態は共有ViewModelへ寄せる」方が安定します。

ContentViewを“部品”として保ちたいなら:BindablePropertyで入力/出力を定義する

「ContentViewを、どのページでも再利用できる部品にしたい」「親ページのViewModelの形に依存させたくない」場合、ContentViewにBindablePropertyを用意して、親から値を渡すのが定石です。

ContentView側:FontSizeを受け取るBindableProperty

public partial class MyTraceCalculations : ContentView
{
    public static readonly BindableProperty FontSizeValueProperty =
        BindableProperty.Create(
            nameof(FontSizeValue),
            typeof(double),
            typeof(MyTraceCalculations),
            16d);


public double FontSizeValue
{
    get => (double)GetValue(FontSizeValueProperty);
    set => SetValue(FontSizeValueProperty, value);
}

public MyTraceCalculations()
{
    InitializeComponent();
}


} 

ContentViewのXAML:BindablePropertyにバインド

<ContentView ... x:Name="self">
  <VerticalStackLayout>
    <Label Text="部品側の表示"
           FontSize="{Binding Source={x:Reference self}, Path=FontSizeValue}" />


<!-- ほかの項目は通常どおりBindingContext(親VM)を見に行ける -->
<Label Text="{Binding SomeOtherText}" />



 

この場合の x:Reference は「部品の中で自分自身(self)を参照」しているだけなので、名前スコープ問題を起こしません。親ページから見たら、単にプロパティを渡す部品として扱えます。

親ページ側:ViewModelの値をContentViewのBindablePropertyへ渡す

&lt;local:MyTraceCalculations FontSizeValue="{Binding FontSizeValue}" /&gt;

これで、ContentViewは「親が何のViewModelを使っているか」を知らなくても、FontSizeValueだけ受け取って描画できます。UI部品としての独立性が上がるため、画面が増えたときの保守性が良くなります。

実装の具体例:MainPage+ContentView+別ページでフォントサイズを完全共有

ここまでの内容を、最小構成でまとめると次のようになります。

役割持つべきものバインド
SharedFontViewModelFontSizeValue(状態)INotifyPropertyChangedで通知
MainPageフォントサイズを変更するUI(Stepper/Slider)Value ↔ FontSizeValue(TwoWay)
ContentViewフォントサイズを使うUI(Label/Editor等)FontSize ← FontSizeValue
SecondPage別ページの表示同じVMをBindingContextにする

「状態はViewModel」「UIはバインド」「共有したければ同じViewModelインスタンス」を守るだけで、x:Reference をページ外へ持ち出す必要がなくなり、コンパイルエラーも設計のねじれも一気に解消します。

コンパイル時にバインドミスを減らす:x:DataTypeの活用

MVVMへ寄せたら、次に効いてくるのが x:DataType(コンパイル時バインディング)です。指定しておくと、プロパティ名のタイプミスを早期に検出しやすくなり、リファクタリング耐性も上がります。

<ContentPage ...
             xmlns:vm="clr-namespace:YourApp.ViewModels"
             x:DataType="vm:SharedFontViewModel">



 

ContentViewでも同様に x:DataType を付けられます(ただし、ContentViewが親のBindingContextを継承する前提で型を合わせておくのがコツです)。

トラブルシュート:MVVMにしたのに反映されないときのチェックリスト

MVVMへ寄せたあとも、次のどこかで詰まることがあります。原因の切り分けに使ってください。

BindingContextが想定どおりに流れているか

  • ページのコンストラクタで BindingContext = vm; しているか
  • ContentView側で BindingContext = this; などをして上書きしていないか
  • DataTemplateやCollectionView内なら、BindingContextが「アイテム」になっている(親VMが見えない)可能性がある

プロパティ変更通知が飛んでいるか

  • FontSizeValue のsetterで値が変わったときに PropertyChanged が呼ばれているか
  • 比較条件が厳しすぎて通知が止まっていないか(doubleの等価比較で弾いている等)

バインドモードが適切か

  • Stepper/Sliderは TwoWay にしているか(既定でTwoWayのこともありますが、明示すると迷いが減る)
  • 表示側はOneWayで十分(FontSize="{Binding FontSizeValue}")

値の型や範囲が適切か

  • FontSizeはdoubleで扱われるため、整数に揃えたい場合はStepperのIncrementやConverterを検討する
  • Minimum/Maximumが現実的な範囲になっているか(極端に小さい/大きい値だと見た目が崩れる)

まとめ:x:Referenceは“同一XAML内の小技”、共有状態はMVVMへ

x:Reference は、同一XAML内で「あるコントロールの値を別コントロールが参照する」用途には便利です。しかし、ContentView化やページ分割をした瞬間に、XAML名前スコープの壁で破綻します。

ページ/ContentViewをまたいで値(フォントサイズなど)を共有したいなら、UI要素を参照するのではなく、ViewModelのプロパティを共有するのが最短ルートです。DIで同じViewModelインスタンスを配り、各ページとContentViewが同じBindingContext(または同じプロパティ)を見るように設計すると、コンパイルエラーを避けつつ拡張にも強い構成になります。

この記事を書いた人

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

コメント

コメントする

目次