情報応用演習Ⅰ(2024)

【T4b】モデルとビューの連携(後編)(4/6)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T4b
ソリューション名PIT4
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

4b-4. 属性を使った入力値の制限とチェック

次に体重や身長の入力値の範囲を制限してみよう.BMIの計算では「身長(cm)」の二乗で除する部分があるが, 仮にこの入力欄にゼロを入力されてしまうと,BMIの計算時にゼロ除算が発生してしまう. このため身長や体重に下限と上限を設けることにしよう.身長に関しては前述の通りゼロが入力できないように 下限を1にしておこう.また体重に関しても負数が入力されてしまうのは望ましくないので下限をゼロとする. 上限に関してはこの場合は必ずしも必要とはいえないが練習として設けてみることにしよう. 記録によれば身長の世界記録は272cm,体重の世界記録は 635kgとのことなので,それぞれの上限は300cm,700kgとしよう. モデルクラスに_のように追記してみよう.属性を指定するときは指定することができる値がサジェストされる. 何が指定可能なのかを知るうえで有用であるためサジェストの内容をよく見ておくとよいだろう

「身長と体重」クラスへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class HealthInfo
{
    // 体重(kg)
    [Display(Name = "体重(kg)")]
    [Range(0, 700)]
    public double Weight { get; set; }

    // 身長(cm)
    [Display(Name = "身長(cm)")]
    [Range(1, 300)]
    public double Height { get; set; }

    // BMIの計算をする読み取り専用プロパティ
    public double BMI => Weight / Math.Pow(Height / 100, 2);
}

ここまで入力したら実行してみよう. それぞれの入力欄に指定した範囲外の数値を指定すると _のようなメッセージが表示されるはずである.

実行結果

ちなみに属性の指定順序は特に規定されていない.また_では複数の属性を各々[]で囲って 指定しているが,[Display(Name = "体重(kg)"), Range(0, 700)]のようにカンマ区切りで指定することも可能である.

つぎにサーバー側での入力値のチェックを実装しよう.ここまでの入力値のチェックはJavaScriptを使って, クライアント側,すなわちウェブブラウザ側で行われている.制限に引っ掛かる値が入力されている場合は 「送信」ボタンを押してもサーバ側へのPOSTリクエストが実行されることはないが,これは誤った入力内容に対する絶対的な防壁としては実は使用できない. 仮に(現在ではあまりないことだが)ウェブブラウザ側でJavaScriptが無効化されている場合は, 入力値が不正であっても送信が行われてしまう.このことを確かめてみよう.

フォームの送信を処理するためには,そのためのアクションメソッドが必要である. 今回はビュー内のフォーム( form 要素)で<form asp-for="Index">...</form>という指定をしているので, 「送信」ボタンを押した際にはHomeコントローラーのIndexアクションに対してPOSTリクエストが発生する. Homeコントローラーには元から定義されている GET用のアクションメソッド は存在するが, フォームの入力を受け付けるための POST用のアクションメソッド はまだ用意していない.まずはこれを定義しよう. Controllers/HomeController.cs に_に示す メソッドを 追記する (既存のIndex()メソッドを書き換えてはならない).

Homeコントローラーへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Indexアクション(GET用)            // ←このコメント行を追記しておくとこの後の作業が分かりやすい.
public IActionResult Index()         //
{                                    // 既存,こちらは書き換えない!
    return View(new HealthInfo());   //
}                                    //

// Indexアクション(POST用)
[HttpPost]
public IActionResult Index(HealthInfo healthInfo)
{
    return View(healthInfo);         // 入力されたデータを維持する(後述)
}

強調した,POST用のIndex()メソッドに着目する.チュートリアル【T1c】のCalcBMIメソッドと異なり, このメソッドでは double 型のweightheightというような,対応するフォームのデータ項目名に合わせた引数を使っていない. その代わり先ほどから定義しているHealthInfoクラスの引数を使用している. モデルバインディングを使用している場合,フォームの入力値を個別の引数として受け取る代わりに, このように型がモデルクラスとなっている引数として受け取ることが可能である. このアクションメソッドでは今のところは何もすることはないので,引数として渡された,すなわちフォームで送信された モデルクラスのインスタンスをView()メソッドに渡して,対応するビューを表示している (11行目のView()メソッドの呼び出し).

ここまで書けたら,どのタイミングで_に示すどちらのメソッドが動作しているのかを 検証するため,_に示す通り,_の3行目と10行目のメソッドの始まりの中カッコ{にブレークポイント (参考:プログラミング演習Ⅲ(2023; 金澤クラス)第02回)を設置してから実行してみよう. すると_のようにウェブブラウザは読み込み中となり,Visual Studio を見ると_のように GET用のIndexアクション(引数なしのほう)が作動していることが分かるはずである.このまま Visual Studio の「続行」ボタンを クリックすると,ウェブブラウザ側で無事にフォームが表示されるはずである.ここで_のように 入力欄にエラーにならない適当な 値を入力してから 「送信」をクリックしてみよう.すると_に示す通り,先ほどと同様に実行が一時停止し 今度はPOST用のIndexアクション(引数ありのほう)が作動していることが分かるはずである.

ブレークポイントを設置しての実行

このまま「続行」ボタンを押すとウェブブラウザでの表示が行われる.入力欄には先ほどの入力値が入力されたままの 状態になっているはずである. この状態で,こんどは 入力欄を空にするかエラーとなる範囲の値を入力して 「送信」ボタンをクリックしてみよう.先ほどまでのように エラーメッセージが表示されるはずであるが, 今度はブレークポイントに引っ掛かっていないことが分かるだろうか . この入力チェックは前述のとおりウェブブラウザ側で行われており, エラーがある場合はそもそもサーバー側に何も送信されないようになっている . このチェックはウェブブラウザ側で動作するプログラム(=JavaScript)を用いて実現されているため,当然のことながら ウェブブラウザのJavaScript機能をオフにすれば機能しなくなる .ためしに以下の手順でJavaScript機能をオフにしてみよう. ウェブブラウザのウィンドウで新しくタブを開きアドレスバーにabout:configと入力してEnterキーを押下する(__). 「危険を承知の上で使用する」ボタンをクリックし,検索欄に javascript.enabled を検索し設定値をfalseにする(_).

一時的にJavaScriptを無効化して送信する

この状態でフォームが表示されているタブのアドレスバーでEnterキーを押して再読み込みしてみよう. すると,_と同様にGET用のIndexアクション(引数なしのほう)が作動していることが分かるはずである. このまま Visual Studio の「続行」ボタンをクリックして,今度は入力欄を空にするかエラーとなる範囲の値を入力して 「送信」ボタンを押してみよう.すると_に示す通り POST用のIndexアクション(引数ありのほう)が作動していることが分かるはずである. 先ほど,つまりウェブブラウザのJavaScript機能が有効だった時点では,入力エラーがある状態で「送信」ボタンを押しても, ブレークポイントに引っ掛かからない,すなわちサーバ側ではいかなる処理も実行されてはいなかった. しかし今は JavaScript機能をオフにしたため,先ほどまで異なり入力内容にエラーがあってもサーバー側への送信が行われ, POST用のIndexアクションが作動した ,というわけである.

このように,フォームからサーバ側に送られてきているデータ(≒モデルクラスのインスタンス)には,常に正しい値が入力されているとは限らない,ということを覚えておこう .つまり送られたデータは常にサーバ側でもチェックをする必要があるということである.幸い,不正な入力値が送信されたことをサーバ側,つまり アクションメソッド内の処理で検知するための機能が ASP.NET Core には備わっている.この機能も試してみることにしよう. プログラムをいったん終了して,POST用のIndexアクションに_に示す内容を追記し, また Views/Home/Index.cshtml に_に示す内容を追記しよう. なお先ほどのブレークポイントは設置したままにしておくこと

HomeControllerへの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Indexアクション(POST用)
[HttpPost]
public IActionResult Index(HealthInfo healthInfo)
{
    if (ModelState.IsValid)
    {
        ViewData["result"] = $"正しい入力 - BMI: {healthInfo.BMI}";
    }
    else
    {
        ViewData["result"] = "誤った入力";
    }
    
    return View(healthInfo); // 入力されたデータを維持する(後述)
}
Views/Home/Index.cshtmlへの追記内容1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@model HealthInfo

@{
    ViewData["Title"] = "Home Page";
    
    if (Model is null) throw new ArgumentNullException(nameof(Model)); // ad-hoc!: 非null保証のための回避策
}

@if (ViewData["result"] != null) 
{
    <p>投稿結果: @ViewData["result"]</p>
}

<form asp-action="Index">
    <label asp-for="@Model.Weight"></label>: <input asp-for="@Model.Weight" />
    <span asp-validation-for="@Model.Weight"></span>
    <br />
    
    <label asp-for="@Model.Height"></label>: <input asp-for="@Model.Height" />
    <span asp-validation-for="@Model.Height"></span>
    <br />
    
    <input type="submit" value="送信" />
</form>

@section Scripts
{
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

_の4行目ではModelState.IsValidという bool 型のプロパティを使用している. フォームへの入力値が正しい(≒制限に引っ掛からない)場合は,このプロパティの値は true となる. 実際のシステムなどではこのようにしてサーバ側に送られた入力内容が正しいかどうかを常にチェックするべきである. つまり誤った入力値が送られた場合にはそれ以降の処理はキャンセルされなければならない. 本節で説明したクライアント側でのチェックやアンチフォージェリトークン(第03回参照)などによって, あるていどは予防が可能ではあるが,「HTMLのフォーム」と「そのフォームからのPOSTを処理するプログラム」は本質的には独立したものであり 誤ったフォーマットの入力が行われる可能性は常にあると考えるべきである.

ここでは入力が正しく行われた場合にはViewDataの "result" というキーに"正しい入力 - BMI: 計算したBMI値", 誤った入力が行われた場合には"誤った入力"という文字列を格納して,ビュー側では_の 9~12行目でそのデータを表示している.

ここまで書けたら実行してみよう.先ほどと同様にGET用のIndexアクションで一時停止するので Visual Studio の「」ボタンをクリックして続行し,フォームを表示させよう. そして 入力欄を空にするかエラーとなる範囲の値を入力して 「送信」ボタンをクリックしてみよう. いまだウェブブラウザのJavaScript機能をオフにしたままであるので,入力に誤りがある場合であってもサーバー側への送信が行われ POST用のIndexアクションが作動するはずである(_).この状態で, if文の条件文の .IsValid の部分にマウスカーソルを重ねてみよう .すると_のように このプロパティが false つまり入力内容に誤りがあることが検出できていることが分かるはずである. この状態で3回ていどステップオーバー(F10キー)すると,_に示すようにif文がスキップされelse節の方が実行されることが分かるだろう. 以上を確認したら,Visual Studio の「続行」ボタンをクリックして続行する.すると _に示すように 投稿結果: 誤った入力」という表示と,それから誤りのある各入力欄の隣に 「値 "" は正しくありません.」といった表示が現れている ことが分かるはずである.

つぎに 入力欄にエラーにならない適当な値を入力してから 「送信」をクリックしてみよう(_). ブレークポイントで停止するので,先ほど同じ要領で .IsValid がどのように評価されているかを確認してみよう. 今回は入力内容が正しいので,_に示すように このプロパティはtrueと評価されているはずである . また,この状態で3回ていどステップオーバー(F10キー)すると,先ほどは異なり_に示すように if文の内部の処理が実行されることが分かるはずである.そして,このまま「続行」ボタンをクリックして続行すると, _に示すようにこんどは投稿結果: 正しい入力 - BMI: 数値という表示が現れているはずである.

実行結果

「送信」ボタンをクリックしたときの動作をまとめておこう.

  • ウェブブラウザのJavaScript機能が有効な場合
    • 入力が正しい場合:
      • サーバー側へ送信が行われPOST用のアクションメソッドが動作する.
    • 入力に誤りがある場合:
      • サーバー側へ送信が行われない(ウェブブラウザ側でチェックが行われるため).
  • ウェブブラウザのJavaScript機能が無効な場合
    • 入力が正しい場合:
      • サーバー側へ送信が行われPOST用のアクションメソッドが動作する.
    • 入力に誤りがある場合:
      • サーバー側へ送信が行われPOST用のアクションメソッドが動作する.

またウェブブラウザにおけるJavaScript機能の有効/無効に関係なく,

  • フォームへの入力内容が正しい場合
    • アクションメソッドでは ModelState.IsValid が true となる.
  • フォームへの入力内容に誤りがある場合
    • アクションメソッドでは ModelState.IsValid が false となる.

ここまでを理解することができたら,もう一度about:configを開きjavascript.enabledの設定値を true に戻しておこう(_). また先ほどのブレークポイントは解除しておこう(_).

実行結果
Last updated on 2024-04-24
Published on 2024-04-24

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.