みなさんこんにちは!ヒロポンです!!
今回は 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 に渡す
}
}
ポイント:
- Form_Load の中身がそのまま Action の中身になる感覚で書ける
lblTitle.Text直接代入 →ViewBag.Title経由dataGridView1.DataSource直接代入 →return View(customers)で Model 渡し- 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行ルール にまとめた:
- Action は3段階に絞る(リクエスト受付 → Service 呼び出し → View 返却・DB アクセスは Service 層)
- 主要データは Model 経由、ViewBag は補助情報のみ(型安全性で typo を実行前に検出)
- POST Action の戻り値は RedirectToAction で PRG パターン(F5 二重登録予防)
このルール化で、Controller 周りの責務分離違反 / typo / F5 事故が消えた。書き方を1パターンに揃えるだけで保守工数と事故率が両方下がるので、業務系チームにはおすすめのルールっす。
まとめ
| 状況 | WinForms の対応 → Controller の答え |
|---|---|
| 画面表示時の初期化 | Form_Load → Index() Action |
| ボタンクリック処理 | btn_Click → [HttpPost] public ActionResult Search(...) |
| データ表示 | DataGridView.DataSource = list → return 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 関連の隣接トピックも貼っておきます。
関連記事
- WinForms の Form と Razor View の対応関係を業務SE が一日で腹落ちさせる — 連載第1回・View 側のマッピングを先に押さえたい時に効く
- C# のコールバック・デリゲート・イベントの違いを業務SEが30分で腹落ちさせる — Controller の Action とイベントハンドラの概念対応を整理する時に効く
- 正社員2年で独立した時に最初にやった3つのこと(綺麗事抜き) — 同じ流通系SIer出身として技術キャッチアップの実例を知りたい時に効く
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仲間いたら、どんどんシェア待ってるぜ!!


コメント