WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

クライアント側検証の無効化

by WebSurfer 17. February 2015 18:40

先の記事 コレクションのデータアノテーション検証 でユーザー入力の検証の話を書きましたが、デフォルトで有効になっているクライアント側での検証を無効にするにはどうすればいいかという話を書きます。

ユーザー入力の検証

これは、Visual Studio 2010 で MVC4 アプリを インターネットアプリケーション テンプレートで作った場合の話で、Visual Studio 2013 とか MVC5 とかでは違うかもしれませんのでご注意ください。

アプリケーションルート直下の web.config での以下の定義がされており、クライアント側での検証がアプリケーション全体で有効に設定されています。

<appSettings>
  ・・・中略・・・
  <add key="ClientValidationEnabled" value="true" />
  ・・・中略・・・
</appSettings>

クライアント側での検証を無効にしたいことがあるとすると多分特定の View だけでしょうから、その場合は当該 View のコードに下記を追加すれば OK です。

@{
  Html.EnableClientValidation(false);
}

クライアント側検証には EnableClientValidation の他に EnableUnobtrusiveJavaScript が使われていますが、stackoverflow の記事 によると、後者は Ajax にも使われていて、false にすると Ajax が動かなくなるとのことなので注意してください。

以上で話は終わりなのですが、それだけでは記事としてはちょっと面白くないので、上記に関連して調べたことを備忘録として書いておきます。(笑)

MVC4 の新機能として JavaScript / CSS ファイルの縮小化と結合処置の自動化機能があり、その機能を利用するため、App_Start フォルダの BundleConfig.cs ファイルに以下の定義がされています。

public class BundleConfig
{
  public static void RegisterBundles(BundleCollection bundles)
  {
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

    bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
                        "~/Scripts/jquery-ui-{version}.js"));

    bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.unobtrusive*",
                        "~/Scripts/jquery.validate*"));

    // ・・・中略・・・
  }
}

アプリケーションルート直下の Scripts フォルダには jquery-1.7.1.js, jquery.validate.js その他必要な jQuery 関係の外部スクリプトファイルが自動的に配置されます。

Global.asax には以下のコードが記述されており、アプリケーション起動時に外部スクリプトファイルのパスが ASP.NET の BundleCollection オブジェクトに登録されるようになっています。(詳しくは @IT の記事 Visual Studio 2012の新機能とASP.NET 4.5のコア機能 (3/4) を見てください)

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
      // ・・・中略・・・

      BundleConfig.RegisterBundles(BundleTable.Bundles);

      // ・・・中略・・・
  }
}    

BundleCollection オブジェクトに登録した外部スクリプトファイルを参照するには、View で Scripts.Render メソッド を使用します。

具体的には _Layout.cshtml(Web Forms アプリのマスターページに相当)の @Scripts.Render("~/bundles/jquery") と View の @Scripts.Render("~/bundles/jqueryval") で以下の外部スクリプトファイルへの参照が HTML ソースに定義されます。

<script src="/Scripts/jquery-1.7.1.js"></script>
<script src="/Scripts/jquery.unobtrusive-ajax.js"></script>
<script src="/Scripts/jquery.validate.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js"></script>

(注)上記は <compilation debug="true" ... /> の場合です。debug="false" の場合はファイルの縮小化と結合処置の自動化機能が働いて以下のようになります。

<script src="/bundles/jquery?v=1A_Q..."></script>
<script src="/bundles/jqueryval?v=-tc2Q..."></script>

以上ような設定において、例えば Model にアノテーション属性を以下のように付与したとします。

public class Parent
{
  //・・・中略・・・

  [Required(ErrorMessage = "{0} は必須")]
  [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
  [Display(Name = "Parent Name")]
  public string Name { get; set; }

  //・・・中略・・・
}

そして、View のコードを以下のように記述したとします。

<div class="editor-label">
  @Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
  @Html.EditorFor(model => model.Name)
  @Html.ValidationMessageFor(model => model.Name)
</div>

そうすると、クライアント側検証が有効の場合(Html.EnableClientValidation(true) の時)は、ASP.NET がレンダリングする HTML ソースは以下のようになります。

<div class="editor-label">
  <label for="Name">Parent Name</label>
</div>
<div class="editor-field">
  <input 
    class="text-box single-line" 
    data-val="true" 
    data-val-length="Parent Name は 5 文字以内" 
    data-val-length-max="5" 
    data-val-required="Parent Name は必須" 
    id="Name" 
    name="Name" 
    type="text" 
    value="" />
  <span 
    class="field-validation-valid" 
    data-valmsg-for="Name" 
    data-valmsg-replace="true">
  </span>
</div>

上記のように input 要素に付与された data-val* 属性に従って、参照された外部スクリプトに含まれる Unobtrusive(控えめな)JavaScript がクライアント側での検証を行うようになります。

一方、クライアント側検証を無効にした場合(Html.EnableClientValidation(false) の時)は ASP.NET がレンダリングする HTML ソースは以下のようになります。(注:外部スクリプトファイルへの参照は変わりません)

<div class="editor-label">
  <label for="Name">Parent Name</label>
</div>
<div class="editor-field">
  <input 
    class="text-box single-line" 
    id="Name" 
    name="Name" 
    type="text" 
    value="" />
</div>

クライアント側での検証はかかりませんがサーバー側で検証は行われ、検証結果が NG の場合は input 要素の直後に以下の span 要素が追加され、エラーメッセージ(上の画像の赤い文字)が表示されます。

<span class="field-validation-error">
  Parent Name は必須
</span>

Tags: ,

MVC

データアノテーション検証の多言語対応

by WebSurfer 11. September 2014 20:36

ASP.NET MVC Web アプリケーションのデータアノテーション属性を使用する際、リソースファイル(.resx)を使って表示されるメッセージを多言語化する方法を書きます。

エラーメッセージの多言語化

先の記事 コレクションのデータアノテーション検証 では、モデルのソースコードに中でメッセージをハードコーディングしました。

この記事では、メッセージをリソースファイルに格納し、そこからメッセージを取得して表示します。さらに、日本語と英語のリソースファイルを追加し、ブラウザの言語設定によって言語を切り替える例も紹介します。

さて、まずリソースファイルの格納場所ですが、どこが適当でしょうか?

ASP.NET には App_GlobalResources, App_LocalResources というリソースファイルを格納する専用のフォルダがあります。しかし、そのフォルダにリソースファイルを置くとユニットテストの際に問題があるそうです。(詳しくは Resource Files and ASP.NET MVC Projects の記事を参照)

ということで、この記事では App_GlobalResources フォルダは使わないで、とりあえずアプリケーションルート直下に直接置きました。(フォルダに入れる場合、名前空間にそのフォルダ名が追加になるので注意)

まず、Visual Studio のソリューションエクスプローラーを操作して[新しい項目の追加]ダイアログを表示し、[アセンブリ リソースファイル]を選んでリソースファイルを追加します。下の画像の例ではリソースファイル名を Resources1.resx という名前にしました。そうすると自動生成される Resource1.Designer.cs に定義される「厳密に型指定されたリソースクラス」において Resources1 がクラス名になります。

アセンブリ リソースファイルの追加

リソースファイルを追加したら、下の画像のように、Visual Studio の左側のウィンドウでその内容を設定します。表示するメッセージを[値]欄に、そのメッセージを取得するキー名を[名前]欄に記入します。ここで設定したものがデフォルトのメッセージとなります。

デフォルトのメッセージの設定

上の画像の例では、データアノテーション属性として DisplayAttribute, RequiredAttribute, StringLengthAttribute の 3 つを使用するという前提で、それぞれ Name, Required, StringLength というキー名(名前は任意です)でメッセージを設定しています。上のメッセージは説明用にテキトー書いたものです。実際にはデフォルトにふさわしいメッセージにしてください。

アクセス修飾子の設定を Public にするのを忘れないでください(上の画像で赤枠で囲んだ部分)。デフォルトでは Internal です。Public にするのを忘れると、後でユニットテストをする際に困ることになるはずです。(ちなみに、Internal ⇒ Public に変更すると、プロパティウィンドウで見て、カスタムツールが ResXFileCodeGenerator ⇒ PublicResXFileCodeGenerator に変わります)

Resources1.resx の設定が終わったら、日本語用に Resources1.ja-JP.resx、英語用に Resources1.en-US.resx という名前のリソースファイルを追加します。下の画像は日本語用の Resources1.ja-JP.resx の設定です。英語用も同様に(もちろんメッセージは英語で)設定します。

日本語リソースの設定

ja-JP, en-US というカルチャ名をリソースファイル名に追加するところがミソです。これによってブラウザの言語設定を ASP.NET が検出して(=要求ヘッダ情報を見て)自動的に使用するリソースファイルが切り替わります。

設定が完成すると以下の画像のようなリソースファイルができているはずです。(赤枠で囲った部分)

リソースファイル

次に、モデルでデータアノテーション属性を以下のように設定します。この例では Parent2 クラスの Name プロパティのみにリソースファイルからメッセージを取得して表示するようにしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;

namespace Mvc4App.Models
{
  public class Parent2
  {
    public Parent2()
    {
      Children = new List<Child2>();
    }

    public int Id { get; set; }
        
    [Required(
        ErrorMessageResourceType=typeof(Mvc4App.Resource1),
        ErrorMessageResourceName="Required")]
    [StringLength(
        5, 
        ErrorMessageResourceType = typeof(Mvc4App.Resource1), 
        ErrorMessageResourceName = "StringLength")]
    [Display(
        Name = "Name", 
        ResourceType = typeof(Mvc4App.Resource1))]
    public string Name { get; set; }

    public virtual IList<Child2> Children { get; set; }
  }

  public class Child2
  {
    public int Id { get; set; }
        
    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(5, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "Child Name")]
    public string Name { get; set; }

    public virtual Parent2 Parent { get; set; }
  }
}

RequiredAttribute, StringLengthAttirbute は ErrorMessageResourceType プロパティに Resource1.Designer.cs で定義される「厳密に型指定されたリソースクラス」の型を指定します(この例では typeof(Mvc4App.Resource1) です。Mvc4App が名前空間名、Resource1 がクラス名)。ErrorMessageResourceName プロパティにはリソースファイルに設定したキー名を設定します。

DisplayAttribute は、上の 2 つの属性とは設定するプロパティが異なり、ResourceType プロパティに「厳密に型指定されたリソースクラス」の型、Name プロパティにキー名を設定します。

最後に、web.config で globalization 要素の uiCulture, culture 属性を auto に設定します。これを忘れると、ブラウザの言語指定によるリソースファイルの自動切り替えは行われないので注意してください。(詳しくはこの記事の下の方の「2016/6/17 追記」を見てください)

<system.web>
  <globalization uiCulture="auto" culture="auto" />
</system.web>

これにより、例えば IE の言語の優先順位の設定を、下の画像のように日本語を最優先にしておくと、日本語のリソースファイル Resources1.ja-JP.resx からメッセージを取得して表示します。

IE の言語の優先順位の設定

その結果が一番上の画像です。

-------- 2016/6/17 追記 --------

Culture, UICulture を "auto" に設定すると、ASP.NET は、ブラウザから送信されてくる要求ヘッダに含まれる Accept-Language の設定を調べて、その要求を処理するスレッドのカルチャを Accept-Language に設定されているカルチャに書き換えるようです。

そして、リソースマネージャが実行時に、Thread.CurrentUICulture などで得られる CultureInfo(現在の要求を処理しているスレッドのカルチャ情報)を参照してローカライズされたリソースを検索し、UI に表示されるテキストを取得するという仕組みになっています。

Culture, UICulture を "auto" に設定するのを忘れるとブラウザの言語設定は無視されます。デフォルトではシステムのロケールに該当するカルチャがスレッドに設定されますので、例えば日本語 OS で xxx.ja-JP.resx というリソースがあれば、常にそれから UI に表示されるテキストを取得します。

Web サイトが日本語専用でサーバーも日本にあれば忘れても問題ないかもしれませんが、ホスティングサービス(Azure も含む)でサーバーが外国にある場合は Culture, UICulture を "auto" に設定するのを忘れると問題が出ると思います。

ロケール、カルチャ、Culture と UICulture の違いなどについては、記事「カルチャの基本とカルチャ情報」が参考になりましたので、忘れないようにリンクを張っておきます。

Tags: , ,

MVC

コレクションのデータアノテーション検証

by WebSurfer 1. September 2014 18:25

ASP.NET MVC Web アプリケーションでユーザー入力の検証を行う場合、MVC2 以降ではデータアノテーション検証を標準で使用することができます。

データアノテーション検証

データアノテーション検証とは、簡単に言えば、入力モデルのクラスの public プロパティにアノテーション属性を付与することによって、クライアントから送信されてきたデータをアクションメソッドのパラメータにバインド(モデルバインディング)する際に、付与した属性に従って検証を実行し、その結果を ModelStateDictionary(Controller.ModelState プロパティで取得できます)に格納することです。

記事としてはちょっと古いのですが、Microsoft が提供するチュートリアルの [C#] #21. データ アノテーション検証コントロールでの検証 がまとまっていて理解しやすいと思います。← このチュートリアルはリンク切れになってしまいました。別の記事「データ注釈検証コントロールに (c#) を使用した検証」を紹介しておきます。

上記のサーバーサイドの検証に加えて、MVC3 以降ではクライアントサイドでの jQuery ライブラリによる検証もデフォルトで実装されるようになっています。

モデルに付与したアノテーション属性と EditorFor などの Html ヘルパーが連動して、レンダリングされる html 要素(例: <input type="text" ... />)に jQuery ライブラリによる検証機能が働くように必要な属性(例: data-val="true" など)を追加します。

クライアントサイドで検証結果 NG になるとエラーメッセージを表示し、submit はキャンセルされます。それゆえ、検証結果 NG のときはクライアントとサーバーの間の無駄なやりとりは起こりません。

さて、ちょっと前置きが長くなりましたが、ここでの本題、入力モデルが複合型のコレクションで入れ子になっているような複雑な場合、モデルバインディングとデータアノテーション検証(クライアントサイドを含む)をどう実装できるかという話を以下に書きます。

まずコレクションのモデルバインディングですが、それがうまく行われるようにするには、レンダリングされる html 要素の name 属性が連番のインデックスを含むようにします。

具体的には、name="prefix[index].Property" というパターンにします。prefix の部分にはアクションメソッドのパラメータ(仮引数)名が入ります。index は 0 から始まる連番です。数字の連続が途切れた場合は解析が停止し、0 から途切れる前までのデータが返されます。

マイクロソフト公式解説書の「プログラミング ASP.NET MVC」という本の 3.2.2 章に例がありますが、そこではビューで以下のようなコードで name 属性に連番のインデックスを持つ名前を設定しています。

var index = 0;
foreach (var country in Model.CountryList)
{
  <fieldset>
    <div> 
      <b>Name</b>
      <br />
      <input type="text" 
        name="countries[@index].Name" 
        value="@country.Name" />
      <br />
      <b>Capital</b>
      <br />
      <input type="text" 
        name="countries[@index].Details.Capital" 
        value="@country.Details.Capital" />
      <br />
      <b>Continent</b>
      <br />
      @{
        var id = String.Format(
                "countries[{0}].Details.Continent", 
                index++);
       }
       @Html.TextBox(id, country.Details.Continent)
       <br />
     </div>
   </fieldset>
}

上記のコードで countries はコントローラーの当該アクションメソッドの IList<Country> 型の仮引数の名前と一致させています。そうすることにより、クライアントから送信されてきたデータが正しくモデルバインディングされ、アクションメソッドに引き渡されます。

次に、これにデータアノテーション検証(クライアントサイドを含む)を追加するにはどうすればいいでしょう?

必要なアノテーション属性を入力モデルに追加するだけでもサーバーサイドでの検証は可能になります。しかし、それだけではクライアントサイドでの検証は動きません。

なぜなら、ASP.NET がレンダリングする html 要素には、クライアントサイドでの jQuery ライブラリによる検証に必要な属性(例: data-val="true" など)は追加されないからです。

検証に必要な属性が追加されるようにするにはビューに EditorFor のような Html ヘルパーを使うことです。これにより name 属性に設定する連番のインデックスも ASP.NET が自動的に追加してくれます。

モデル、コントローラー、ビューのコード例は以下の通りです。これを動かした結果が上の画像です。

(1) Model

モデルには普通に RequiredAttribute 他必要なアノテーション属性を追加すれば良いです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;

namespace Mvc4App.Models
{
  public class Country
  {
    public Country()
    {
      Details = new CountryInfo();
    }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(15, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "国名")]
    public String Name { get; set; }

    public CountryInfo Details { get; set; }
  }

  public class CountryInfo
  {
    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(15, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "首都")]
    public String Capital { get; set; }

    [Required(ErrorMessage = "{0} は必須")]
    [StringLength(15, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "大陸")]
    public String Continent { get; set; }
  }

  public class ListCountriesViewModel
  {
    public ListCountriesViewModel()
    {
      CountryList = new List<Country>();
      SelectedCountries = new List<Country>();
    }
    public IList<Country> CountryList { get; set; }
    public IList<Country> SelectedCountries { get; set; }
  }
}

(2) Controller

アクションメソッドの仮引数の名前は、本にあったコード例の countries から countrylist に変更しています。これは、モデルの ListCountriesViewModel クラスのプロパティ名 CountryList と合わせる必要があるためです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc4App.Models;

namespace Mvc4App.Controllers
{
  public class ComplexController : Controller
  {
    [ActionName("Countries")]
    public ActionResult ListCountriesForGet()
    {
      var defaultCountries = GetDefaultCountries();
      var model = new ListCountriesViewModel { 
                CountryList = defaultCountries };
      return View(model);
    }

    [HttpPost]
    [ActionName("Countries")]
    public ActionResult ListCountriesForPost(
            IList<Country> countrylist)
    {
      if (!ModelState.IsValid)
      {
        var model = new ListCountriesViewModel
        {
          CountryList = countrylist
        };

        return View(model);
      }
      else
      {
        var defaultCountries = GetDefaultCountries();

        var model = new ListCountriesViewModel
        {
          CountryList = defaultCountries,
          SelectedCountries = countrylist
        };

        return View(model);
      }            
    }

    private static IList<Country> GetDefaultCountries()
    {
      var countries = new List<Country>();
      countries.Add(new Country() { 
                Name = "Italy", 
                Details = new CountryInfo { 
                    Capital = "Rome", 
                    Continent = "Europe" } });
      countries.Add(new Country() { 
                Name = "Spain", 
                Details = new CountryInfo { 
                    Capital = "Madrid", 
                    Continent = "Europe" } });
      countries.Add(new Country() { 
                Name = "USA", 
                Details = new CountryInfo { 
                    Capital = "Washington", 
                    Continent = "NorthAmerica" } });
      return countries;
    }
  }
}

(3) View

EditorFor(m => m.CountryList[0].Name) からは name="CountryList[0].Name" という name 属性が生成されます。それゆえ、コントローラーのアクションメソッドの仮引数の名前を countrylist にしています。(注:モデルバインディングでは大文字小文字は区別されません)

@model Mvc4App.Models.ListCountriesViewModel

@{
  ViewBag.Title = "Countries";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

@using (Html.BeginForm())
{
  <h2>Select your favorite countries</h2>

  for (int i = 0; i < Model.CountryList.Count; i++)
  {
    <fieldset>
    <div> 
      @Html.LabelFor(m => m.CountryList[i].Name)
      @Html.EditorFor(m => m.CountryList[i].Name)
      @Html.ValidationMessageFor(m => m.CountryList[i].Name)
      <br />
      @Html.LabelFor(m => m.CountryList[i].Details.Capital)
      @Html.EditorFor(m => m.CountryList[i].Details.Capital)
      @Html.ValidationMessageFor(m => m.CountryList[i].Details.Capital)
      <br />
      @Html.LabelFor(m => m.CountryList[i].Details.Continent)
      @Html.EditorFor(m => m.CountryList[i].Details.Continent)
      @Html.ValidationMessageFor(m => m.CountryList[i].Details.Continent)
      <hr />
    </div>
    </fieldset>
  }
    
  <input type="submit" value="Send" />
} 
  <hr />
    
  <h2>Countries submitted</h2>
  <ul>
  @foreach (var country in Model.SelectedCountries)
  {
    <li>@country.Name</li>
  }
  </ul>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

Tags: ,

MVC

About this blog

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

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar