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現場の判断軸っす。

WinForms Form_Load vs ASP.NET MVC Controller Action のライフサイクル対応

対応マップ— 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仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次