アプリケーションを開発していると、あるウィンドウから別のウィンドウを開くというのはよくあるケースです。しかし、WPFでMVVMパターンを利用して開発していると、簡単そうに思えることでもちょっと頭を悩ませてしまいます。
例えば、
(ViewModel) MainViewModel => (View) MainWindow
(ViewModel) SubViewModel => (View)SubWindow
という関係で、MainWindowのボタンクリックでSubWindowをモーダルで開くとします。
ViewModelから直接サブウィンドウのViewを生成する
何も考えないでコードを書いたら以下のようになりました。
class MainViewModel : ViewModelBase
{
public ICommand OpenSubWindowCommand { get; private set; }
public MainViewModel()
{
OpenSubWindowCommand = CreateCommand(v =>
{
SubWindow wnd = new SubWindow();
wnd.ShowDialog();
});
}
}
しかしこのコードはいただけません。
ViewModelからViewを直接生成しています。MVVMパターンではViewModelは原則としてViewのことを知らない筈です。例えそれが自身と結合されるViewでなかったとしてもです。これではViewModelがViewに依存してしまいます。
View側でサブウィンドウのViewを生成する
では、Viewのコードビハインドに記述するというのはどうでしょうか?
class MainViewModel : ViewModelBase
{
public ICommand OpenSubWindowCommand { get; private set; }
public event EventHandler OpenSubWindow;
public MainViewModel()
{
OpenSubWindowCommand = CreateCommand(v =>
{
if (OpenSubWindow != null)
OpenSubWindow(this, EventArgs.Empty);
});
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContextChanged += (o, e) =>
{
MainWindowViewModel vm = DataContext as MainWindowViewModel;
vm.OpenSubWindow += (sender, arg) =>
{
SubWindow wnd = new SubWindow();
wnd.ShowDialog();
};
};
DataContext = new MainWindowViewModel();
}
}
ViewModelからイベント経由で通知を受け取り、View側でサブウィンドウを生成・表示させてみました。
ViewModelのViewへの依存はこれでなくなりました。
しかしこれもイマイチに思えます。MainViewModelはSubViewModelをViewModelとして持つViewを開きたい訳ですが、これでは実際にどのようなサブウィンドウがが表示されるのか完全にViewに依存してしまい、ViewModel側で把握できなくなってしまいます。
パラメーターでViewModelを渡すという方法もありますが、View側で別のViewを開くという機能を持たせること自体なんだかしっくりきません。
また、コードビハインドにこのようなコードを書くこと自体に抵抗を感じる人もいるでしょう。
AppクラスにViewModelからViewを生成する機能を持たせる。
イメージとしては、MainViewModel から SubViewModel を生成することで SubWindowも生成したい訳ですが、ViewModelから直接Viewを生成することはしたくない。そこでAppクラスに ViewModel と View の対応を管理させるというのはどうでしょうか?
Appクラスであれば、ViewModelとView、双方の関連を保持していても不自然ではありません。AppクラスにViewModelとViewの対応を管理する辞書を持たせて、ViewModelからViewを生成する機能を追加します。
public partial class App : Application
{
private Dictionary<Type, Type> ViewModels { get; set; }
// コンストラクタ
public App()
: base()
{
// ViewModel と View の対応を設定する
ViewModels = new Dictionary<Type,Type>();
ViewModels.Add(typeof(SubViewModel), typeof(SubWindow));
}
// ViewModelからViewを生成する
public Window CreateView<T>(T viewModel)
{
// ViewModel に対応する Viewが存在する?
if (ViewModels.ContainsKey(viewModel.GetType()))
{
// View を生成し、DataContext に ViewModel を設定する
Type viewType = ViewModels[viewModel.GetType()];
Window wnd = Activator.CreateInstance(viewType) as Window;
if (wnd != null)
wnd.DataContext = viewModel;
return wnd;
}
else
return null;
}
// ViewModelからモーダルでViewを表示する
public bool ShowModalView<T>(T viewModel)
{
Window view = CreateView(viewModel);
if (view != null)
return (view.ShowDialog() == true);
else
return false;
}
// ViewModeからモードレスでViewを表示する
public void ShowView<T>(T viewModel)
{
Window view = CreateView(viewModel);
if (view != null)
view.Show();
}
}
VeiwModelからサブウィンドを開くコードは以下のようになります。
// SubWindow の ViewModel
class SubViewModel : ViewModelBase
{
public SubViewModel()
{
}
}
// MainWindow の ViewModel
class MainViewModel : ViewModelBase
{
public ICommand OpenSubWindowCommand { get; private set; }
public MainViewModel()
{
OpenSubWindowCommand = CreateCommand(v =>
{
App app = App.Current as App;
app.ShowModalView(new SubViewModel());
});
}
}
たかが別のウィンドウを開くだけで面倒だなぁという気もします。
何かもっと巧い方法はないものでしょうか?