2025 業務アプリ向け WinForms  初級「MVPパターン」解説（自動テスト含む）

本記事では、C#のWinFormsアプリケーションを例に、MVPパターンにおける各クラスの役割と特徴を解説します。

MVP（Model-View-Presenter）パターンは、GUIアプリケーションの構造を明確にし、保守性とテスト容易性を高めるための設計パターンです。​

## １．前提条件
* Visual studio 2022 Version 17.13.6
* .Net 9

:::note 
なお、すべてのソースコードを公開しています。
:::
https://github.com/masayahak/MVPdemo

:::note 
このデモプログラムのDBアクセスは「あえて」簡易版です。
（DB構築なしで即実行できるようにしました。）

DBアクセスを含むフルスタックのサンプルについては、以下の記事をご覧ください。
:::

https://qiita.com/masayahak/items/abf639991e4501ca18e0


## ２．MVP（Model + View + Presenter）パターンの概要

### (1) MVPの概要
MVPイメージ図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/0c1735d9-1083-4f70-acb5-a92ce0aad767.png)

MVPパターンは、以下の3つのコンポーネントで構成されます：​

| コンポーネント | 役割 |
|:-|:-|
|Model | アプリケーションのデータとビジネスロジックを管理|
|View | ユーザーインターフェースを担当し、ユーザーの入力を受け取る（WinFormsだとForm）|
|Presenter | ModelとViewの仲介役であり、ユーザーの操作に応じてModelを更新し、その結果をViewに反映させる|

この構造により、各コンポーネントの責務が明確になり、コードの再利用性やテストのしやすさが向上します。


### (2) なぜMVCではなくMVPなのか？

 * MVCは元々「Webアプリ（リクエスト・レスポンス）」に最適化された構造です
 * MVPは「UIイベント中心のアプリ（WinForms/WPFなど）」に向いています
 * WinFormsではViewに直接Button.Clickなどのイベントが来るため、それをPresenterへ委譲するMVPが自然なんです


### (3) WinFormsとMVPの相性が良い理由 
|観点 |	MVP |	MVC|
|:-|:-|:-|
|UIイベントとの対応 |	View → Presenterに直接イベント通知できる（eventで） |	コントローラーが直接イベントを監視しづらい|
|自動テスト |	PresenterはViewのインターフェースで疎結合→モック化しやすい |	ControllerがUIに依存しやすい |
|責務分離 |	ViewはUI描画だけ、Presenterがロジック全て持つ |	ControllerとViewの責務が曖昧になりやすい |
|WinFormsとの親和性 |	Formにイベント＋バインドを集約しやすい | ViewとControllerの役割分担が混ざりやすい|



## ３．画面イメージから「View」と「リポジトリ」インターフェースの定義

:::note
リポジトリとビューは、それぞれインターフェースを通じて「何を提供できるか」を外部に明示します。
これにより、内部の実装に依存することなく、外部から必要な機能だけを利用できるようになります。
:::


### (1) 画面イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/f553010f-6438-44bc-86cd-53cc74604116.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/c8f2fc8a-dd5d-44e0-8d8c-44c568200bd3.png)


### (2) UI操作とそれに対応する処理の責任範囲
| UI上の操作 | プレゼンターの責任 | リポジトリの責任 |
|:-|:-|:-|
|画面起動 | GetAll() を呼び出して View に一覧を渡す |利用者一覧を DB（またはメモリ）から取得 |
|追加ボタンクリック |	View の入力欄を初期化し編集モードに切り替える |	なし|
|グリッドの選択 |	選択データを View に渡し、編集モードに切り替える |	なし|
|保存クリック |	新規か更新かで Add / Edit を分岐し実行 |	データの Insert または Update を実行|
|削除クリック |	Delete を実行し View に結果を通知する |	データの Delete を実行|
|キャンセルクリック |	View の入力状態をクリアし一覧表示に戻す |	なし|

> 🎯 Presenter は「UI操作」と「データ操作」の橋渡し役です。
> View や Repository に直接仕事をさせず、「どの操作で、何が起きるか」をプレゼンターがコントロールしています。


### (3) UI上の操作からVIEWのインターフェースを定義する

```csharp
public interface IView利用者
{
    // 画面起動（一覧表示用のデータを受け取る）
    void Set利用者ListBindingSource(BindingSource 利用者list);

    // 追加ボタンクリック
    event EventHandler AddNewEvent;   
    // グリッドの選択（修正開始要求）
    event EventHandler EditEvent;     
    // 保存ボタンクリック
    event EventHandler SaveEvent;     
    // 削除ボタンクリック
    event EventHandler DeleteEvent;   
    // キャンセルボタンクリック
    event EventHandler CancelEvent;   
}
```

### (4) 必要なDBアクセスからリポジトリのインターフェースを定義する

```csharp
public interface I利用者Repository
{
    // DBから利用者の一覧をSelectする
    IEnumerable<利用者Model> GetAll();

    // 入力した利用者をDBへInsert
    bool Add(利用者Model user);
    // 入力した利用者をDBへUpdate
    bool Edit(利用者Model user);
    // 選択した利用者をDBからDeleteする
    bool Delete(利用者Model user);
}
```


## ４．各コンポーネントの役割と実装

### (1) 🔷 Model の役割
データの構造や、データ操作（追加・修正・削除・検索）を提供します。ビジネスロジックも含めます。

:::note
極力データアノテーションを指定して、ビジネスルールをデータの構造で表現します。
これによりビジネスルールに合致しないデータの受入れを構造的に排除できます。
:::

```csharp
public class 利用者Model
{
    [Key]
    public int ID { get; set; }

    [Required(ErrorMessage = "利用者名は必須です。")]
    [StringLength(20, MinimumLength = 2, ErrorMessage = "利用者名は2文字以上+20文字以内で入力してください。")]
    public string 利用者名 { get; set; } = string.Empty;

    [StringLength(200, ErrorMessage = "住所は200文字以内で入力してください。")]
    public string? 住所 { get; set; }

    [Required(ErrorMessage = "誕生日は必須です。")]
    [DataType(DataType.Date, ErrorMessage = "誕生日は日付を入力してください。")]
    public DateTime 誕生日 { get; set; }

    [Required]
    public int Version { get; set; }
}
```

通常リポジトリーはDBアクセスを担当します。
今回のデモでは下記のように、DBの代わりにBindingList<利用者Model>を利用してます。

```csharp
public class 利用者Repository : I利用者Repository
{
    private BindingList<利用者Model> _users = new();

    public 利用者Repository()
    {
        // 初期データ
        _users.Add(new 利用者Model { ID = 1, 利用者名 = "山田太郎", 住所 = "東京都", 誕生日 = new DateTime(1990, 1, 1), Version = 1 });
        _users.Add(new 利用者Model { ID = 2, 利用者名 = "鈴木花子", 住所 = "大阪府", 誕生日 = new DateTime(1995, 5, 5), Version = 1 });
        _users.Add(new 利用者Model { ID = 3, 利用者名 = "佐藤次郎", 住所 = "北海道", 誕生日 = new DateTime(1988, 8, 8), Version = 1 });
    }

    public bool Edit(利用者Model user)
    {
        var index = _users.FindIndex(u => u.ID == user.ID && u.Version == user.Version);
        if (index < 0) return false;
        user.Version++;
        _users[index] = user;
        return true;
    }
```

### (2) 🔷 View の役割
WinFormsでは一般にFormのこと。
ただしMVPではFormの中にビジネスロジックを直接書かず、プレゼンターへ通知する。

```csharp
public interface IView利用者
{
    event EventHandler AddNewEvent;   // 「追加を要求されたときの振る舞い」がある
    event EventHandler EditEvent;
    event EventHandler DeleteEvent;
    event EventHandler SaveEvent;
    event EventHandler CancelEvent;
}
```

この `IView利用者` を実装した `利用者View` クラスは、以下のようにイベント発行を行います：

```csharp
this.Button追加.Click += delegate
{
    // Button追加.Click時にInterfaceで定義した「追加を要求されたときの振る舞い」を起動する
    AddNewEvent?.Invoke(this, EventArgs.Empty);
    Panel操作.Visible = true;
    this.Button削除.Visible = false;
};
```

### (3) 🔷 Presenter の役割
View からのイベント通知を受け取り、Model を操作し、結果を View に反映させます。

```csharp
public class 利用者Presenter
{
    public 利用者Presenter(IView利用者 view, I利用者Repository repository)
    {
        // 「追加を要求されたときの振る舞い」とは「AddNew利用者」である
        _view.AddNewEvent += AddNew利用者;
        _view.EditEvent += LoadSelected利用者ToEdit;
        _view.DeleteEvent += Delete利用者;
        _view.SaveEvent += Save利用者;
        _view.CancelEvent += Cancel利用者;

        _view.Set利用者ListBindingSource(_bindingSource利用者);

        LoadAll利用者();
        _view.Show();
    }
```
Presenter は Model の戻り値を元に View ｈメッセージを返したり、状態を反映させます：

```csharp
// ここで初めて、「追加を要求されたときの振る舞い」が実装される
private void AddNew利用者(object? sender, EventArgs e)
{
    // 追加時の初期値
    _view.IsEdit = false;

    CleanViewFields();
}
```

### (4) 🔷 Model / View / Presenter の呼び出し方

```csharp
[STAThread]
static void Main()
{
    IView利用者 view = new 利用者View();
    I利用者Repository repo = new 利用者Repository();
    
    _ = new 利用者Presenter(view, repo);

    Application.Run((Form)view);
}
```
:::note
IView利用者 view = new 利用者View();

→ これは「変数はインターフェース型で宣言し、実体は具体クラスで生成する」という設計の例です。
つまり、「実装にではなく抽象に依存する（Depend on abstractions）」という原則に従った書き方です。
:::

### (5) 🔷 プロジェクト全体のクラスダイアグラム

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/0522cea2-d4ff-4a97-ab5f-26972ce9159d.png)








## ５．Presenterの自動単体テスト（Moq使用）
MVPパターンの最大のメリットは、PresenterがViewやModelから疎結合であるため、**単体テストが非常に容易になる**ことです。

### (1) テストプロジェクトの追加と実行イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546646/71e43f9a-37ec-477c-8bad-71751b24df12.png)



### (2) テストクラスの定義
以下のように、ViewとRepositoryをMockし、Presenterの動作のみを検証できます。

```csharp
[TestMethod]
public void LoadAll利用者_初期表示で全件取得される()
{
    var mockView = new Mock<IView利用者>();
    var mockRepo = new Mock<I利用者Repository>();

    // --------------------------------------------------------------------
    // 単体テストの基本的な考え方：
    //     実際のDBの値に依存しないように、モックを使ってテストする。
    //     （ ここでモックが返す値を定義している ）
    // --------------------------------------------------------------------
    var 利用者一覧 = new List<利用者Model>
    {
        new 利用者Model { ID = 1, 利用者名 = "山田太郎", Version = 1 },
        new 利用者Model { ID = 2, 利用者名 = "鈴木花子", Version = 1 }
    };

    mockRepo.Setup(r => r.GetAll()).Returns(利用者一覧);

    BindingSource? capturedBindingSource = null;
    mockView.Setup(v => v.Set利用者ListBindingSource(It.IsAny<BindingSource>()))
            .Callback<BindingSource>(bs => capturedBindingSource = bs);

    mockView.Setup(v => v.Show());

    // Act
    var presenter = new 利用者Presenter(mockView.Object, mockRepo.Object);

    // Assert
    Assert.IsNotNull(capturedBindingSource);
    var bs = capturedBindingSource;

    // 件数の確認
    Assert.AreEqual(2, bs.Count);
    // 1件目の利用者名の確認
    Assert.AreEqual("山田太郎", ((利用者Model)bs[0]!).利用者名);
}
```
このように、Presenterだけをテストすることで、UIなしでもアプリのロジックを検証できます。
他にも「登録成功」「削除成功」「排他ロック失敗」など、様々なケースをテストできます。


## ６．MVPのメリットまとめ

- **Viewの疎結合化** → テストや画面差し替えが容易
- **Presenterのテスト容易性** → Moqを使ったPresenter単体テストが実現可能
- **Modelの独立性** → ビジネスロジックをViewと切り離して管理

---

## ７．おわりに

MVPパターンは、WinFormsのようなデスクトップアプリケーションでも非常に有効なアーキテクチャです。
この記事が少しでもお役に立てれば嬉しく思います。