WebSurfer's Home

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

EDM にデータアノテーション属性を付与

by WebSurfer 2017年5月21日 15:10

Microsoft のチュートリアル「10 行でズバリ!! ASP.NET MVC を構成する各コンポーネントとネーミング ルール (C#)」のように、既存の SQL Server データベースから Entity Data Model (EDM) を生成して ASP.NET MVC アプリで使用する場合、ユーザー入力を検証するためのデータアノテーション属性をどのように付与できるかを書きます。

Visual Studio で生成した EDM

上の画像は、SQL Server 2008 Express のデータベース AdventureWorksLT をベースに、Visual Studio 2015 + EF6 で生成した EDM (Model1.edmx) です。(注:Visual Studio 2010 + EF4 で生成した EDM は内容が異なりますが、データアノテーション属性を付与する方法は同じです)

生成した EDM をベースに、上に紹介した「10 行でズバリ!!」チュートリアルと同様に、Visual Studio のスキャフォールディング機能を利用して、Address テーブルを表示・編集する ASP.NET MVC アプリの Controller と View を一式自動生成する場合を例に考えます。

自動生成された EDM で Address テーブルを表す Model のコードは、上の画像で示すように Address.cs にあります。そのコードは以下の通りです(説明に不要な部分は省略しています)。

namespace AdventureWorksLT
{
    using System;
    using System.Collections.Generic;
    
    public partial class Address
    {
        //・・・コンストラクタ(省略)・・・
        
        public int AddressID { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string StateProvince { get; set; }
        public string CountryRegion { get; set; }
        public string PostalCode { get; set; }
        public System.Guid rowguid { get; set; }
        public System.DateTime ModifiedDate { get; set; }
    
        //・・・ナビゲーションプロパティ(省略)・・・
    }
}

これに手を加えればよさそうに思えますが、それは NG です。何故なら、コードのコメントに書いてあるように、"このファイルを手動で変更すると、アプリケーションで予期しない動作が発生する可能性があります。このファイルに対する手動の変更は、コードが再生成されると上書きされます" ということだからです。

ではどうするかですが、Microsoft の文書「Validation with the Data Annotation Validators (C#)」の「Using Data Annotation Validators with the Entity Framework」のセクションに書いてある方法を取るのがよさそうです。(日本語版は機械翻訳なので英文を読むことをお勧めします)

TechNet の記事を読めばこれ以降の説明は不要かもしれませんが、記事がリンク切れになったりすると困るので、上の Address クラスの場合を例に方法を書いておきます。

具体的には以下のようなコードを EDM とは別の場所にクラスファイルを追加してそれに含めます。上の画像のソリューションエクスプローラの AddressMetaData.cs がそのクラスファイルです。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace AdventureWorksLT
{
    [MetadataType(typeof(AddressMetaData))]
    public partial class Address
    {

    }

    public class AddressMetaData
    {
        [Display(Name = "住所1")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object AddressLine1 { get; set; }

        [Display(Name = "住所2")]
        public object AddressLine2 { get; set; }

        [Display(Name = "街")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        [StringLength(10, ErrorMessage = "{0} は {1} 以内。")]
        public object City { get; set; }

        [Display(Name = "州")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object StateProvince { get; set; }

        [Display(Name = "国名")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object CountryRegion { get; set; }

        [Display(Name = "郵便番号")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object PostalCode { get; set; }

        [Display(Name = "GUID")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object rowguid { get; set; }

        [Display(Name = "更新日")]
        [Required(ErrorMessage = "{0} は必須入力です")]
        public object ModifiedDate { get; set; }
    }
}

上のコードで AddressMetaData が TechNet の記事で言うメタデータクラスです。それにプロキシプロパティを定義して必要なデータアノテーション属性を付与しています。

TechNet の記事にも書いてありますが、プロキシプロパティの型は自動生成された Address クラスのプロパティの型と同じである必要はないそうです(同じでも OK ですが)。上のコードでは TechNet の記事にならってプロキシプロパティの型は Object 型にしています。

自動生成された Address クラスは partial として定義されているところに注目してください。partial なので、上のコードのように別のクラスファイルに Address クラスを定義してそれに MetadataTypeAttribute Classを付与することができます。(自動生成された Address クラスに手を加える必要はありません)

MetadataType 属性の引数に typeof(AddressMetaData) を設定することにより、メタデータクラス AddressMetaData を Address クラスに関連付けています。

以上の設定により、データアノテーション属性による検証が、クライアント側とサーバー側の両方で行われるようになります。

さらに、Display 属性も追加していますので、表示名が Display 属性の Name プロパティで設定した通りになります。

Tags: ,

Validation

CustomValidator で jQuery.ajax 利用

by WebSurfer 2016年10月20日 23:18

ASP.NET Web Forms アプリで、ユーザー入力の検証に CustomValidator と jQuery.ajax を用い、クライアント側での検証を行うというコードを書いたときに、無知な自分がハマったことを忘れないように備忘録として書いておきます。

CustomValidator

シナリオは、ユーザー入力が有効かどうかサーバー側でなければ確認できない場合、クライアント側から jQuery.ajax を使ってサーバーに問合せて検証し、検証結果が NG であればエラーメッセージを表示してユーザーに再入力を促すというものです。

CustomValidator は検証用のコードを自力で書かなければなりませんが、その分自由度が高く、クライアント側での検証用スクリプトを自分で書いて、jQuery.ajax を使ってサーバー側に問い合わせ、その結果で検証するようにできます。

(ちなみに、RequiredFieldValidator とか RegularExpressionValidator などは、クライアント側での検証用スクリプトとサーバー側での検証用コードの両方とも ASP.NET が自動的に生成してくれます)

また、ASP.NET Web Forms 用に多々用意されている検証コントロールの一つなので、他の検証コントロールと合わせて、全体的に整合が取れたユーザー入力の��証を行うことができます。

というわけで、今回のシナリオには CustomValidator を使うのがよさそうなので、CustomValidator と jQuery.ajax を組み合わせて使ったサンプルコードを書いてみました。下にアップしたのがそれです。

Microsoft が提供するサンプルデータベース Northwind の Customers テーブルを利用し、ユーザーが TextBox に入力した文字列が Customers テーブルの CustomerID フィールドに存在しない場合は検証 NG としています。他に、未入力および入力形式(アルファベット 5 文字)の検証もしています。

で、自分がハマったことですが、以下の 1 と 2 です。3 以降は自分的に備忘録として残しておいた方がよさそうだと思って書きました。下のサンプルコードを参照しながら読んでください。

  1. jQuery.ajax の設定 の async オプションを false に設定する。
    デフォルトは true で「非同期」になります。非同期では、検証用スクリプトの ClientValidate 関数は、$.ajax({ ... }); が実行されると、success, fail オプションに設定されたコールバックが実行されないまま即完了してしまいます。結果、ClientValidate 関数では args.IsValid には何も設定されません(初期値の true のままになる)。
    なので async:false に設定する必要があります。API Documentation の説明によると "Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active." とのことなので、あまりお勧めはできないようですが、他に手段が見つからないのでやむを得ません。
    なお、async:false とした場合は、jqXHR.done() は使わないで、success, error, complete にコールバックを書けと言うことですのでそうしてあります。
  2. jQuery.ajax によるサーバーへの要求が 2 回出てしまうケースがある。
    TextBox にアルファベット 5 文字を入力してから(正規表現による検証はパスさせてから)Button をクリックすると 2 回要求が出てしまいます(1 回無駄)。理由は、Button クリックで TextBox からフォーカスが外れて検証がかかり、Button クリックでもう一度検証かかかるからです。
    もともとそのようなケースでは検証が 2 回かかってしまうのが仕様(?)のようですが、今回のようにサーバーのデータベースへの問合せが行われており、それが 1 回余分になるのは何とかした方がよさそうな気がします。回避方法は調査中です。(見つからないかも)
  3. ユーザーが未入力の場合の検証は RequiredFieldValidator を利用しています。
    先の記事「CustomValidator と RequiredFieldValidator」で書きましたが、CustomValidator で未入力の検証も可能です。しかし、検証コントロールのエラーメッセージは固定的なので、CustomValidator 一つで済ませる場合、エラーメッセージは "未入力もしくはデータベースに存在しません" というようにせざるを得ません。
    それより、RequiredFieldValidator と CustomValidator を併用して、未入力の時は "未入力です" というエラーメッセージを、入力はあるが無効な場合は "データベースに存在しません" とした方がユーザーフレンドリーだと思います。
  4. 上で「検証コントロールのエラーメッセージは固定的」と書きましたが、サーバー側での検証メソッドでは検証内容に応じてエラーメッセージを書き換えることができます。
    しかしながら、先の記事「FileUpload と CustomValidator」でもいろいろ考えましたが、クライアント側でのエラーメッセージを書き換える方法が分かりません。それも RequiredFieldValidator と CustomValidator を併用した理由です。
  5. RegularExpressionValidator と併用すると表示が 2 重になる。
    最初、RequiredFieldValidator で未入力の検証、RegularExpressionValidator でアルファベット 5 文字であることの検証、CustomValidator でデータベースにデータがあるかの検証を行うつもりでした。
    しかしユーザー入力があって RequiredFieldValidator での検証をパスした場合、RegularExpressionValidator と CustomValidator の両方の検証がかかり、両方のエラーメッセージ(例えば、「アルファベット 5 文字としてください」と「データベースに存在しません」)が出てしまいます。
    それが、以下のサンプルコードで、CustomValidator でアルファベット 5 文字とデータベースの有無の両方の検証をすることにした理由です。
  6. 上の画像のエラーメッセージ "CustomValidator" は、CustomeValidator をドラッグ&ドロップしたときのデフォルトです。
    ユーザー入力 abcde はデータベースに存在しないので、クライアント側での検証結果が NG となり、ErrorMessage プロパティに設定されたエラーメッセージ "CustomValidator" が赤文字で表示されたところです。
    もちろんこれは初期設定で変更できます。サーバー側ではエラーメッセージを動的に書き換えることもできます。下のコードで、クライアント側での検証が働かないようにして(ClientValidate 関数の中のコードを全てコメントアウトするなどして)サーバー側だけで検証をかけると、ServerValidate メソッドで書き換えられたエラーメッセージが表示されます。
    ただし、上にも書きましたが、クライアント側でのエラーメッセージは固定的になります。
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Text.RegularExpressions " %>
<%@ Import Namespace="System.Web.Configuration" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Web.Services" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

  // jQuery.ajax で POST された文字列を受け、DB に存在するか
  // をチェックするためのページ内の静的メソッド。
  // public static にして WebMethodAttribute 属性を付与
  [WebMethod]
  public static string ValidateId(string id)
  {
    // Northwind サンプルデータベース Customers テーブルの
    // CustomerID フィールドにユーザーが入力した文字列と同じ
    // データが存在するかを調べ、存在すればその CustomerID
    // データを、無ければ null を返す        
    string query = "SELECT CustomerID FROM Customers " + 
                   "WHERE CustomerID = @CustomerID";
    string connString = WebConfigurationManager.
        ConnectionStrings["NORTHWINDConnectionString"].
        ConnectionString;

    using (SqlConnection conn = new SqlConnection(connString))
    {
      using (SqlCommand cmd = new SqlCommand(query, conn))
      {
        cmd.Parameters.Add("@CustomerID", SqlDbType.NChar);
        cmd.Parameters["@CustomerID"].Value = id;
        conn.Open();
        return (string)cmd.ExecuteScalar();
      }
    }
  }
    
  // CustomValidator のサーバー側での検証用メソッド
  protected void ServerValidate(object source, 
      ServerValidateEventArgs args)
  {
    // CustomerID はアルファベット 5 文字なので、まず
    // 正規表現でアルファベット 5 文字にマッチするかを
    // チェック
    string pattern = "^[a-zA-Z]{5}$";

    if (Regex.IsMatch(args.Value, pattern))
    {
      // 上のヘルパメソッドを流用。DB にデータが
      // 存在しない場合 null が id に代入される
      string id = ValidateId(args.Value);

      if (id != null)
      {
        args.IsValid = true;
      }
      else
      {
        // サーバー側でならエラーメッセージを書き換
        // えることが可能
        ((CustomValidator)source).ErrorMessage = 
                    "データベースに存在しません";
        args.IsValid = false;
      }
    }
    else
    {
      ((CustomValidator)source).ErrorMessage = 
              "アルファベット 5 文字としてください";
      args.IsValid = false;
    }
  }   

  protected void Button1_Click(object sender, EventArgs e)
  {
    // IsValid で検証結果を調べて処置を行うのが原則
    if (Page.IsValid)
    {
      Label1.Text = "OK";
    }
    else
    {
      Label1.Text = "NG";
    }
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
  <script src="/Scripts/jquery-1.11.1.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    // async: false を設定しないとコールバックは非同期で実行
    // され args.IsValid が設定できないので注意
    // TextBox に入力してから Button をクリックすると 2 回要
    // 求が出でしまう。理由は、Button クリックする際 TextBox
    // からフォーカスが外れて検証がかかり、Button クリックで
    // もう一度検証かかかるから(デフォルトでそういう仕様)
    function ClientValidate(sender, args) {
      // 正規表現パターン(アルファベット 5 文字)            
      if (args.Value.match(/^[a-zA-Z]{5}$/)) {
        $.ajax({
          type: "POST",
          async: false,
          url: "0180-CustomValidatorAjax.aspx/ValidateId",
          data: '{"id":"' + args.Value + '"}',
          contentType: "application/json; charset=utf-8",

          success: function (data) {
            // .NET 3.5 で追加された d パラメータの処置
            if (data.hasOwnProperty('d')) {
              data = data.d;
            }
            if (data) {
              args.IsValid = true;
            } else {
              args.IsValid = false;
            }
          },

          error: function (jqXHR, textStatus, errorThrown) {
            args.IsValid = false;
          }
        });
        } else {
          args.IsValid = false;
        }
      }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
    
  <asp:TextBox ID="TextBox1" runat="server">
  </asp:TextBox>
    
  <asp:RequiredFieldValidator 
    ID="RequiredFieldValidator1" 
    runat="server" 
    ErrorMessage="必須入力です" 
    Display="Dynamic"
    ControlToValidate="TextBox1" 
    ForeColor="Red">
  </asp:RequiredFieldValidator>

  <asp:CustomValidator 
    ID="CustomValidator1" 
    runat="server" 
    ErrorMessage="CustomValidator" 
    ControlToValidate="TextBox1" 
    Display="Dynamic"
    ForeColor="Red" 
    OnServerValidate="ServerValidate" 
    ClientValidationFunction="ClientValidate">
  </asp:CustomValidator>

  <br />
  <asp:Button ID="Button1" runat="server" 
    Text="送信" OnClick="Button1_Click" />
  <br />
  <asp:Label ID="Label1" runat="server"></asp:Label>
  </form>
</body>
</html>

Tags: ,

Validation

DataType 属性による検証

by WebSurfer 2016年3月8日 17:21

データアノテーション検証でのデフォルトのエラーメッセージと DataType 属性による検証についておぼえておいた方がよさそうなことを備忘録として書きます。

エラーメッセージ

上の画像は、下のサンプルコードの Model のパブリックプロパティに付与したデータアノテーション属性によるエラーメッセージです。

(注1:上に表示されているエラーメッセージは全てクライアント側での検証結果によるものです。)

(注2:BrithDate (html で input type="date" となる) はブラウザ依存の動きとなります。IE9, IE11 では自由な文字列の入力ができ、0001 で何故かクライアント側での検証はパスします。ただし、サーバー側で検証がかかって、コントローラーで ModelState.IsValid は false になります。エラーメッセージは上記と違って「値 '0001' は BirthDate に対して無効です」となります)

なお、自分が検証に使った環境は Vista SP2 32-bit, .NET 4, Visual Studio 2010 プロフェッショナル, MVC4 のインターネットアプリケーションのテンプレート使用、ASP.NET 開発サーバー利用、IE9, IE11 です。

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

namespace Mvc4App2.Models
{
  public class DefaultErrorMessage
  {
    [Required]
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [RegularExpression(@"^(?=.*\d)(?=.*[A-Z])[A-Z0-9]{4,8}$")]
    public string PassWord { get; set; }

    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [DataType(DataType.PhoneNumber)]
    public string Phone { get; set; }

    [DataType(DataType.Date)]
    public DateTime BirthDate { get; set; }

    [DataType(DataType.Url)]
    public string Url { get; set; }

    [StringLength(10)]
    [DataType(DataType.MultilineText)]
    public string Message { get; set; }
  }
}

各属性の ErrorMessage プロパティに何も設定しなくても上の画像のようにデフォルトでエラーメッセージが出ます。

しかし、正規表現がそのまま出たり(「フィールド PassWord は正規表現 '^(?=.*\d)(?=.*[A-Z])[A-Z0-9]{4,8}$' と一致する必要があります。」とか言われても一般ユーザーには訳がわからない)、一部はローカル化されておらず英文のままだったりとユーザーフレンドリーとは言えないようです。

さらに、MSDN フォーラムのスレッドASP.NET MVC5 でVaridateメッセージが英語で初めて知りましたが、自動的にはローカル化されず全部英語になってしまうこともあるようです。

また、DataType 属性については、付与しても検証は行われずエラーメッセージは出ないものがありますので注意が必要です。上の例では Password, PhoneNumber, MultilineText では検証が行われていません。

Password と MultilineText にはエラーメッセージが出ていますが、それは DataType 属性によるものではなく、追加で付与した RegularExpression 属性と StringLength 属性によるものです。

一方、EmailAddress, Date, Url は検証がかかります。クライアント側での検証はブラウザ依存になると思いますので自分が試した IE9 以外で同じ結果になるかは分かりませんが(未検証・未確認です)。なので、DataType 属性による検証に頼らず、自分で StringLength 属性や RegularExpression 属性等を使ってきちんと検証したほうがよさそうです。

ただし、DataType 属性を付与することによるメリットは検証以外にもあります。それは、View のコードに全て Html.EditorFor を利用しても、DataType 属性によって生成される html ソースの input 要素の type 属性が変わってくる(MultilineText の場合は input 要素でなく、textarea 要素になる)ことです。ちなみに、上の Model の DataType 属性の設定では以下のようになります。

<input ... name="Name" type="text" value="" />
            
<input ... name="PassWord" type="password" value="" />
            
<input ... name="Email" type="email" value="" />
            
<input ... name="Phone" type="tel" value="" />
            
<input ... name="BirthDate" type="date" value="0001/01/01" />
            
<input ... name="Url" type="url" value="" />

<textarea ... id="Message" name="Message"></textarea>

それゆえ、Password と MultilineText にいては DataType 属性を付与するのがよさそうです。他のデータアノテーション属性による検証とバッティングすることもなさそうですし。

以上のような訳で、DataType 属性による検証やデフォルトのエラーメッセージでは期待した結果を得るのは難しいので、

  1. DataType 属性による検証は利用せず Range, RegularExpression, Required, StringLength 属性などを組み合わせて使う。
  2. DataType 属性を使用するのは、Password や MultilineText のように、レンダリングされる html ソースの input 要素の type 属性を "password" にしたいとか、input 要素でなく textarea 要素にしたい場合に限る。
  3. エラーメッセージは自分で各データアノテーション属性の ErrorMessage プロパティに設定する。(先の記事「コレクションのデータアノテーション検証」参照)
  4. Model のコード内にエラーメッセージをハードコーディングしたくない場合は自分でリソースファイル (.resx) を作って使う。(先の記事「データアノテーション検証の多言語対応」参照)
  5. エラーメッセージを多言語化する場合は自分でリソースファイルを追加してサテライトアセンブリを作って使う。(同じく、先の記事「データアノテーション検証の多言語対応」参照)

というようにするのが正解だと思いました。

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

ErrorMessage プロパティの設定は行わず、デフォルトの日本語のエラーメッセージを利用したいのであれば、Azure など特にサーバーが外国にある場合は Culture, UICulture を "auto" に設定するのを忘れないようにしてください。

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

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

Culture, UICulture を "auto" に設定するのを忘れるとブラウザの言語設定は無視されます。デフォルトではシステムのロケールに該当するカルチャがスレッドに設定されますので、例えば英語 OS では英語のリソースから UI に表示されるテキストを取得します。(日本語のリソースがあっても使われません)

Tags: ,

Validation

About this blog

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

Calendar

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

View posts in large calendar