4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

現代のWPF開発でMVVMに固執しない設計を考える

4
Posted at

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 は有力な選択肢になります。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?