ASP.NET MVCによるデータバインドとモデルバインドの基礎を解説する。プロジェクト名はTestとする。
モデルから作成する。ModelsフォルダにAInt.csを作成し、以下の内容にする。
namespace Test.Models{ public class AInt { public static int v = 0; }}次に、[Views]>[Home]フォルダのIndex.cshtmlを以下の内容にする。
@{ ViewData["Title"] = "Home Page"; }@model Test.Models.AInt<div> @AInt.v</div>実行すると以下のように表示される。
vの初期値を変えて、ビルドして実行すると変えた値になる。モデルの値がビューに紐付けされているのである。これがデータバインド、またはデータバインディングである。
モデル、すなわちAInt.vの値をブラウザから変えてみたい。Index.cshtmlの内容を以下のように変更する。
@{ ViewData["Title"] = "Home Page"; }@model Test.Models.AInt<form method="post"> <div class="form-group"> @AInt.v <input type="submit" value="+" class="btn btn-primary" /> </div></form>これで実行すると「+」ボタンがbootstrapスタイルの追加される。このままでは、ボタンを押しても再度読み込まれるだけで値は変わらない。
ControllersフォルダのHomeController.csのIndexメソッドを以下のように変更する。
[HttpGet] public IActionResult Index() { _logger.LogInformation("Info: Index()"); return View(); } [HttpPost] public IActionResult Index(int noUse = 0) { _logger.LogInformation("Info: Index() by post"); AInt.v++; return View(); }メソッドに属性を追加することによって、Get用とPost用の2つに分けた。これでボタンを押すたびに値がカウントアップする。ログ出力も参考になるだろう。
ボタンを押してカウントアップした後にリロードすると2番目のメソッドが実行され、カウントアップする。
一方、Privacyに移動後、Indexページに移動してリロードした場合はカウントアップしない。
Post用のメソッドをコメントアウトしてビルド・実行し、ボタンを押すと「HTTP ERROR 405」エラーがでる。
GetとPost、submitの関係について解説する。通常ブラウザはWebサーバーにURLを記述したリクエスト(要求)を送信し、Webサーバーはレスポンス(応答)を返すことでWebページを表示する。submitはURL以外のデータをサーバーに送信したい時に使用する。input要素でtype属性にsubmitを設定すると、ボタンを押したときにデータ送信を行い、同時に通常のリクエストからの処理も行う。Getはデータ送信をURLに追加する。「http://localhost/?a=1&b=3」といった具合で「?」以降が送信データである。この問題点はURLにパスワードなどを送ることができないことと、URLは全体の長さに限りがあることである。Postはデータ送信の内容を本文に埋め込む。ページの「<html>~</html>」のようなテキストを送信時にも送るということだ。
Index.cshtmlの「<form method="post">」はPostで送信することを示す。HomeController.cs の [HttpGet] 属性はGet時に処理するという意味であるが、通常のリクエストにも対応する。[HttpPost] 属性はPost時に処理するという意味だ。2つ目のIndexメソッドにNoUse引数があるのは、属性が違うだけでは違うメソッドとしてみなされないため、違うメソッドにしてコンパイルエラーを出さないためである。
Getの引数を処理するには、例として引数が「a=1&b=3」の場合は、メソッド引数を「Index(string a, string b)」とすれば、引数に送信データが格納される。Postの場合も、同じような方法である。
ブラウザを2つ開き、一方で「+」ボタンを押すと内部の値はカウントアップする。もう一方のブラウザには反映される前の値が表示され、「+」ボタンを押すと2増えることになる。今回は簡単なテストなので困らないが、改良が必要なら、値を送信し期待どおりではない場合はその旨表示処理が必要になる。
また、「AInt.v++」のところは更新処理であるが、同時アクセスが多い場合データ不整合が起こる可能性がある。対処方法としてはHomeControllerクラスの2つめのIndexメソッドを以下のように変更する。
[HttpPost] public IActionResult Index(int NoUse = 0) { int lv = System.Threading.Interlocked.Increment(ref AInt.v); _logger.LogInformation("Info: Index() by post. AInt.v=" + lv); return View(); }これでカウントアップ(インクリメント)確実でログはカウントアップ直後の値だが、表示がカウントアップ直後の値になるとは限らない。より凝った設計にするならコントローラーの値をページに出力する専用ページが必要かもしれない。
vがオブジェクトであれば以下のような書き方ができる。
[HttpPost] public IActionResult Index(int NoUse = 0) { lock (AInt.v) { // ここで同期中にAInt.vの処理を行う。 } _logger.LogInformation("Info: Index() by post. AInt.v=" + AInt.v); return View(); }lock の引数は参照型でなければならないため、値型ではこの方法はエラーになるのである。
カウントダウン用のボタンを追加して、ボタンを2つに増やす。Index.cshtmlを以下のように変更する。
@{ ViewData["Title"] = "Home Page"; }@model Test.Models.AInt<form method="post"> <div class="form-group"> <input type="submit" name="btn" value="-" class="btn btn-primary" /> @AInt.v <input type="submit" name="btn" value="+" class="btn btn-primary" /> </div></form>ボタンを区別する仕掛けのため、name属性を追加している。value属性は表示テキストであるが、内容は異なっているので、これでコントローラーで処理を分岐できる。コントローラーのコードが同じままなら、「-」ボタンを押してもカウントアップしてしまう。HomeController.csのポスト用Indexメソッドを以下のように変更する。
[HttpPost] public IActionResult Index(string btn, int NoUse = 0) { if (btn == "+") { AInt.v++; } else if (btn == "-") { AInt.v--; } _logger.LogInformation( "Info: Index() by post. v=" + AInt.v + ", btn=" + btn); return View(); }引数に「string btn」が追加された。引数の変数名はボタンのname属性の内容「btn」である。引数の値はvalueの内容となるので、それが"+"であればカウントアップする処理をする、"-"であればカウントダウンする処理をする。
クライアントからデータを取得し、コントローラーのメソッド引数に渡すことをモデルバインドという。
データ送信でテキストを送る。Views>Homeフォルダに新しくText.cshtmlを作成する。以下のコードをコピーする。
@{ ViewData["Title"] = "Home Text"; }@model Test.Models.AInt<form method="post"> <div class="form-group"> <input type="text" name="v" value=@AInt.v /> <input type="submit" name="btn" value="更新" class="btn btn-primary" /> </div></form>トップページからナビゲーションリンクを追加しよう。Views>Homeフォルダにある_Layout.cshtmlの「navbar-nav」が設定されているタグの内容を以下に変更する。
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Text">Text</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> </ul> </div>変更部分は太字で示した。ビルドして実行すると、Textページへのリンクが追加されている。コントローラーで表示するメソッドを用意していないので、「HTTP ERROR 404 」エラーが発生するという段階である。HomeController.csに以下のメソッドを追加する。テキストボックスにはnameに"v"を設定しているので、引数は「string v」としてモデルバインドする。
[HttpGet] public IActionResult Text() { _logger.LogInformation("Info: Text() AInt.v=" + AInt.v); return View(); } [HttpPost] public IActionResult Text(string v,int NoUse = 0) { if (!string.IsNullOrEmpty(v)) { AInt.v = int.Parse(v); } _logger.LogInformation("Info: Text() by post AInt.v=" + AInt.v); return View(); }実行して動作を確認する。
更新ボタンを押すと、テキストボックスの内容がAInt.vに更新される。英字や小数、intに納まらないテキストであった場合は、int.Parseメソッド実行時に例外が発生し、エラーページが表示される。
エラーページを出ないようにしたい。今回は入力を検証も行う。検証に必要な修正のため、大掛かりな書き換えを行う。AInt.csのクラスを以下のようにする。
public class AInt { public static int s_v = 0; public AInt() { this.v = s_v; } [Range(int.MinValue, int.MaxValue)] public int v { get; set; } }モデルはインスタンスのプロパティにアクセスできるようにする。プロパティにはRangeを付けることによって検証を行う。代わりに、静的変数s_vでは永続的な値を保持する。
HomeController.csのIndexメソッドとTextメソッドを以下のようにする。
[HttpGet] public IActionResult Index() { _logger.LogInformation("Info: Index() AInt.v=" + AInt.s_v); return View(new AInt()); } [HttpPost] public IActionResult Index(string btn, int NoUse = 0) { if (btn == "+") { AInt.s_v++; } else if (btn == "-") { AInt.s_v--; } _logger.LogInformation( "Info: Index() by post. v=" + AInt.s_v + ", btn=" + btn); return View(new AInt()); } [HttpGet] public IActionResult Text() { _logger.LogInformation("Info: Text() AInt.v=" + AInt.s_v); return View(new AInt()); } [HttpPost] public IActionResult Text(string v,int NoUse = 0) { if (!string.IsNullOrEmpty(v)) { AInt.s_v = int.Parse(v); } _logger.LogInformation("Info: Text() by post AInt.v=" + AInt.s_v); return View(new AInt()); }ViewメソッドにはAIntをインスタンス化したものを渡している。また、モデルの静的変数はs_vに変えたので、こちらでも変数を変えている。変数を変えるのはTextメソッドだけでも良いが、Indexメソッドと統一のため両方変更した。
続いてIndex.cshtmlは「@AInt.v」を変える。静的変数にするなら「@AInt.s_v」、インスタンス変数にするなら「@Html.Value("v")」になる。インスタンス変数の場合を示す。
@{ ViewData["Title"] = "Home Page"; }@model Test.Models.AInt<form method="post"> <div class="form-group"> <input type="submit" name="btn" value="-" class="btn btn-primary" /> @Html.Value("v") <input type="submit" name="btn" value="+" class="btn btn-primary" /> </div></form>最後に、Text.cshtmlは以下のようにする。
@{ ViewData["Title"] = "Home Text"; }@model Test.Models.AInt@section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }}<form method="post"> <div class="form-group"> <input asp-for="v" /> <span asp-validation-for="v" class="text-danger"></span> </div> <input type="submit" name="btn" value="更新" class="btn btn-primary" /></form>「@section Scripts」については、[Views]>[Shared]フォルダ内の「_Layout.cshtml」の「@RenderSection("Scripts", required: false)」に「_ValidationScriptsPartial.cshtml」の中身を埋め込む処理である。これでjQueryの検証ライブラリを使用可能なる。
Tex.cshtmlに戻って「<input asp-for="v" />」のところは、インスタンス変数の v フィールドに対してタグヘルパーを利用している。入力ボックスに値の表示と検証を一度に行える。これはDRY ("Don't Repeat Yourself")の設計である。「<span asp-validation-for="v" class="text-danger"></span>」は検証で引っかかったときに、「The field v must be between -2147483648 and 2147483647.」というエラーテキストを表示する。
ここからは、モデルのAInt.csを少し変えることができる。一例として、クラスを以下のようにする。
public class AInt { public static int s_v = 0; [Range(int.MinValue, int.MaxValue)] public int v { get { return s_v; } } }変更点としては
1.インスタンスのコンストラクタを削除
2.プロパティのsetを削除。→不用意を書き換えをコンパイルエラーで防ぐ。
3.プロパティのgetでは静的変数を返す。→より新しい時点の値を返すが、処理によっては途中で値が変わる。
というところがある。コントローラーの方に手を加えるなら、シングルトンパターンを採用することもできるだろう。