はじめに:MVVMの理想と、現場で直面する「つらさ」
WPFでUIを開発するとき、MVVMパターンは今やデファクトスタンダードですよね。ViewModelはViewを知らず、テストが容易で、関心の分離が美しい...。理想は素晴らしいものです。
しかし、現場で実装していると、こんな「つらさ」に直面しませんか?
- 「すべてをデータバインドでやらなきゃ…」という制約。ダイアログ表示やアニメーションのトリガーなど、バインドだけでは煩雑になる処理がある。
- ViewModelから能動的にViewを操作したいのに、そのタイミングをうまく制御できない。
-
IsHogeFugaTriggerのような、Viewへの通知ためだけの**無駄な「トリガープロパティ」**をViewModelに作りがち。 - 特にCADビューアのような高性能なUIでは、データバインドの更新頻度がパフォーマンスの足かせになる。
これらの問題を解決するために、MVVMの原則を守りつつも、より現実的なアプローチとして**「ViewService」**を導入するハイブリッドな設計について紹介します。
解決策:IView / ViewServiceという「通訳」の導入
この設計の核となるのは、ViewModelとViewの間に、UI操作を代行してくれる**「通訳」役のインターフェース**を置くことです。ここではIViewServiceと呼びます。
-
ViewModelは、具体的なView(
MainWindow.xamlなど)のことは知りません。 - 代わりに、
ISomeViewServiceという抽象的なインターフェースにのみ依存します。 - ViewServiceの実装クラスが、Viewへの具体的な操作をすべて引き受けます。
これにより、ViewModelのテスト容易性というMVVM最大のメリットは維持したまま、Viewにしかできない処理を安全に呼び出せるようになります。
実装例:IMainContentViewServiceの設計
私が実際のCAD系アプリケーションで実装したViewServiceのインターフェースがこちらです。
public interface IMainContentViewService
{
// ViewからVMへ、必要なデータだけを公開する
IEnumerable<IProfileDataPointElement> SelectedVertices { get; }
// VMからViewへ、振る舞いを命令するためのコマンド
RelayCommand FitCommand { get; }
RelayCommand SelectAllCommand { get; }
RelayCommand ShowSettingDialogCommand { get; }
RelayCommand ShowGridCommand { get; }
// ...など多数
}
ポイント:ICommandを直接公開する
void Fit()のようなメソッドではなく、RelayCommandのようなICommandプロパティを公開しているのが特徴です。
これにより、UIの組み立てをXAMLの宣言的な記述だけで完結させることができます。
例えば、メインウィンドウのメニュー項目から、このViewのFitCommandを直接バインドできます。
<MenuItem Header="フィット表示"
Command="{Binding MainContentService.FitCommand}" />
ViewServiceの実装クラス
このインターフェースの実装は、受け取った命令をViewの各部品に伝達するだけのシンプルなものになります。
public class MainContentViewService : ViewService<MainContentView>, IMainContentViewService
{
public MainContentViewService(MainContentView mainContentView) : base(mainContentView) { }
// SelectedVerticesは、Viewの内部コントロールからデータを取得して返すだけ
public IEnumerable<IProfileDataPointElement> SelectedVertices => this.View.ProfileDataTableView.SelectedItemsAsProfilePointsData;
private RelayCommand? _fitCommand = null;
public RelayCommand FitCommand
{
get
{
if (this._fitCommand == null)
{
// canExecuteのロジックは、Viewの状態に直接アクセスできるのでシンプル
bool canExecute(object obj)
{
return this.View.ProfileDataTableView.Items.Count > 0;
}
// executeは、Viewの特定の部品のメソッドを呼ぶだけ
void execute(object obj)
{
this.View.ProfileDataGraphView.InternalZoomViewer.Fit();
}
this._fitCommand = new RelayCommand(canExecute, execute);
}
return this._fitCommand;
}
}
// ...他のコマンド実装
}
canExecuteの判定にthis.View.ProfileDataTableView.Items.Countを直接使っている点に注目してください。ViewModelにこの状態を同期させるよりも、はるかにシンプルで効率的です。
設計原則:私たちのハイブリッドMVVMルール
このアーキテクチャをうまく運用するために、私たちは以下の4つのルールを設けました。これにより、実装の際に「これはどっちで書くべき?」という迷いがなくなります。
1. 状態の同期 → データバインド
ViewModelのプロパティとViewの見た目を常に同期させたい場合は、伝統的なデータバインドを使います。これはMVVMの基本です。
(例: TextBoxのText、ItemsControlのItemsSource)
2. 振る舞いの命令 → ViewService
ViewModelからViewへ一度きりの動作を命令したい場合は、ViewServiceのメソッドやコマンドを使います。
(例: FitToScreen(), ShowDialog())
3. View内部の処理 → コードビハインド
ViewModelが知る必要のない、Viewだけで完結するUIロジックは、コードビハインド(.xaml.cs)に書くことを許可します。
(例: ウィンドウの閉じるボタン、マウスオーバー時のアニメーション)
4. VMが知るべき情報 → 必要最低限に
ViewからViewModelへ情報を伝えるのは、その情報がビジネスロジックで本当に必要な場合のみです。UIの見た目の状態(例: パネルが開いているか)はViewModelに伝えてはいけません。
おわりに
MVVMは強力なパターンですが、その「理想」に固執しすぎると、かえって複雑で非効率なコードを生んでしまうことがあります。
今回紹介したViewServiceを導入するハイブリッドなアプローチは、MVVMの「テスト容易性」や「関心の分離」といったメリットを享受しつつ、現場での開発効率と保守性を高めるための現実的な落とし所です。
WPF/MVVMでの実装に悩んでいる方の、一つのヒントになれば幸いです。