WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

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

by WebSurfer 2014年9月11日 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: , ,

Validation

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

by WebSurfer 2014年9月1日 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: ,

Validation

Web サービス経由で非接続型データアクセス

by WebSurfer 2014年8月23日 16:50
2018/3/4 追記:
Microsoft によると Web サービスは "Legacy Technology" なので WCF を使えとのことゆえ、接続先を WCF サービスに変更し、非同期にアクセスするサンプルを作ってみました。興味がありましたら記事「WCF サービス経由で非接続型データア��セス」を見てください。

DataGridView と型付 DataSet + TableAdapter を用いて非接続型データアクセスを行う Windows アプリケーションで、Web サービス経由で SQL Server のデータの取得更新を行う例の紹介です。(Windows アプリから直接 SQL Server に接続するのではなく)

DataGridView

Visual Studio のデータソース構成ウィザードを使うと、MSDN ライブラリの Windows フォーム アプリケーションでのデータへの接続 の図のような構成の Windows アプリケーションを作成できます。具体的な作成手順例は、チュートリアル「10 行でズバリ !! 非接続型のデータ アクセス」を見てください。

上に紹介したチュートリアルの例では Windows アプリから直接 SQL Server に接続していますが、セキュリティ上の問題などで、Windows アプリ ⇔ サーバー ⇔ SQL Server というようにサーバーを介してアクセスしたいということがあると思います。

Web サーバー (IIS) を利用し、それに Web サービス(.asmx)を実装して、Windows アプリケーションとの間で型付 DataSet をやりとりすることで Windows アプリ ⇔ サーバー ⇔ SQL Server という構成を実現できます。

(1) Web サービス

まず Web サービスですが、(a) SQL Server から型付 DataSet にデータを取得して Windows アプリケーション渡すメソッド、(b) ユーザーが編集済みの型付 DataSet を受け取って SQL Server のレコードを更新するメソッドを実装します。

具体例は以下のようになります。GetDataSet メソッドが上記 (a)、Update メソッドが上記 (b) に該当します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;

namespace WebApplication1
{
  using ProductsDataSetTableAdapters;
    
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  [System.ComponentModel.ToolboxItem(false)]
  public class ProductsWebService : WebService
  {
    [WebMethod]
    public ProductsDataSet GetDataSet()
    {
      ProductsDataSet dataset = new ProductsDataSet();
      ProductsTableAdapter adapter = 
                          new ProductsTableAdapter();            
      adapter.Fill(dataset.Products);
      return dataset;
    }

    [WebMethod]
    public int Update(ProductsDataSet dataset)
    {
      TableAdapterManager manager = new TableAdapterManager();
      manager.ProductsTableAdapter = new ProductsTableAdapter();
      return manager.UpdateAll(dataset);
    }
  }
}

SQL Server データベースには、Microsoft が無償で提供しているサンプルデータベース Northwind の Products テーブルを使っています。

ProductsDataSet, ProductsTableAdapter, TableAdapterManager は、 Visual Studio のデータソース構成ウィザードを使って自動生成させた .xsd ファイル下の .Designer.cs に含まれる型付 DataSet + TableAdapter + TableAdapterManager です。

(2) クライアント・アプリケーション

上記の Web サービスをクライアント・アプリケーションでは以下のように利用できます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace UpdateUsingWebService
{
  public partial class Form2 : Form
  {
    private ProductsServiceReference.
                        ProductsWebServiceSoapClient client;
    private ProductsServiceReference.
                        ProductsDataSet productsDataSet;

    public Form2()
    {
      InitializeComponent();
      InitializeComponent2();
      this.client = new ProductsServiceReference.
                            ProductsWebServiceSoapClient();
    }

    private void productsBindingNavigatorSaveItem_Click(
        object sender, EventArgs e)
    {
      this.Validate();
      this.productsBindingSource.EndEdit();
            
      // Web サービスに渡すのはユーザーが編集した行のみで可
      ProductsServiceReference.ProductsDataSet ds =
                  (ProductsServiceReference.ProductsDataSet)
                           this.productsDataSet.GetChanges();

      // 編集済みの型付 DataSet を Web サービスに渡し、更新
      // をかける。戻り値 n は更新されたレコード数
      int n = this.client.Update(ds);

      // 更新後の型付 DataSet を取得。DataGridView 書き換え
      this.productsDataSet = this.client.GetDataSet();
      this.productsBindingSource.DataSource = 
                                    this.productsDataSet;
    }

    private void Form2_Load(object sender, EventArgs e)
    {
      // Web サービスより型付 DataSet を取得
      this.productsDataSet = this.client.GetDataSet();
      this.productsBindingSource.DataSource = 
                                     this.productsDataSet;
    }

    // ・・・中略・・・

    private void InitializeComponent2()
    {
      // ・・・中略・・・
    }

  }
}

まず、Visual Studio のサービス参照の追加ウィザードを利用して、上記 (1) で作成した Web サービスを参照し、プロキシクラスと型付 DataSet(上記のコード例では ProductsWebServiceSoapClient と ProductsDataSet)を自動生成させます。

次に、Visual Studio のデータソース構成ウィザードを使って、上記 (1) で作成した型付 DataSet + TableAdapter + TableAdapterManager と全く同じものを自動生成させます。

その上で、Visual Studio のデザイン画面で、「データソース」ウィンドウの項目を Form 画面にドラッグ&ドロップすると、DataGridView, BindingSource, BindingNavigatorなどのコードが自動生成されます。

それらを組み合わせれば容易に実装できるはずです。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar