WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

int 型プロパティの検証、エラーメッセージ

by WebSurfer 24. March 2019 16:06

ASP.NET MVC で、モデルのプロパティが int 型の場合の検証とエラーメッセージに関する注意点を書きます。

検証、エラーメッセージ

モデルのプロパティが int 型の場合、クライアント側の検証を有効にしておけば、RequiredAttribute, RegularExpressionAttribute は付与しなくても input 要素には data-val-required="xxx フィールドが必要です。", data-val-number="フィールド xxx には数字を指定してください。" という属性が付与され、入力に応じてそれらのエラーメッセージが表示されます。

プロパティに RequiredAttribute が付与され ErrorMessage が設定されている場合は、data-val-required 属性に設定される文字列が ErrorMessage に置き換わります。(ちなみに、プロパティが int? 型の場合は data-val-required 属性そのものが付与されません)

上記は TextBoxFor, EditorFor いずれを使っても同じです。

ただし、EditorFor を使うと input 要素の type 属性が "number" となるので、それによりブラウザ依存の動きが出るのに要注意です。(TextBoxFor を使った場合は type 属性は "text" となります)

input 要素の type 属性が "number" となると、例えば、Chrome は数字以外の入力は受け付けなくなりますが、IE11 は最初の文字が数字であれば後に続く文字は何でも入力できてしまうという違いが出ます。

さらに、プロパティに RegularExpressionAttribute を追加して数字か否かをチェックするようにしても、input 要素の type 属性が "number" となっていると無視されます。

その場合動きはブラウザ依存になり、"1x" というような入力を受け付ける IE11 では data-val-number 属性に設定されたメッセージが、Firefox では data-val-required 属性に設定されたメッセージが表示されます。Chrome は "1x" というような文字は入力できませんが、"1..." という文字列は受け付けるので、その場合は Firefox と同様に data-val-required 属性に設定されたメッセージが表示されます。

EditorFor ではなく TextBoxFor を使えば input 要素の type 属性は "text" となって、RegularExpressionAttribute による検証が行われ、検証 NG の場合は ErrorMessage に設定したメッセージが表示されます。

input 要素の type 属性が "number" となることによりブラウザ依存の動きとなって期待と異なるエラーメッセージが出るのを避けるためには以下の対応が必要です:

  1. TextBoxFor を使って input 要素の type 属性が "text" となるようにし、さらに
  2. RegularExpressionAttribute で数字か否かの検証を行う。  

以上はクライアント側での検証の話です。サーバー側での検証によるエラーメッセージは上記とは異なります。上の画像の「価格2 (int)」のエラーメッセージを見てください。

クライアント側での検証を無効にして "2000x" という文字列を送信していますが「値 '2000x' は 価格2 (int) に対して無効です。」というエラーメッセージが出ています。

それは EditorFor (type="number") でも TextBoxFor (type="text") でも同じで、数字として不正な文字が混ざって POST されると、モデルバインディングの際 int 型にパースできないということで、RegularExpressionAttribute による検証が行われる前に検証 NG となって、そのエラーメッセージが出るようです。

RegularExpressionAttribute の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が POST されては何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

マイクロソフト公式解説書「プログラミング ASP.NET MVC」の p186「エラーメッセージを制御する」に書いてあったことですが、ModelStateDictionary に含まれる ModelState は同じ Key でマージした方に上書きされます。具定例は以下のコードの通りです。上の画像の「ID (int)」がこのコードによる書き換え結果です。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult WankumaEdit(Keiyaku model)
{
  if (ModelState.IsValid)
  {
    // DB の編集処理
    return RedirectToAction("Index");
  }

  // デバッグ用
  ModelStateDictionary dictionary = ModelState;

  // ValidationSummary(true) に表示するために追加
  var newDictionary = new ModelStateDictionary();
  newDictionary.AddModelError("",
    "ValidationSummary に表示するために追加。");
  ModelState.Merge(newDictionary);

  // エラーメッセージを書き換えることはできる。
  // 「プログラミング ASP.NET MVC」の p186「エラーメッセージ
  // を制御する」参照。同じ Key でマージした方に上書きされる
  ModelState state = dictionary["KeiyakuID"];

  if (state.Errors.Count > 0)
  {
    string msg = state.Errors[0].ErrorMessage;
    if (msg.StartsWith("値"))
    {
      // マージすると Value が null になるので書き戻すために
      // 取得しておく
      ValueProviderResult value = state.Value;
      var newDictionary2 = new ModelStateDictionary();
      newDictionary2.AddModelError("KeiyakuID",
        "入力不正(デフォルトの「値 'xx' は ID に対して" +
        "無効です。」を書き換え)");
      ModelState.Merge(newDictionary2);

      // Value を書き戻す。そうしないと再描画されたとき元の
      // ユーザー入力が表示されず 0 になってしまう
      ModelState["KeiyakuID"].Value = value;
    }
  }
  return View(model);
}

Tags: , ,

MVC

DropDownList への SelectList の渡し方

by WebSurfer 25. February 2019 14:37

ASP.NET MVC5 の Html ヘルパーの DropDownList および DropDownListFor に表示するデータを、アクションメソッドからビューにどのように渡すかということについて書きます。

DropDownList

上の画像は、先の記事「スキャフォールディング機能」で書いた通りにスキャフォールディング機能を使って自動生成させたコードで、その中の Edit 画面を表示したものです。

SupplierID と CategoryID が Html ヘルパーの DropDownList を使ってドロップダウン形式で表示されるようになっています。上の画像は CategoryID を展開したところで、CategoryName の一覧が表示されています。

スキャフォールディング機能で自動生成されたコードが基本になるでしょうから、それがどうなっているかを書きます。

まずアクションメソッド Edit で SelectList オブジェクトを生成し ViewBag に設定しています。以下のコードの通りです。

public ActionResult Edit(int? id)
{
  NORTHWINDEntities db = new NORTHWINDEntities();
  Products products = db.Products.Find(id);

  ViewBag.CategoryID = 
    new SelectList(db.Categories, "CategoryID", 
                   "CategoryName", products.CategoryID);

  ViewBag.SupplierID = 
    new SelectList(db.Suppliers, "SupplierID", 
                   "CompanyName", products.SupplierID);

  return View(products);
}

コンストラクタに SelectList(IEnumerable, String, String, Object) を使って、第 4 引数に selectedValue を設定しているところに注目してください。これによりビューの DropDownList が html に変換された際、select 要素内の当該 option 要素に selected 属性が付与されます。

ビューの DropDownList のコードは以下のようになります。第 1 引数がアクションメソッドで設定した ViewBag のキー名、第 2 引数が null になっているところに注目してください。

@Html.DropDownList("SupplierID", null, 
        htmlAttributes: new { @class = "form-control" })

@Html.DropDownList("CategoryID", null, 
        htmlAttributes: new { @class = "form-control" })

DropDownList の第 2 引数が null となっていますが、第 2 引数の設定に関わらず ViewData / ViewBag から型が IEnumerable<SelectListItem> でキー名が第 1 引数と同じものを探してきます。

(例えば、上記のアクションメソッドで ViewBag.SupplierID の設定を削除すると、ビューの DropDownList のコードで「キー 'SupplierID' を持つ ViewData 項目の型は 'System.Int32' ですが、'IEnumerable<SelectListItem>' でなければなりません」というエラーになります)

DropDownList の第 1 引数を元に ViewBag で渡されたデータ(アクションメソッドで設定された SelectList オブジェクト)を取得するので、上の画像の通りドロップダウン形式で表示できるようになります。さらに、SelectList コンストラクタの第 4 引数に設定した selectedValue によって当該 option 要素に selected 属性が設定された結果が表示されます。

なお、DropDownList の第 2 引数を (SelectList)ViewBag.Supplier としたりすると、SelectList のコンストラクタで第 4 引数に設定した selectedValue が無視されるので注意してください。理由は不明です。

ViewData / ViewBag に DropDownList の第 1 引数と同じキー名がない場合は、DropDownList の第 2 引数の設定が有効になるようです。例えば、アクションメソッドで ViewBag.SupplierID を ViewBag.Supplier に変更した場合、DropDownList("SupplierID", (SelectList)ViewBag.Supplier, ...) として selected の設定を含めて期待した結果が得られます。

ViewData / ViewBag を探す順序ですが、検証してみると、まず最初に ViewData を、それになければ ViewBag を探すという結果になりました。ViewData / ViewBag に同じキー名があると、ViewData のデータが使われます。その際、もし ViewData のデータが不正ですと(IEnumerable<SelectListItem> 型でないと)エラーになります。

以上は DropDownListFor を使っても同様です。第 1 引数は model => model.SupplierID のようになりますが、プロパティ名 SupplierID から ViewData / ViewBag を探して設定してくれます。

Tags: , ,

MVC

日付と通貨の書式設定

by WebSurfer 4. February 2019 18:10

ASP.NET MVC5 アプリで日付(DateTime 型)と通貨(Decimal 型)を表示し、編集してデータベースを更新する場合、どのように書式設定を行うのが良いかということについて書きます。

日付と通貨の書式設定

上の画像は DisplayFor と TextBoxFor を上下に並べて表示していますが、その「契約日」と「価格」に示したように、

  1. DisplayFor 等を使った表示の場合のみ書式設定を行い、
  2. テキストボックスに値を表示する場合は書式設定しない。または、"{0:F0}" のような書式設定に留めパースできない文字は含めないようにする。

・・・のがよさそうです。

スキャフォールディング機能を使って CRUD 可能なアプリを作ると Index (レコード一覧), Create, Delete, Edit, Details というアクションメソッドとそれに対応するビューが生成されますが、Index, Delete, Details を上記 (1) で、Create, Edit を上記 (2) で表示するということです。

テキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のように表示することはできますが、クライアント側での検証を無効にしてサーバーに POST したとしても、DateTime 型や Decimal 型にパースできないので結局モデルバインディングの際の検証が通りません。

どうしてもテキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のようにしたいということであれば、モデルのプロパティも含めて全て String 型として扱い、検証は正規表現のみを使って行うということも考えられますが、データベースの型が datetime とか money でしょうから、サーバー側での取り扱いを考えると現実的ではなさそうです。

それに、ユーザーとしても、編集する際にいちいち ¥ とか , を入力するのは煩わしいはずで、喜んではくれないのでは?

上の画像のように表示するための方法ですが、DisplayFor での表示の書式はモデルのプロパティに DisplayFormatAttribute を付与して設定し、テキストボックスには TextBoxFor を使ってその第 2 引数に "{0:yyyy/MM/dd}"、"{0:F0}" などの書式を設定するのがよさそうです。

EditorFor を使うと書式が自由に設定できないし(DisplayFormatAttribute の設定と同じでよければ可能ですが)、レンダリングされる html ソースの input 要素の type 属性が問題になることがありますので使う場合は要注意です。

具体的には以下のようにします。

Model

public class PurchaseRecord
{
  [Display(Name = "ID")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  public int ID { get; set; }

  [Display(Name = "契約日")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(
      @"^\d{4}/\d{2}/\d{2}( \d{1,2}:\d{2}:\d{2})?$", 
      ErrorMessage = "yyyy/MM/dd 形式")]
  [DisplayFormat(DataFormatString = "{0:yyyy年M月d日}")]
  public DateTime ContractDate { get; set; }

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  [DisplayFormat(DataFormatString = "{0:C0}")]
  public decimal Price { get; set; }
}

View(一部の抜粋)

<div class="form-group">
  @Html.LabelFor(model => model.ID, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ID)
    @Html.EditorFor(model => model.ID, 
      new { htmlAttributes = new { @class = "form-control" } })
    @Html.ValidationMessageFor(model => model.ID, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.ContractDate, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ContractDate)
    @Html.TextBoxFor(model => model.ContractDate, 
      "{0:yyyy/MM/dd}", new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.ContractDate, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.Price, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.Price)
    @Html.TextBoxFor(model => model.Price, 
      "{0:F0}",new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.Price, 
      "", new { @class = "text-danger" })
  </div>
</div>

あと、オマケの話ですが、ASP.NET Web Forms アプリで使う GridView などで書式設定する際も同様なことはできます。以下の画像を見てください。

GridView で日付と通貨の書式設定

赤枠で囲った部分は[編集]ボタンをクリックして編集モードにしたテキストボックスですが、他の行との違いを見てください。

Tags: , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  August 2019  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar