みなさんこんにちは!ヒロポンです!!
今回は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仲間いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント