はじめに: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での実装に悩んでいる方の、一つのヒントになれば幸いです。