WPF でログイン画面などの複数画面を切り替えるとき、「UserControl の Visibility を ViewModel にバインドして制御したい」というニーズはとてもよくあります。ところが、バインディングもプロパティも一見正しく見えるのに、なぜか UserControl が表示されない……というハマりどころも鉄板です。ここでは、実際によくある「DataContext を子 UserControl で new してしまった」ケースを題材に、原因の仕組みと最小限の修正方法、そしてもう一歩進んだ MVVM 的な改善案まで、実装例付きで丁寧に整理していきます。
WPF/MVVMで「UserControl の Visibility を ViewModel で切り替えたい」シナリオ
今回の前提となる構成は、次のような典型的なログイン画面です。
- 親ビュー:
UserView.xaml - 子 UserControl:
- サインイン用:
signinUC(例:SignInView.xaml) - パスワード変更用:
resetPwdUC(例:ResetPwdView.xaml)
- サインイン用:
- 親 ViewModel:
UserViewModel
要件はシンプルです。
- 最初はサインイン画面(
signinUC)を表示する - 認証後、「初期パスワードだった場合」はパスワード変更画面(
resetPwdUC)を表示する - パスワードを変更したら、再びサインイン画面(
signinUC)に戻す
これを MVVM で実装するために、多くの方が次のような構造を採用します。
UserViewModelにSignInView/ResetPwdViewというVisibility型プロパティを持たせる- 親ビューの XAML で、各 UserControl の
Visibilityをそのプロパティにバインドする
<controls:signinUC Visibility="{Binding SignInView}" />
<controls:resetPwdUC Visibility="{Binding ResetPwdView}" />
ところが実際に動かしてみると、
signinUCはちゃんと非表示になる- にもかかわらず
resetPwdUCはいつまでたっても表示されない
という不可解な挙動に遭遇します。デバッグログでは ResetPwdView == Visible と出ているのに、画面はなにも変わらない……。この状態のまま数時間溶かした経験、ありませんか?
結論:子 UserControl での DataContext = new UserViewModel() が全ての元凶
この症状の原因はとてもシンプルで、かつ超頻出のパターンです。
子 UserControl(signinUC / resetPwdUC)のコンストラクタで、DataContext = new UserViewModel() を書いてしまっている ことが原因です。
// ありがちな NG パターン
public partial class SignInView : UserControl
{
public SignInView()
{
InitializeComponent();
DataContext = new UserViewModel(); // ← これが犯人
}
}
public partial class ResetPwdView : UserControl
{
public ResetPwdView()
{
InitializeComponent();
DataContext = new UserViewModel(); // ← これも犯人
}
}
Accepted Answer としては非常に簡単で、
- 子 UserControl の
DataContext = new UserViewModel()を全部削除する
これだけで、Visibility の切り替えが素直に動くようになります。
なぜそれだけで直るのか? DataContext の継承ルールを理解する
WPF の DataContext は、基本的に「親から子へ継承」されます。親の UserControl に ViewModel をセットすると、その下にぶら下がる要素が同じ ViewModel インスタンスを共有する、という仕組みです。
今回の正しい構成は次のようになります。
// 親 UserControl だけが DataContext を設定する
public partial class UserView : UserControl
{
public UserView()
{
InitializeComponent();
DataContext = new UserViewModel(); // ここで 1 回だけ生成
}
}
そして、子 UserControl は DataContext を一切触らず、親からの継承に任せます。
// OK パターン:DataContext は何も設定しない
public partial class SignInView : UserControl
{
public SignInView()
{
InitializeComponent();
}
}
public partial class ResetPwdView : UserControl
{
public ResetPwdView()
{
InitializeComponent();
}
}
これで、
UserViewが持っているUserViewModelのインスタンスsigninUCが参照するDataContextresetPwdUCが参照するDataContext
のすべてが同じインスタンスになります。結果として、ViewModel の SignInView / ResetPwdView を更新すると、両方の UserControl の Visibility が同じ値を参照して正しく切り替わるわけです。
逆に子で new UserViewModel() してしまうと何が起こるか
子 UserControl で DataContext = new UserViewModel() を書いてしまうと、次のような状態になります。
| 場所 | 保持している ViewModel インスタンス |
|---|---|
親ビュー UserView | インスタンス A(new UserViewModel()) |
子ビュー signinUC | インスタンス B(new UserViewModel()) |
子ビュー resetPwdUC | インスタンス C(new UserViewModel()) |
つまり、見た目は全部 UserViewModel ですが、実体は 3 つの別々のオブジェクトです。このとき、
- 認証処理は親が持っているインスタンス A のプロパティを更新している
- しかし
signinUCはインスタンス B のSignInViewを見ている resetPwdUCはインスタンス C のResetPwdViewを見ている
……という破綻した状態になります。
結果として、
- コンソールログ:インスタンス A の
ResetPwdViewがVisibleになったことを表示 - UI:インスタンス C の
ResetPwdViewは一度も変更されていないので表示が変わらない
という「コード上は正しく見えるのに画面が変わらない」状況が発生します。
「型」ではなく「インスタンス」が一致しているかどうかが非常に重要なポイントです。
最小修正例:子 UserControl の DataContext 設定を削除する
それでは、実際に最低限どこを直せばよいかをまとめます。
| 場所 | 修正前(NG) | 修正後(OK) |
|---|---|---|
親ビュー UserView.xaml.cs | public partial class UserView : UserControl { public UserView() { InitializeComponent(); DataContext = new UserViewModel(); } } | // そのままで OK(親だけが DataContext を設定) |
子ビュー SignInView.xaml.cs | public partial class SignInView : UserControl { public SignInView() { InitializeComponent(); DataContext = new UserViewModel(); // ← 削除対象 } } | public partial class SignInView : UserControl { public SignInView() { InitializeComponent(); // DataContext はいじらない } } |
子ビュー ResetPwdView.xaml.cs | public partial class ResetPwdView : UserControl { public ResetPwdView() { InitializeComponent(); DataContext = new UserViewModel(); // ← 削除対象 } } | public partial class ResetPwdView : UserControl { public ResetPwdView() { InitializeComponent(); // DataContext はいじらない } } |
XAML 側の Visibility バインディングはそのままで構いません。
<controls:signinUC Visibility="{Binding SignInView}" />
<controls:resetPwdUC Visibility="{Binding ResetPwdView}" />
これだけで、SignInView / ResetPwdView の値変更に合わせて UserControl の表示・非表示が切り替わるようになります。
補足:PasswordBox.PasswordChanged での ViewModel 参照はどうするか
ログイン画面では PasswordBox を使うことが多く、Password プロパティを直接バインドできないため、
PasswordChangedイベントハンドラで ViewModel にパスワードを流し込む
という実装もよく使われます。
<PasswordBox
PasswordChanged="PasswordBox_OnPasswordChanged" />
private void PasswordBox_OnPasswordChanged(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as UserViewModel;
if (vm == null) return;
var passwordBox = sender as PasswordBox;
vm.Password = passwordBox?.Password ?? string.Empty;
}
このコードは、子 UserControl であっても DataContext を自分で new しない限り、親から継承された UserViewModel インスタンスをちゃんと参照してくれます。
つまり、
- 「新しい ViewModel を生成しない」ことさえ守れば、このイベントハンドラはそのまま利用可能
ということになります。
Visibility プロパティ設計のパターン整理
今回のように Visibility を直接 ViewModel のプロパティにする方法は、わかりやすい反面、やや扱いづらい部分もあります。代表的なパターンと特徴を整理してみます。
| パターン | プロパティ型 | 特徴 | 向いているケース |
|---|---|---|---|
| Visibility 直バインド | Visibility | コード側で Visible/Collapsed を直接設定。XAML がシンプル。 | 画面数が少なく、単純に出し分けしたい場合 |
| bool + コンバーター | bool | BooleanToVisibilityConverter で変換。ロジック上は true/false のみ扱えば良い。 | ビジネスロジック的には「表示フラグ」の方が自然なとき |
| ContentControl + DataTemplate | 任意の型 | Content の型に応じてビューを自動切り替え。MVVM 的に最も柔軟。 | 画面数が増える、状態が複雑になる、大規模な画面構成 |
特に、WPF らしさ・MVVM らしさを出したい場合は、次に紹介する ContentControl + DataTemplate パターンが強力です。
より MVVM らしく:ContentControl + DataTemplate で画面を切り替える
Visibility を個別に制御する代わりに、
- ViewModel に「現在表示する画面」を表すプロパティ(例:
CurrentView)を持たせる - 親ビューには
ContentControlを 1 つ置くだけにする - 中身は DataTemplate で「型に応じて」自動的に切り替える
という構成にすると、画面が増えてもシンプルに保ちやすくなります。
ViewModel 側の例
public class UserViewModel : INotifyPropertyChanged
{
private object _currentView;
public object CurrentView
{
get => _currentView;
set
{
if (_currentView == value) return;
_currentView = value;
OnPropertyChanged(nameof(CurrentView));
}
}
public SignInViewModel SignInVM { get; } = new SignInViewModel();
public ResetPwdViewModel ResetPwdVM { get; } = new ResetPwdViewModel();
public UserViewModel()
{
// 初期状態はサインイン画面
CurrentView = SignInVM;
}
public void OnSignInSucceeded(bool isInitialPassword)
{
if (isInitialPassword)
{
// 初期パスワードならパスワード変更画面へ
CurrentView = ResetPwdVM;
}
else
{
// 通常ログインならそのまま
}
}
public void OnPasswordResetCompleted()
{
// パスワード変更が終わったらサインイン画面に戻す
CurrentView = SignInVM;
}
// INotifyPropertyChanged 実装は省略
}
ビュー側 XAML の例
親ビューでは ContentControl を 1 つ置き、Content を CurrentView にバインドします。
<UserControl
x:Class="Sample.Views.UserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Sample.ViewModels"
xmlns:views="clr-namespace:Sample.Views">
<UserControl.Resources>
<DataTemplate DataType="{x:Type vm:SignInViewModel}">
<views:SignInView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ResetPwdViewModel}">
<views:ResetPwdView />
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl Content="{Binding CurrentView}" />
</Grid>
</UserControl>
CurrentView に SignInViewModel をセットすれば自動的に SignInView が表示され、ResetPwdViewModel をセットすれば自動的に ResetPwdView に切り替わります。
この方式のメリットは次の通りです。
- Visibility の組み合わせを ViewModel で細かく管理する必要がない
- 画面が増えても「ViewModel と View のペア」を追加するだけで拡張しやすい
- View ごとに専用の ViewModel を用意できるため、責務分割が明確になる
特にログインフローが
- サインイン
- 2段階認証コード入力
- 初回パスワード変更
- 利用規約同意
のようにステップが増えていく場合、この方式にしておくとコードの見通しが大きく変わります。
Visibility を使う場合の実装パターン:bool + コンバーター
既存プロジェクトの方針などで「とりあえず Visibility プロパティ方式のまま行きたい」という場合でも、内部ロジックは bool を使い、XAML 側で Visibility に変換すると扱いやすくなります。
XAML にコンバーターを用意
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
</UserControl.Resources>
<Grid>
<controls:signinUC Visibility="{Binding IsSignInVisible,
Converter={StaticResource BoolToVis}}" />
<controls:resetPwdUC Visibility="{Binding IsResetVisible,
Converter={StaticResource BoolToVis}}" />
</Grid>
ViewModel 側のプロパティ
private bool _isSignInVisible = true;
public bool IsSignInVisible
{
get => _isSignInVisible;
set
{
if (_isSignInVisible == value) return;
_isSignInVisible = value;
OnPropertyChanged(nameof(IsSignInVisible));
}
}
private bool _isResetVisible;
public bool IsResetVisible
{
get => _isResetVisible;
set
{
if (_isResetVisible == value) return;
_isResetVisible = value;
OnPropertyChanged(nameof(IsResetVisible));
}
}
public void SwitchToResetPassword()
{
IsSignInVisible = false;
IsResetVisible = true;
}
bool であれば、「サインイン画面が表示されているか?」という論理をテストコードで扱いやすくなりますし、「Visible か Collapsed かは UI の都合」という責務分割にもなります。
イベントはコマンド化してテストしやすくする
MVVM では、ボタンのクリックなどは ICommand を通じて ViewModel に伝えるのが定番です。今回の例でいえば、
- 「サインイン」ボタン押下 →
SignInCommand - 「パスワード変更」ボタン押下 →
ResetPasswordCommand
のようにコマンド化しておくと、次のような利点があります。
- ViewModel 単体で「ログイン成功時に CurrentView が切り替わるか?」といったテストが書きやすい
- 画面の挙動を XAML と ViewModel に閉じ込められる(コードビハインドが痩せる)
- バインディングエラーが起きても XAML 側で把握しやすい
簡単な RelayCommand の例と、サインイン処理をコマンド化したサンプルを示します。
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
}
public class UserViewModel : INotifyPropertyChanged
{
public ICommand SignInCommand { get; }
public UserViewModel()
{
SignInCommand = new RelayCommand(_ => SignIn());
}
private void SignIn()
{
// 認証処理
// var result = Authenticate(UserId, Password);
bool isInitialPassword = true; // 仮
if (/* 認証成功 */ true)
{
OnSignInSucceeded(isInitialPassword);
}
}
// OnSignInSucceeded は前述の CurrentView 切替処理など
}
<Button Content="サインイン"
Command="{Binding SignInCommand}" />
このように、画面切り替えの状態遷移を ViewModel に集約しておくことで、DataContext の設計ミス(今回のような new しすぎ問題)を早い段階で検出しやすくなります。
Visibility 切り替えでハマりがちなその他のポイント
最後に、Visibility を使ったビュー切り替えでありがちな落とし穴をまとめておきます。今回の DataContext 問題と併せてチェックしておくと、トラブルシューティングの効率がかなり良くなります。
| ポイント | ありがちな症状 | 対処のヒント |
|---|---|---|
| Hidden と Collapsed の違い | 見えないのに画面のレイアウトが崩れる / 余白だけ残る | Hidden はレイアウト上は場所を取る。基本的には Collapsed を使うと扱いやすい。 |
| Grid 上の重なり順 | どちらも Visible にすると、どちらが表示されているのか分からない | 原則として「必ずどちらか一方のみ Visible」にする。重ねる場合は ZIndex も確認。 |
| バインディングエラー | デバッグ出力に BindingExpression path error が出る | プロパティ名のスペルミスや DataContext の型不一致をチェック。nameof() で通知する癖をつける。 |
| INotifyPropertyChanged の未実装 | 初期表示は正しいが、その後の変更が UI に反映されない | OnPropertyChanged(nameof(SignInView)) の呼び忘れを確認。 |
| DataContext の上書き | 今回のように、一部の UserControl だけ挙動が異なる | 子 UserControl のコンストラクタや Loaded イベントで DataContext = ... していないか確認。 |
チェックリスト:本番前に確認しておきたい項目
最後に、今回のケースに直接関係するチェックポイントをリストアップしておきます。開発中・リファクタリング時のセルフレビューにそのまま使えます。
- 子 UserControl(
signinUC/resetPwdUC)にDataContext = new UserViewModel()が残っていないか - 親の
UserViewだけがDataContextを 1 度だけ設定しているか SignInViewとResetPwdViewの Visibility が「排他的」に設定されているか(両方 Visible になるパスがないか)INotifyPropertyChangedの通知がnameofを使って安全に書かれているか- デバッグ出力にバインディングエラーが出ていないか(特にプロパティ名のスペルミス)
- 可能であれば ContentControl + DataTemplate への移行を検討しているか
まとめ:WPF/MVVM での UserControl 切り替えは DataContext 設計がすべて
ここまで、ログイン画面を例に、
- UserControl の
Visibilityを ViewModel のプロパティで切り替えたい - しかし、なぜか片方の画面が表示されない
という問題の原因と解決策を見てきました。ポイントを改めて整理すると、次の通りです。
- 原因:子 UserControl が
DataContext = new UserViewModel()で独自に ViewModel を生成していた - 結果:親と子で別々の ViewModel インスタンスを参照しており、親がプロパティを変えても子のバインディング先は変わらなかった
- 最小解決策:子 UserControl の DataContext 設定を削除し、親
UserViewが 1 つの ViewModel インスタンスを持つようにする - 推奨パターン:中長期的には
ContentControl + DataTemplateを使った画面切り替えに移行すると、MVVM 的にすっきり保守しやすい
WPF は「同じ型の ViewModel を new しても意味が違う」世界です。画面をまたいで共有すべき情報は、必ず同じインスタンスを共有するように DataContext の設計を意識することが、MVVM アーキテクチャを長く保守していくうえでの重要なポイントになります。
もしあなたのプロジェクトでも「Visibility で切り替えているのに画面が出てこない」「ログイン後に別画面に遷移しない」といった症状が出ていたら、まずは子 UserControl のコンストラクタを開き、
「つい癖で書いてしまった DataContext = new ... が紛れ込んでいないか?」
を確認してみてください。それだけで、長年のモヤモヤが一瞬で解決するかもしれません。

コメント