WPF FrameでPage読み込み時にUserControlのボタンを表示する方法|Visibilityと依存プロパティの落とし穴

WPFのFrameにPageを表示していると、Pageの切り替えに合わせてMainWindow側のUserControl(メニューバー)のボタン表示を変えたくなることがあります。ところが新しくUserControlをnewすると別インスタンスになり、依存プロパティの扱いを誤ると反映されません。この記事では、Page読み込み時に「戻る/次へ」だけを確実に表示する実装手順と、つまずきやすいポイントを整理します。

目次

状況を整理:MainWindow+Frame+Page+UserControlで起きやすい「表示が変わらない」問題

今回の構成は、WPFではとても一般的です。たとえばMainWindowにFrame(例:PageFrame)を置き、そこへ複数のPage(例:PageCreate、PageOutput)を入れ替えて表示します。同時に、MainWindow上部に共通のUserControl(例:MenuBar)を配置し、ページ遷移に合わせてメニューやナビゲーションボタンの表示状態(Visibility)を切り替えたい、という要件です。

具体的には次のような要望になります。

  • MenuBar(UserControl)内に「戻る(btnBack)」「次へ(btnNext)」ボタンがある
  • アプリ起動直後は両方とも隠しておきたい(Visibility.Hidden など)
  • PageFrameにPageOutputが読み込まれたタイミングで、この2つだけを表示(Visibility.Visible)にしたい

ところが、ページ側のコードからボタンを表示しようとしても、次のような“ありがちな落とし穴”に引っかかると、画面は一切変化しません。

やりがちな操作なぜ反映されないのか正しい考え方
btnBackVisibilityProperty など依存プロパティ“そのもの”を書き換えようとする依存プロパティフィールドは「定義情報(メタデータ)」であり、インスタンスの値そのものではありません。さらに通常は static readonly で代入できません。インスタンスに対して、ラッパープロパティ(例:BtnBackVisibility)や SetValue で「値」を設定します。
new MenuBar() で新しいUserControlを作り、そこでボタンを表示にするそれは“画面に表示されているMenuBar”とは別インスタンスです。表示されているのはXAMLで配置した1個だけなので、別物をいじってもUIは変わりません。MainWindow上に存在するインスタンス(例:x:Name="uCtrl")を取得して操作します。
ElementNameバインディングを使っているのに、参照先に名前がないElementName=MenuBar のように指定しても、XAML上にその名前の要素が存在しないとバインディングは失敗します(出力ウィンドウにBindingエラーが出ます)。UserControlのルートに x:Name を付けるか、RelativeSource を使って確実に参照します。
Pageのコンストラクタで Window.GetWindow(this) を呼ぶコンストラクタ実行時点では、PageがまだVisualTreeに乗っていないことがあり、null が返るケースがあります。「読み込み完了」に近いタイミング(Loaded、Navigated)で切り替えます。

結論:やるべきことは3つだけ

解決のポイントはシンプルです。

  • UserControl内のバインディングを「確実に」通す(ElementNameの参照先を作る)
  • 依存プロパティは“フィールド”ではなく“インスタンスの値”を変える
  • ページから操作するなら、画面に表示されているMainWindow上のMenuBarインスタンスにアクセスする

UserControl側の修正:ルートに名前を付けてElementNameバインディングを成立させる

まずMenuBar(UserControl)側です。UserControl内で自分自身の依存プロパティ(例:btnBackVisibility / btnNextVisibility)にバインドしたい場合、ElementNameを使うなら参照先となる名前が必要です。ルートの<UserControl>にx:Nameを付け、その名前をElementNameとして指定します。

&lt;UserControl x:Class=&quot;Scores.Views.UserControls.MenuBar&quot;
             x:Name=&quot;menuBar&quot;
             xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
             xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;&gt;
    &lt;Grid&gt;

        &lt;Button x:Name=&quot;btnBack&quot;
                Content=&quot;戻る&quot;
                Visibility=&quot;{Binding ElementName=menuBar, Path=btnBackVisibility, Mode=TwoWay}&quot; /&gt;

        &lt;Button x:Name=&quot;btnNext&quot;
                Content=&quot;次へ&quot;
                Visibility=&quot;{Binding ElementName=menuBar, Path=btnNextVisibility, Mode=TwoWay}&quot; /&gt;

    &lt;/Grid&gt;
&lt;/UserControl&gt;

これで、menuBar.btnBackVisibility の値を変えると、内部のボタンの Visibility が連動して変わります。逆に、ルートに名前がない状態で ElementName=MenuBar と書いても参照できないので、UIは変化しません。

補足:Mode=TwoWayは本当に必要?

ボタンの表示状態を「外から設定するだけ」なら、通常は Mode=OneWay で十分です。TwoWayにすると“ボタンのVisibility変更が依存プロパティに戻る”動きになりますが、ボタン側からVisibilityを変更する場面は少ないため、OneWayの方が意図が明確です。

&lt;Button Visibility=&quot;{Binding ElementName=menuBar, Path=btnBackVisibility, Mode=OneWay}&quot; /&gt;

ただし、すでにTwoWayで動いていて問題がないなら、無理に変える必要はありません。チームのコーディング規約や実装意図に合わせて選択してください。

代替案:ElementNameを使わずRelativeSourceで参照する

「名前を付けるのが嫌」「名前が増えると管理が面倒」という場合は、RelativeSourceでUserControl自身を参照する方法もあります。こちらは名前に依存しないので、リファクタリングにも強いです。

&lt;Button Visibility=&quot;{Binding Path=btnBackVisibility,
                             RelativeSource={RelativeSource AncestorType=UserControl}}&quot; /&gt;

どちらを使っても目的は達成できます。今回のようにElementNameを採用しているなら「ルートにx:Name」を付けるのが最短です。

依存プロパティの定義:フィールドではなくラッパープロパティを使う

「依存プロパティのフィールド(btnBackVisibilityProperty)を書き換える」操作がうまくいかない理由を、コードで確認しておきましょう。依存プロパティは通常次の形で定義されます。

public partial class MenuBar : UserControl
{
    public static readonly DependencyProperty btnBackVisibilityProperty =
        DependencyProperty.Register(
            nameof(btnBackVisibility),
            typeof(Visibility),
            typeof(MenuBar),
            new PropertyMetadata(Visibility.Hidden));

    public Visibility btnBackVisibility
    {
        get =&gt; (Visibility)GetValue(btnBackVisibilityProperty);
        set =&gt; SetValue(btnBackVisibilityProperty, value);
    }

    public static readonly DependencyProperty btnNextVisibilityProperty =
        DependencyProperty.Register(
            nameof(btnNextVisibility),
            typeof(Visibility),
            typeof(MenuBar),
            new PropertyMetadata(Visibility.Hidden));

    public Visibility btnNextVisibility
    {
        get =&gt; (Visibility)GetValue(btnNextVisibilityProperty);
        set =&gt; SetValue(btnNextVisibilityProperty, value);
    }

    public MenuBar()
    {
        InitializeComponent();
    }
}

ここで重要なのは次の点です。

  • btnBackVisibilityProperty は「依存プロパティを登録した結果として得られる定義情報」です(値そのものではない)
  • 実際の値は、UserControlインスタンスが内部に持つ“依存プロパティストア”に入ります
  • したがって、値を変えたいときは btnBackVisibility(ラッパープロパティ)または SetValue を使います

また、WPFの慣習としては、プロパティ名をPascalCaseにすることが多いです(例:BackButtonVisibility)。ただ、既存コードが btnBackVisibility で統一されているなら、まずは動く形を優先して問題ありません。後からリネームする場合は、バインディングの Path も同時に更新してください。

MainWindow側:表示されているMenuBarインスタンスを参照できるようにする

次にMainWindowです。XAMLで配置しているMenuBarにx:Nameを付け、コードから参照できるようにします。質問の例ではすでにuCtrlという名前が付いているので、そのまま利用できます。

&lt;UserControls:MenuBar x:Name=&quot;uCtrl&quot;
                      ClickBk=&quot;btnMove_Click&quot;
                      ClickNx=&quot;btnMove_Click&quot; /&gt;

&lt;Frame x:Name=&quot;PageFrame&quot; /&gt;

これで、コードビハインドから uCtrl.btnBackVisibility のようにアクセスできます。ここで重要なのは「表示されているMenuBar」はこの1つだけ、という点です。別途new MenuBar()しても、それは画面に出ていないので効果がありません。

Pageからボタンを表示する:MainWindowを取得してuCtrlの依存プロパティを変更する

最後にPage側です。PageOutputが表示されたタイミングでボタンを表示したいなら、ページ側で「表示中のMainWindow」を取得し、MainWindowに置かれたuCtrlの依存プロパティを変更します。

ただし、コンストラクタ直後はまだWindowが取れないことがあるため、確実にしたい場合はLoadedイベントで行うのがおすすめです。

public partial class PageOutput : Page
{
    public PageOutput()
    {
        InitializeComponent();
        Loaded += PageOutput_Loaded;
        Unloaded += PageOutput_Unloaded;
    }

    private void PageOutput_Loaded(object sender, RoutedEventArgs e)
    {
        var mainWindow = (MainWindow)Window.GetWindow(this);
        if (mainWindow == null) return;

        mainWindow.uCtrl.btnBackVisibility = Visibility.Visible;
        mainWindow.uCtrl.btnNextVisibility = Visibility.Visible;
    }

    private void PageOutput_Unloaded(object sender, RoutedEventArgs e)
    {
        // 他ページへ移動したら元に戻したい場合(要件に合わせて調整)
        var mainWindow = (MainWindow)Window.GetWindow(this);
        if (mainWindow == null) return;

        mainWindow.uCtrl.btnBackVisibility = Visibility.Hidden;
        mainWindow.uCtrl.btnNextVisibility = Visibility.Hidden;
    }
}

Unloadedで元に戻すかどうかは設計次第です。「PageOutputのときだけ見せたい」なら、戻す処理を入れておくと状態が崩れにくくなります。逆に「一度表示したらアプリ終了まで表示のままでよい」なら、戻す処理は不要です。

Page遷移の直前に切り替える例(ボタンを押してPageOutputへ移動する場合)

「PrintボタンでPageOutputへ移動する」フローなら、遷移直前にMainWindow側のMenuBarを更新してからFrameに表示するのも分かりやすいです。

private void Print_Click(object sender, RoutedEventArgs e)
{
    // 入力チェックなどの既存処理…

    var mainWindow = (MainWindow)Window.GetWindow(this);
    if (mainWindow == null) return;

    mainWindow.uCtrl.btnBackVisibility = Visibility.Visible;
    mainWindow.uCtrl.btnNextVisibility = Visibility.Visible;

    // PageFrame.Content でもよいが、ナビゲーション履歴を使うなら Navigate の方が扱いやすい
    mainWindow.PageFrame.Navigate(new PageOutput());
}

ここでも重要なのは「uCtrlを操作している」点です。新しいMenuBarインスタンスを作らない、依存プロパティのフィールドを触らない。この2つだけ守れば、反映されない問題はほぼ解消できます。

どこで切り替えるのがベスト?2つの実装パターンを比較

同じ要件でも、実装の置き場所は大きく2パターンあります。どちらが正しいというより、保守性と責務の切り分けで選びます。

パターンやり方メリット注意点
Page側で切り替えるPageOutput.Loaded等でMainWindowを取得し、uCtrlを更新実装が直感的で早い。ページごとに振る舞いを書き分けやすい。PageがMainWindowに依存する(結合度が上がる)。ページが増えると同じ処理が散らばりやすい。
MainWindow側で一括制御Frame.Navigatedで遷移先のPageを判定し、uCtrlを更新「どのページで何を表示するか」をMainWindowに集約できる。戻し忘れが減る。ページ判定ロジックが増えるとMainWindowが肥大化する。ページの種類追加時にMainWindow修正が必要。

MainWindow側でFrameのNavigatedイベントを使う例

「PageOutputが表示されたときだけVisible」「それ以外はHidden」をMainWindowで一括管理したい場合は、Navigatedイベントが便利です。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        PageFrame.Navigated += PageFrame_Navigated;
    }

    private void PageFrame_Navigated(object sender, NavigationEventArgs e)
    {
        // 既定では非表示
        uCtrl.btnBackVisibility = Visibility.Hidden;
        uCtrl.btnNextVisibility = Visibility.Hidden;

        // 遷移先がPageOutputのときだけ表示
        if (e.Content is PageOutput)
        {
            uCtrl.btnBackVisibility = Visibility.Visible;
            uCtrl.btnNextVisibility = Visibility.Visible;
        }
    }
}

この方法だと、他のPageへ移ったときの「非表示に戻す処理」も同じ場所で完結します。ページが増えてきた段階では、MainWindow側一括制御の方がトラブルが減ることが多いです。

「Hidden」と「Collapsed」の違いを理解してUI崩れを防ぐ

Visibilityを切り替える際、HiddenにするかCollapsedにするかで、レイアウトの見え方が変わります。

値見え方レイアウトへの影響向いている場面
Visible表示される表示領域を確保する通常表示
Hidden見えない表示領域は確保したままボタンの位置をズラしたくない、レイアウトを固定したい
Collapsed見えない表示領域を確保しない(詰める)不要な余白を消したい、要素が増減するUI

「初期状態ではHiddenにしておきたい」という要件は、レイアウトを変えずにボタンだけ出し入れしたいケースが多いので合理的です。逆に、出していないときの余白が気になるならCollapsedも検討してください。

バインディングが効かないときの実務的デバッグ手順

WPFで最も時間を溶かしやすいのが「バインディングが効いていないのに気づかない」状態です。次の順で確認すると原因を潰しやすくなります。

  • Visual Studioの「出力」ウィンドウでBindingエラーを探す(ElementName参照先なし、プロパティ名ミスなど)
  • ボタンのVisibilityに一時的に固定値を入れて、UI自体が出せることを確認する
  • 依存プロパティの既定値(PropertyMetadata)が想定どおりか確認する
  • PageのコンストラクタではなくLoaded/Navigatedで切り替えているか確認する

さらに踏み込むなら、バインディングにトレースを仕込むと原因特定が速いです。

&lt;Button Visibility=&quot;{Binding ElementName=menuBar,
                             Path=btnBackVisibility,
                             PresentationTraceSources.TraceLevel=High}&quot; /&gt;

これにより、どのタイミングで何が解決できていないかが出力に出るため、「ElementNameが見えていない」「Pathのプロパティ名が違う」といった初歩的ミスを即座に発見できます。

もう少し良い設計にするなら:PageがMainWindowを直参照しない選択肢

ここまでの解決策は「最短で動かす」には強い一方、PageがMainWindowに直接依存します。小規模なら十分実用的ですが、画面数が増えると次の課題が出やすいです。

  • Page側のコードがMainWindowの構造(uCtrlという名前、MenuBarのプロパティ名)に引っ張られる
  • MainWindowの変更がPage全体に波及しやすい
  • 単体テストやモックがしづらい

この結合を弱める現実的な案を2つ紹介します。

案1:MainWindowが「表示状態の判定」だけ持ち、Pageは何もしない

前述のFrame.Navigated集中管理は、実務ではかなり効果があります。ページ側は表示ロジックを一切持たず、MainWindowが遷移先の型を見てMenuBarを制御します。ページ数が増えても「制御点が1つ」なので、状態が破綻しにくいのがメリットです。

案2:ViewModelのプロパティに寄せて、MenuBarはバインドだけにする(MVVM寄り)

MVVMを採用しているなら、MenuBarが参照するVisibilityをViewModel側に持たせ、MainWindow(またはナビゲーションサービス)がViewModelを更新するのが王道です。イメージとしては次の形です。

  • ViewModelに IsOutputPage や NavigationButtonsVisibility を用意
  • MainWindowが遷移イベントでViewModelを更新
  • MenuBarはViewModelのプロパティをバインドして表示を切り替える(UserControlが他画面でも再利用しやすい)

「まずは動かす」段階では今回の手法で問題ありません。中長期で画面が増えるなら、段階的に集中管理やMVVMへ寄せると保守性が上がります。

チェックリスト:実装後に確認しておきたいポイント

  • MenuBarのルートにx:Nameを付け、ElementNameバインディングが参照できている
  • MainWindow側でMenuBarのインスタンスにx:Nameが付いており、コードから同一インスタンスを触っている
  • 依存プロパティはフィールドではなくラッパープロパティ(またはSetValue)で値を更新している
  • Page側でWindow取得が必要なら、LoadedやNavigatedなど適切なタイミングで実行している
  • Visibilityが変わらないときは出力ウィンドウのBindingエラーを最優先で確認している

まとめ:表示中インスタンスと依存プロパティの“正しい距離感”が解決の鍵

WPFで「Page読み込み時にMainWindow上のUserControlのボタンを表示する」要件は、実装自体は難しくありません。しかし、依存プロパティの役割(定義情報と値の違い)と、UIに表示されているインスタンスがどれか、そしてバインディングの参照先が正しいか、この3点を取り違えると一気にハマります。

今回のポイントをもう一度だけまとめると、MenuBarの内部バインディングを成立させる、値を変える対象は必ず表示中のuCtrl、切り替えタイミングはLoaded/Navigatedで確実に。これだけで、Frame+Page構成でも意図どおりにボタン表示を制御できるようになります。

この記事を書いた人

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

コメント

コメントする

目次