Controller は WinForms の Form_Load 拡張版だと理解する — ASP.NET MVC 5 業務SE 入門

みなさんこんにちは!ヒロポンです!!

今回は ASP.NET 生存ガイド連載・第2回 の本論記事。WinForms 業務SE がガチで詰まりやすいやつ!!の話。

ASP.NET MVC 5 のプロジェクトを開いて Controllers/ フォルダを覗いた瞬間に、「ここで何を書けばいいの??」「Form_Load の代わりってこと??」って固まったこと、ないっすか??

俺も最初は流通系SIer時代に Controller を見て同じだった。ファイル名が HomeController.cs みたいになってて、中に Index() という素っ気ないメソッドが1個だけある。「Form_Load 感覚で全部ここに書けばいいか」と思って DB アクセスもビジネスロジックも全部 Action に書いたら、コードレビューで Service 層に出して と指摘されて時間溶かした経験があります。

結論から言うと、Controller の Action は WinForms の Form_Load 拡張版です。違いは ①ステートレス(毎回呼ばれる短命な Form と思え)、②UI 直接書き換えじゃなく Model / ViewBag 経由で View に渡す、③HTTP メソッド(GET / POST)で挙動が変わる の 3点だけ

連載第1回(WinForms の Form と Razor View の対応関係)で「View 側」のマッピングを押さえたので、本記事は「Controller 側」のマッピング。第1回の 「HTML レイヤーか、ASP.NET レイヤーか」分離思考 が、第2回では 「Controller レイヤーか、View レイヤーか」分離思考 に進化します。

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 / ASP.NET MVC 5 環境で、Form_Load ↔ Controller Action の対応マップを中心に、ミニマム検証と**段階構築(ViewBag → Model → Service)**を実演します。コード7本掲載。

3行で結論:

  • Controller の Action は WinForms の Form_Load 拡張版(毎回呼ばれる短命な Form と思え)
  • 違いは3点だけ: ステートレス / Model 経由でデータを渡す / GET vs POST で挙動分岐
  • **詰まったら「Controller レイヤーか、View レイヤーか」**で原因切り分け → ミニマム検証で再現が最短ルート
目次

著者の体験 — Controller の責務分離で時間溶かした話

正直に書いておくと、俺が流通系SIer時代に最初に Controller を書いた時、Form_Load 感覚で Action に全部押し込んでいました。DB アクセスも、ビジネスロジックも、エラー判定も、ログ出力も全部 Action 内。

「動くから問題ないだろ」と思ってたら、コードレビューで Service 層に出して と指摘されたんですよね。理由を聞いて納得した:

  • 同じ SQL を別 Action でも使いたくなる → コピペで重複が増える
  • テストが書けない → Action は HTTP コンテキスト依存、単独でテストしづらい
  • 例外処理が散らばる → どの Action でも同じ try-catch を書くハメになる

転機は 「Controller の責務は『リクエスト受付 → Service 呼び出し → View 返却』の3段階に絞る」 と腹落ちした瞬間。Form_Load の責務(DB アクセス + UI 更新 + ロジック)を3つに分解して、Controller には呼び出しと整形だけを残す。

連載第1回で扱った HTML レイヤー / ASP.NET レイヤー の分離思考が、ここではController レイヤー / View レイヤー / Service レイヤー の3層分離に進化します。詰まった時の原因切り分けはこの3層のどこかを問う、というのが業務SE現場の判断軸っす。

対応マップ — Form_Load ↔ Controller Action の8観点

WinForms の Form_Load 周辺と Controller の Action 周辺の対応関係を、8観点でまとめたのがこんな感じ:

WinForms 概念 Controller Action の対応 違いのポイント
Form_Load イベント Index() Action(GET / 引数なし) Form_Load は Form クラス内、Action は Controller ファイル別
this.lblTitle.Text = "Hello" ViewBag.Title = "Hello" or Model のプロパティ UI 直接書き換え → Model / ViewBag 経由
btnSearch_Click(...) Search(string keyword) Action(POST) UI イベント → HTTP リクエスト
Form フィールド private List<T> _items Action 内ローカル変数 / Service 注入 Form は状態保持、Action はステートレス
dataGridView1.DataSource = list return View(list) で Model に List を渡す DataSource 直接 → return
MessageBox.Show("OK") TempData["Message"] = "OK" + RedirectToAction 同期ダイアログ → TempData + リダイレクトサイクル
this.Close() で Form を閉じる RedirectToAction("Index", "Home") UI 切替 → URL 移動
Form コンストラクタ Controller コンストラクタ(DI 注入受け口) DI コンテナの介在(連載第5回で深掘り)

ここから順に、コード対比で見ていきます。Controller レイヤーView レイヤーを意識しながら読むと頭に入りやすい。

対応1: Form_Load ↔ Index() Action

最小の Form_Load と、最小の Index() Action を並べてみます:

// ✅ WinForms 版: Form_Load で初期化処理
public partial class CustomerForm : Form
{
    private void CustomerForm_Load(object sender, EventArgs e)
    {
        lblTitle.Text = "顧客一覧";
        var customers = LoadFromDb();
        dataGridView1.DataSource = customers;
    }
}
// ✅ ASP.NET MVC 版: Index() Action で同等の処理
public class CustomerController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Title = "顧客一覧";
        var customers = LoadFromDb();
        return View(customers);   // Model として View に渡す
    }
}

ポイント:

  1. Form_Load の中身がそのまま Action の中身になる感覚で書ける
  2. lblTitle.Text 直接代入 → ViewBag.Title 経由
  3. dataGridView1.DataSource 直接代入 → return View(customers) で Model 渡し
  4. Action はメソッド名 = URL の一部/Customer/Index で呼ばれる)

業務系の安心ポイント: LoadFromDb() の中身は WinForms と同じコードで動く。DataAdapter / DataReader の知識はそのまま使えるので、新しく覚えるのは「Controller / View 分離」「Model 経由」の概念だけっす。ん?じゃあ Controller って Form_Load の置き場所変えただけじゃない??って気付いた瞬間に、心理的ハードルが一気に下がります。

対応2: btn_Click ↔ POST Action

ユーザーがボタンを押した時の処理は、WinForms ではイベントハンドラ、Controller では POST Action になります:

// ✅ WinForms 版: ボタンクリックでフォーム送信
public partial class SearchForm : Form
{
    private void btnSearch_Click(object sender, EventArgs e)
    {
        string keyword = textBoxName.Text;
        var results = customerService.Search(keyword);
        dataGridView1.DataSource = results;
    }
}
// ✅ ASP.NET MVC 版: POST Action で同等
public class CustomerController : Controller
{
    private readonly ICustomerService _service;

    public CustomerController(ICustomerService service)
    {
        _service = service;   // DI で注入(連載第5回で深掘り)
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Search(string keyword)
    {
        var results = _service.Search(keyword);
        return View("Index", results);
    }
}
@* ✅ View 側: フォーム POST 先を Action 名で指定 *@
@model IEnumerable<CustomerVm>

<form action="/Customer/Search" method="post">
    @Html.AntiForgeryToken()
    <input type="text" name="keyword" />
    <button type="submit">検索</button>
</form>

<table>
    <tbody>
        @foreach (var c in Model)
        {
            <tr><td>@c.Id</td><td>@c.Name</td></tr>
        }
    </tbody>
</table>

ポイント:

  • [HttpPost] 属性で POST 専用 Action を明示
  • <input name="keyword"> と Action パラメータ string keyword が自動マッピング(model binding)
  • [ValidateAntiForgeryToken] で CSRF 攻撃を防ぐ(業務系イントラでも入れる)
  • return View("Index", results) で同じ View をデータ差し替えで再表示

WinForms の「TextBox.Text を直接読む」感覚が、Razor では「<form> で POST → Controller の引数で受ける」になる。サーバとブラウザの境界が1回挟まる、というのが連載第1回からの継続テーマっす。

対応3: DataGridView.DataSource ↔ return View(model)

WinForms の DataGridView へのデータ流し込みと、Controller の Model 受け渡しの対比:

// ✅ WinForms 版: DataSource 直接代入
private void Form_Load(object sender, EventArgs e)
{
    var list = new List<CustomerVm>
    {
        new CustomerVm { Id = 1, Name = "サンプル商事" },
        new CustomerVm { Id = 2, Name = "山田工業" },
    };
    dataGridView1.DataSource = list;
}
// ✅ ASP.NET MVC 版: return View(model) で Model を渡す
public ActionResult Index()
{
    var list = new List<CustomerVm>
    {
        new CustomerVm { Id = 1, Name = "サンプル商事" },
        new CustomerVm { Id = 2, Name = "山田工業" },
    };
    return View(list);   // View へ List<T> を Model として渡す
}
@* ✅ View 側: @model で受け取って HTML テーブルに展開 *@
@model IEnumerable<CustomerVm>

<table>
    <thead><tr><th>ID</th><th>名前</th></tr></thead>
    <tbody>
        @foreach (var c in Model)
        {
            <tr><td>@c.Id</td><td>@c.Name</td></tr>
        }
    </tbody>
</table>

ポイント:

  • WinForms は 「DataSource = 直接代入」で UI が即更新
  • Razor は 「return View(model) → View 側で @foreach 展開」で HTML 生成
  • 結果的に表示される画面は同じだが、生成タイミングがサーバ側か UI スレッドかで違う

これも連載第1回で扱った「サーバとブラウザの境界」の延長。Controller でデータを準備、View で HTML 化、ブラウザに HTTP レスポンスとして返す、という3段階のフローっす。

対応4: MessageBox ↔ TempData + RedirectToAction(PRG パターン)

ユーザーへの「保存しました」メッセージ表示の対比:

// ✅ WinForms 版: MessageBox.Show で1行
private void btnSave_Click(object sender, EventArgs e)
{
    customerService.Save(currentItem);
    MessageBox.Show("保存しました");
}
// ✅ ASP.NET MVC 版: TempData + RedirectToAction(PRG パターン)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Save(CustomerVm model)
{
    _service.Save(model);
    TempData["Message"] = "保存しました";
    return RedirectToAction("Index");   // Post-Redirect-Get で F5 二重登録を予防
}

public ActionResult Index()
{
    // TempData は次のリクエストまで生存する → リダイレクト先で読める
    if (TempData["Message"] != null)
    {
        ViewBag.Message = TempData["Message"];
    }
    return View(_service.GetAll());
}
@* ✅ View 側: ViewBag.Message があれば表示 *@
@if (ViewBag.Message != null)
{
    <div class="alert alert-success">@ViewBag.Message</div>
}

ポイント:

  • TempData は次のリクエストまで生存する(RedirectToAction 先で読める)
  • PRG(Post-Redirect-Get)パターンで F5 リロード時の二重登録を予防
  • WinForms の同期ダイアログは無い、リダイレクトサイクルで擬似的に実現

業務系で保存 → メッセージ表示 → 一覧画面に戻るのフローを書く時の鉄板パターンっす。TempData を使わずに ViewBag で渡すと、リダイレクト後にデータが消えて表示されない罠があるので注意。

ミニマム検証 — 3段階の最小 Action

困ったらミニマム検証で行く、というのが連載通奏低音。Action の最小例を3段階で並べると、こんな感じになります:

// ✅ Step 1: 一番シンプル — Hello を返すだけ
public class HelloController : Controller
{
    public ActionResult Index()
    {
        return Content("Hello, World!");   // 文字列をそのまま返す
    }
}

// ✅ Step 2: 引数を受け取る Action
public ActionResult Greet(string name)
{
    // /Hello/Greet?name=Bob で呼ばれると "Hello, Bob!" を返す
    return Content($"Hello, {name}!");
}

// ✅ Step 3: Model を返す Action
public ActionResult Show()
{
    var model = new GreetingVm { Name = "World", Message = "Hello, MVC!" };
    return View(model);   // View 側で @Model.Name / @Model.Message を表示
}

困った時はこの Step 1(文字列直接) → Step 2(引数受け取り) → Step 3(Model 経由) の順で動作確認すると、原因切り分けが速い:

  • Step 1 で動かない → ルーティング or web.config 問題
  • Step 2 で動かない → model binding 問題(パラメータ名 typo 等)
  • Step 3 で動かない → View ファイル不在 or @model ディレクティブ漏れ

連載第1回の 生 HTML+CSS → Razor 式追加 の段階構築と同じ発想で、ViewBag だけ → Model 経由 → Service 注入 の段階で Action を進化させていくと、業務系チームの新人教育でもいい感じに腹落ちさせられます。

ハマりポイント — 実体験ベースの本番事故3点

1. Action に DB アクセス全部書いて MVC 責務分離違反(半日デバッガで追ってハマった)

流通系SIer時代に Form_Load 感覚で Action に SQL も BL もログも全部押し込んで、別 Action で同じ SQL を書こうとした時にコードレビューで Service 層への抽出を要求された事件。半日デバッガで追ってハマった末に Service クラスへ移したら、テストが書けるようになった上に保守工数も下がった。それ以来、業務系チームで 「Action は『リクエスト受付 → Service 呼び出し → View 返却』の3段階に絞る」 をルール化しました。

2. ViewBag 使いすぎて型安全性ゼロで落ちた(30分溶かした)

別案件で、画面表示用のデータを全部 ViewBag で渡していたら、View 側で @ViewBag.CutomerName(typo)と書いて実行時に null が返るだけで例外なしで詰まった事件。30分溶かした末に typo を発見、Model 化に書き換えてコンパイル時に typo 検出されるようにした。それ以来、ViewBag は Title のような補助情報だけ、主要データは Model 経由を業務系チーム規約に揃えました。

3. POST 後にリダイレクトせず F5 で二重登録(夕方の運用報告で気づいた)

POST Action の最後で return View() で同じ画面を再表示してたら、ユーザーがF5 リロードで「再送信しますか?」のダイアログが出て、間違えて OK 押すと二重登録される事故。夕方の運用報告で「同じ注文が2件入ってる」って報告で気づいたRedirectToAction("Index") で PRG パターンに書き換えて解決。書き込み Action の戻り値は基本 RedirectToActionを業務系チーム規約に入れた。

著者の現場メモ — Controller 設計の業務系ルール

流通系SIer時代に過去コードを grep -rnE "ActionResult|ViewBag|TempData" . で60箇所近くひっかけたら、Action に SQL 直書き・ViewBag 多用・PRG 未適用が混在してた。後輩と一緒に 3行ルール にまとめた:

  1. Action は3段階に絞る(リクエスト受付 → Service 呼び出し → View 返却・DB アクセスは Service 層)
  2. 主要データは Model 経由、ViewBag は補助情報のみ(型安全性で typo を実行前に検出)
  3. POST Action の戻り値は RedirectToAction で PRG パターン(F5 二重登録予防)

このルール化で、Controller 周りの責務分離違反 / typo / F5 事故が消えた。書き方を1パターンに揃えるだけで保守工数と事故率が両方下がるので、業務系チームにはおすすめのルールっす。

まとめ

状況 WinForms の対応 → Controller の答え
画面表示時の初期化 Form_LoadIndex() Action
ボタンクリック処理 btn_Click[HttpPost] public ActionResult Search(...)
データ表示 DataGridView.DataSource = listreturn View(list)
保存後のメッセージ MessageBox.Show("OK")TempData["Message"] = "OK" + RedirectToAction
画面遷移 this.Close()RedirectToAction("Index", "Home")
データ受け渡し コードビハインドで参照 → Model / ViewBag 経由
状態保持 Form フィールド
F5 リロード対策 n/a(WinForms は無関係)

WinForms 業務SE が ASP.NET MVC 5 に移行する時の Controller 設計は、「Action は Form_Load 拡張版」「Controller / View / Service の3層分離」「PRG パターン」 の3点で9割困らなくなります。次回(連載第3回)は「ルーティング — WinForms の Form 切替との対応」を扱うので、Controller の呼び出し側がどう動くかが見えてきます。

よくある質問

Q1. Controller の Action と WinForms の Form_Load は何が違いますか?

A. 違いは大きく3点です。①Action はステートレス(HTTP リクエスト1往復で完結する短命な Form と思え)、②UI を直接書き換えるのではなく Model / ViewBag 経由で View に渡す、③HTTP メソッド(GET / POST)で挙動と用途が分かれる。逆にイベント発火のトリガーや「データ取得 → 画面に反映」の流れは Form_Load の延長で考えてOKです。WinForms 知識を Controller に持ち込めるので、心理的ハードルは低めに見積もれます。

Q2. Controller に DB アクセスを直接書いてもいいですか?

A. 技術的にはできますが、業務系の保守規模では Service 層に切り出すのが推奨です。WinForms の Form_Load 感覚で Controller に SQL を書くと、テスト困難・例外処理の散在・複数 Action からの重複呼び出しという保守性の問題が出てきます。最初は Action 内に直書きでも構いませんが、同じ SQL が2箇所目に出てきた瞬間に Service クラスへ抽出するのが業務系の判断軸です。

Q3. Model と ViewModel の違いは?

A. Model(Domain Model)は DB のテーブル構造に近いビジネスデータの型、ViewModel は画面表示用に整形した型です。一覧画面で Customer テーブルの全カラムを表示する単純なケースなら Model 直渡しで OK ですが、「集計値・フォーマット済み文字列・選択肢リスト」など画面固有の情報が入ってくると ViewModel に切り出すのが綺麗です。Controller の役割は Model → ViewModel の整形 + View への引き渡しと覚えると整理しやすいです。

Q4. ViewBag と Model はどう使い分ければいい?

A. 原則 Model に統一が業務系の本命です。ViewBag は型安全性が効かない(実行時に typo で null が返る)ので、フォーム入力値や一覧データなど主要データは Model 経由が安全。ViewBag.Title のようにレイアウト専用の補助データに留めるのが現代的な判断軸。Model を作るのが面倒に感じても、後段のリファクタコストを考えると最初から Model 化が結局早いです。

Q5. POST と GET の使い分けは?

A. 読み込み(一覧表示・検索)は GET、書き込み(登録・更新・削除)は POST が原則です。POST 後は RedirectToAction で GET にリダイレクトする PRG(Post-Redirect-Get)パターンを使うと、F5 リロードで二重登録される事故を予防できます。WinForms には HTTP メソッド概念がないので最初は意識しづらいですが、ブラウザのキャッシュ・履歴・F5挙動の前提だと考えると腹落ちします。

ここまでで Controller / Action の対応マップ・3層分離思考・PRG パターンは押さえた。次回は「ルーティング」で Controller の呼び出し方を扱うので、Controller の周辺がもう一段見えてきます。WinForms 関連の隣接トピックも貼っておきます。

関連記事

ASP.NET 生存ガイド・連載目次

本記事は WinForms 業務SE のための ASP.NET 生存ガイド 全10回の 第2回 です。

  • 第1回: WinForms の Form と Razor View の対応関係を業務SE が一日で腹落ちさせる
  • 第2回(本記事): Controller は WinForms の Form_Load 拡張版だと理解する ← イマココ
  • 第3回: ルーティング — WinForms の Form 切替との対応(公開予定)
  • 第4回: ORM 3択 — EF6 / Dapper / ADO.NET の業務SE 視点比較(公開予定)
  • 第5回: DI は業務系で必要か — 入れない派の論点(公開予定)
  • 第6回: 業務イントラの認証 — Windows認証 / Forms認証 / Cookie(公開予定)
  • 第7回: CSS が効かない時のチェックリスト10項目(公開予定)
  • 第8回: IIS デプロイ — オンプレ業務系の現実(公開予定)
  • 第9回: ASP.NET MVC 5 トラブルシューティング・チェックリスト20項目(公開予定)
  • 目次(最終回): WinForms 業務SE のための ASP.NET 生存ガイド・全体目次(公開予定)

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次