.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を参照する
<ContentView ...>
<VerticalStackLayout>
<Label Text="ContentView内のLabel"
FontSize="{Binding FontSizeValue}" />
</VerticalStackLayout>
</ContentView>
これで、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を参照する方法が使えます。
<Label Text="親ページのVMを参照したい"
FontSize="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
Path=BindingContext.FontSizeValue}" />
この書き方は「名前スコープの外にある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へ渡す
<local:MyTraceCalculations FontSizeValue="{Binding FontSizeValue}" />
これで、ContentViewは「親が何のViewModelを使っているか」を知らなくても、FontSizeValueだけ受け取って描画できます。UI部品としての独立性が上がるため、画面が増えたときの保守性が良くなります。
実装の具体例:MainPage+ContentView+別ページでフォントサイズを完全共有
ここまでの内容を、最小構成でまとめると次のようになります。
| 役割 | 持つべきもの | バインド |
|---|---|---|
| SharedFontViewModel | FontSizeValue(状態) | 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(または同じプロパティ)を見るように設計すると、コンパイルエラーを避けつつ拡張にも強い構成になります。

コメント