##この記事の内容
最適解を見つけました!!!
ではなく、
- こんな風にも実装できたよ。
- こんな実装考えてみたんだけど意見くれませんか?
という内容になっています。
##前提条件
このアプリは×ボタンを押してもアプリが終了しないような、常駐アプリを想定しています。
すなわちShutdownModeがOnExplicitShutdownに設定してあります。
この設定以外の場合、アプリを終了してもウィンドウが残り、ゾンビ化してしまう場合があります。
別途対応が必要ですので適宜対応してください。
##はじめに
WPFアプリにおいてMVVMアーキテクチャを採用している場合
メインウィンドウに設置したボタンを押したときにサブウィンドウを開くという単純な処理の実装が地味に面倒だったりします。
MVVMアーキテクチャの設計思想として、ViewとViewModelはDatacontextでのみ情報を共有しており、ViewModelはViewの情報を能動的に知ることができません。
ましてや、ViewModelがバインディングされていないViewの情報を知る・操作することはなおさら設計思想に違反するもの(と個人的には思っています。)
この思想を考えないのであれば、メインウィンドウのViewModelからサブウィンドウのViewを参照して、Window.Show()
としてあげれば新しいサブウィンドウを開くことが可能です。
実装としてはエラーも発生しないかと思いますし、これが許される組織等であればこの形がシンプルかと思います。
ただ、個人的にはちょっと気持ち悪い部分があったので、どうにかならないかなと模索してみました。
##じゃあどうするのか
同じ状況で困っている人はたくさん居るはずで、ググれば解決策が見つかるだろう。
と思い色々調べると以下のような感じでずらずらと。
Opening new window in MVVM WPF
Open a new Window in MVVM
ただ個人的にしっくりくるものがありませんでした。
自分なりに考えた方法が以下の通り。
1.App.XamlのStartUpイベントを実装し、StartUpが呼ばれるようにする
<Application x:Class="WPFTestApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFTestApp"
Startup="Application_Startup">
<Application.Resources>
</Application.Resources>
</Application>
2.App.xaml.csのSutartup内において、SubWindowのViewのインスタンスを作成する。ただしShow()は実行しない
3.SubWindowのViewを、MainWindowのViewインスタンス生成時にコンストラクタに渡す。
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
SubWindow subWindow = new SubWindow();
MainWindow mainWindow = new MainWindow(subWindow); //SubWindow情報をコンストラクタに入れる
mainWindow.Show();
}
}
4.MainViewのViewModelにおいてSubWindowのViewのインスタンスから自作のOpenSubWindowServiceのインスタンスを作成
MainViewのButtonのデータバインディングに紐づけたICommandの中身においてOpenSubWindowService.Show()を実行
public partial class MainWindow : Window
{
public MainWindow(Window subWindow)
{
InitializeComponent();
this.DataContext = new VMMainView(subWindow); //ViewModelへと伝播
}
}
class VMMainView : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ICommand OpenCommand { get; private set; }
IOpenWindowService _openService;
public VMMainView(Window subWindow)
{
this.OpenCommand = new OpenCommandImpl(new OpenSubWindowService(subWindow));
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
class OpenCommandImpl : ICommand
{
public event EventHandler CanExecuteChanged;
IOpenWindowService _service;
public OpenCommandImpl(IOpenWindowService subOpen)
{
_service = subOpen;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_service.OpenWindow();
}
}
}
class OpenSubWindowService : IOpenWindowService
{
Window subwindow;
public OpenSubWindowService(Window subWindow)
{
this.subwindow = subWindow;
}
public void OpenWindow()
{
subwindow.ShowDialog(); //Show()にすると後ろにあるMainWindowの操作が可能
}
}
interface IOpenWindowService
{
public void OpenWindow();
}
注意点
一度サブウィンドウを×ボタンで閉じて、再度メイン画面より開こうとするとExceptionが発生します。
理由としては、Windowが閉じた状態でWindownに対してShow()操作を行っているためです。
ただ、今回の実装においてはWindowを生成しているのがApp.xaml.csの部分になるため
再度Windowを生成することが難しくなります。
対策としては、サブウィンドウのClosingイベントにおいてClosingをキャンセルし、Hide()します。
そうすることによって見えなくなっているだけなので再度Show()することが可能となります。
public partial class SubWindow : Window
{
public SubWindow()
{
InitializeComponent();
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
Hide();
}
}
ただし、この実装では元のWindowの情報が残っているので再表示時にClearする必要が出てきたり
膨大な量のWindowをShowする場合はすべてインスタンスが残っている状態になるため注意が必要です。
他にもいろいろと気づけていない制約はあるかもしれません。
まとめ
MVVMを利用したWPFで新たにサブウィンドウを開く方法を自分なりに考えてみました。
メインウィンドウのViewModelがサブウィンドウのViewのことを意識せずShow()できているので、MVVMの設計思想には沿えているのではないかなと思います。
ただ完全にすっきりする実装かといえばそうではないので、まだまだいい方法があるかもしれません。
- こんな実装方法があるよ
- こんな実装してみたんだけどどう?
という内容でした。
誤りがある場合や、よりいい方法がある方はコメント頂けると嬉しいです。