DIコンテナとリアクティブ指向を活かすという選択
本記事は筆者の考えをベースに、ChatGPT 5.4 を用いて構成・推敲したものです。
内容の主張と判断は筆者によるものであり、文章表現の整理に ChatGPT 5.4 を活用しています。
WPF の設計について話すと、かなり高い確率で MVVM が前提として置かれます。
実際、MVVM は WPF と相性のよいパターンですし、今でも有効な場面は多いです。
ただ、実装を続けていると、次のように感じる場面も少なくありません。
- View とほぼ同じライフサイクルの ViewModel を毎回作っている
- UI 固有の状態まで無理に ViewModel に寄せている
- 複雑なインタラクションになると結局 View 側の処理が増える
- Command や状態管理が ViewModel 前提になり、構成がかえって重くなる
この記事では、「常に MVVM を徹底する」のではなく、DI コンテナやリアクティブプログラミングを活かしながら、View を中心に組み立てる設計も十分に実用的ではないか、という立場から整理してみます。
前提: MVVM を否定したいわけではない
最初に前提を置いておくと、この記事の意図は MVVM の全面否定 ではありません。
むしろ言いたいのは次の一点です。
MVVM は有力な選択肢ではあるが、常に最適解とは限らない。
特に WPF では、View 自体がかなり強いオブジェクトです。
- ライフサイクルを持つ
- DependencyProperty を持つ
- RoutedEvent を扱える
- Visual Tree / Logical Tree に参加する
- レイアウト、描画、アニメーション、ヒットテストと密接に関わる
この性質を考えると、すべてを ViewModel 側へ押し出そうとするより、View に置いたほうが自然な責務も確実にあります。
UI 固有の状態は、View に置いたほうが自然なことが多い
たとえば次のような状態です。
- Popup が開いているか
- ドラッグ中か
- タブの挿入位置はどこか
- Busy オーバーレイを出すか
- 一時的な選択状態や hover 状態
- あるコントロール内部でしか意味を持たない中間状態
こうした状態は、業務上の意味を持つというより、その画面・そのコントロールのふるまいに強く結びついています。
この種の状態まで毎回 ViewModel に逃がしてしまうと、
- 状態の置き場所が不自然になる
- Binding や通知の経路が増える
- 結局 View 側との橋渡しコードが増える
という形で、むしろ複雑になることがあります。
そのため自分は、次のように考えることが多いです。
その状態がその View にしか意味を持たないなら、まず View 側に置くことを検討してよい。
ViewModel が「境界」ではなく「複製」になってしまうことがある
MVVM を機械的に適用すると、ViewModel が次のような立場になりがちです。
- View と同じライフサイクルを持つ
- View で必要なプロパティをそのまま並べる
- Command をいくつか持つ
- 実際の処理はサービスに委譲する
- 複雑な UI の制御は結局 View 側でやる
このとき、ViewModel は本当に「抽象化」になっているのか、少し立ち止まって考える必要があります。
もし ViewModel が、
- 明確な状態の集約をしていない
- 複数の View で再利用されない
- 表示ロジックの境界としても機能していない
- ただ View 用のプロパティと Command を置いているだけ
であれば、それは設計上の価値よりも、パターンの形式を守るための層になっている可能性があります。
もちろんすべての ViewModel がそうだとは言いません。
ただ、「View ごとに ViewModel を用意すること」自体が目的化していないかは、見直してよいポイントだと思っています。
View 自体を DataContext にする構成も普通に成立する
たとえば、画面固有の簡単な状態であれば、View 自身を DataContext にしても問題ありません。
public partial class SettingsView : UserControl, INotifyPropertyChanged
{
private bool _isBusy;
private string _keyword = "";
public event PropertyChangedEventHandler? PropertyChanged;
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy == value) return;
_isBusy = value;
PropertyChanged?.Invoke(this, new(nameof(IsBusy)));
}
}
public string Keyword
{
get => _keyword;
set
{
if (_keyword == value) return;
_keyword = value;
PropertyChanged?.Invoke(this, new(nameof(Keyword)));
}
}
public SettingsView()
{
InitializeComponent();
DataContext = this;
}
}
<Grid>
<TextBox Text="{Binding Keyword, UpdateSourceTrigger=PropertyChanged}" />
<ProgressBar IsIndeterminate="True"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
この形は、少なくとも次のような場面では十分実用的です。
- 状態がその画面内で閉じている
- 他画面との共有が不要
- UI 制御の比重が高い
- ViewModel を分けても設計上の利益が小さい
「View に書く = 悪」ではなく、責務の置き方として自然かどうかで判断したほうが実践的です。
Command は ViewModel の所有物でなくてもよい
WPF では Command が ViewModel のプロパティとして置かれることが多いですが、実際には Command はバインド可能な振る舞いオブジェクト として捉えたほうが柔軟です。
たとえば DI コンテナで Command を管理すると、次のような構成がとれます。
public sealed class SaveCommand : ICommand
{
private readonly ISettingsService _settingsService;
public SaveCommand(ISettingsService settingsService)
{
_settingsService = settingsService;
}
public bool CanExecute(object? parameter) => true;
public async void Execute(object? parameter)
{
await _settingsService.SaveAsync();
}
public event EventHandler? CanExecuteChanged;
}
public partial class SettingsView : UserControl
{
public ICommand SaveCommand { get; }
public SettingsView(SaveCommand saveCommand)
{
InitializeComponent();
SaveCommand = saveCommand;
DataContext = this;
}
}
<Button Content="Save" Command="{Binding SaveCommand}" />
この形の利点は分かりやすいです。
- Command の依存関係が明確
- サービスと直接つながる
- ViewModel を経由しなくてよい
- 再利用しやすい
- 構成が軽い
さらに、独自の MarkupExtension を使えば、XAML から直接 resolve する構成も可能です。
[MarkupExtensionReturnType(typeof(object))]
public sealed class ResolveExtension : MarkupExtension
{
public Type Type { get; set; } = null!;
public override object ProvideValue(IServiceProvider serviceProvider)
{
return AppServices.Root.GetRequiredService(Type);
}
}
<Button Content="Save"
Command="{local:Resolve Type={x:Type commands:SaveCommand}}" />
こうすると Command は ViewModel の付属物ではなく、コンテナに管理されるアプリケーションの構成要素として扱えます。
複雑なインタラクションは、どうしても View に寄る
WPF で本当に複雑な UI を作ると、次のような問題にぶつかります。
- ドラッグ操作
- ヒットテスト
- フォーカス制御
- Popup の位置決め
- サイズ計測
- テンプレート内部の連動
- VisualTree の探索
- アニメーション状態の管理
これらは本質的に View の文脈 にある問題です。
もちろん Behavior や Attached Property などで分離することはできます。
ただ、それでも元の問題が View 側にあること自体は変わりません。
そのため、
UI 固有のロジックまで無理に ViewModel に寄せるより、View / Control / Behavior といった UI の層で素直に扱ったほうが分かりやすい
という場面はかなりあります。
リアクティブプログラミングは WPF と相性がよい
WPF の画面は、実際には「状態」だけでなく「イベントの流れ」で動いています。
- 入力イベント
- 選択変更
- スクロール
- 非同期処理の開始と終了
- キャンセル
- 複数イベントの合流
- debounce / throttle
こうした場面では、従来の
- プロパティ
INotifyPropertyChanged- Command
だけで組み立てるより、リアクティブに扱ったほうが素直なことがあります。
たとえば検索入力の debounce は、次のように書けます。
public partial class SearchView : UserControl
{
public SearchView()
{
InitializeComponent();
Observable
.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
h => SearchBox.TextChanged += h,
h => SearchBox.TextChanged -= h)
.Select(_ => SearchBox.Text)
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged()
.ObserveOnDispatcher()
.Subscribe(async text =>
{
await SearchAsync(text);
});
}
private Task SearchAsync(string keyword)
{
// do search
return Task.CompletedTask;
}
}
このような「入力の流れ」や「状態遷移の流れ」は、Reactive な記述のほうが見通しがよいことが多いです。
その意味で、WPF の設計を考えるときは、MVVM だけでなく、
- DI コンテナ
- Reactive
- View/Control 側の責務整理
を含めて考えたほうが実装にフィットしやすいと感じています。
テストについて: 早い段階では断言と可観測性を重視したい
テストについても、少し現実的に考えています。
クライアントアプリ、とくに UI が頻繁に変わる初期〜中期フェーズでは、UI テストは壊れやすく、保守コストも高くなりやすいです。
そのため、自分は早い段階では次のようなものを重視しています。
Debug.Assert- fast fail
- ログ
- 異常状態の早期可視化
- 重要な不変条件の明文化
たとえば:
private void OpenDocument(string filePath)
{
Debug.Assert(!string.IsNullOrWhiteSpace(filePath), "filePath should not be empty.");
Debug.Assert(File.Exists(filePath), $"File not found: {filePath}");
if (!File.Exists(filePath))
{
Debugger.Break();
return;
}
// open logic
}
ここで大事なのは、Debug.Assert が自動テストの代わりだと言いたいわけではないことです。
ただ、変化の大きい段階では、壊れた状態をすぐ検知できることのほうが価値が高いケースは多いです。
安定してきた段階で、
- テストすべき境界
- 腐りにくいレイヤ
- 長期的に保守できるテスト
を見極めて補っていくほうが、結果として健全なこともあります。
まとめ
自分の立場をまとめると、次のようになります。
- MVVM は有効な設計パターンのひとつ
- ただし常に全面採用する必要はない
- UI 固有の状態や複雑なインタラクションは View 側に置いたほうが自然なことが多い
- Command は ViewModel に閉じ込めず、DI コンテナの構成要素として扱ってよい
- イベントや状態遷移の多い画面では Reactive が有効
- 初期段階ではテスト完備より、断言・可観測性・早期失敗を重視する考え方も現実的
つまり、言いたいのはシンプルです。
WPF 開発では、MVVM に固執するよりも、責務に応じて View / DI / Reactive / Service を素直に使い分けたほうが、実装が軽くなり、見通しもよくなることがある。
パターンを守ること自体が目的になると、設計はかえって重くなります。
大事なのは「どの責務をどこに置くと一番自然か」を見極めることだと思います。
おわりに
この記事は、WPF における MVVM の否定ではなく、使い分けの話として書きました。
MVVM を採るべき場面はもちろんあります。
ただ、すべての View に対して機械的に ViewModel を用意するより、UI の性質やライフサイクル、依存関係、イベントの流れを見ながら設計したほうが、実装しやすいケースも少なくありません。
WPF はもともと View 側がかなり強いフレームワークです。
その特性を素直に活かす設計も、もっと普通に選択肢になってよいと考えています。
ZYC.Framework について
ZYC.Framework は、.NET 10 と WPF をベースにした、高性能・モジュール型・拡張可能なデスクトップ自動化開発フレームワークです。モジュールアーキテクチャを中核に、マルチワークスペース/マルチタブ UI、WebView2、Blazor、.NET Aspire との連携を備えており、モダンなデスクトップアプリ、社内ツール、複雑な自動化システムの構築に適しています。
ネイティブな操作性を保ちながら Web 技術も活用し、明確なモジュール境界で継続的に拡張できる基盤を探しているなら、ZYC.Framework は有力な選択肢になります。
- GitHub: ZYC.Framework
- NuGet: ZYC.Framework.Alpha
- License: MIT