次の方法で共有


すてきな ASP.NET

ASP.NET MVC 2.0 でのモデルの検証とメタデータ

K. Scott Allen

コード サンプルのダウンロード

ASP.NET MVC 2.0 のリリースに追加された新機能の 1 つに、サーバー側とクライアント側の両方でユーザー入力を検証する機能があります。検証する必要のあるデータに関する情報をいくつかフレームワークに提供するだけで、フレームワークによって多くの作業が細部まで行われます。

ASP.NET MVC 1.0 で簡単なモデルの検証を行うためにカスタム検証コードとカスタム モデル バインダーを作成していたユーザーにとっては、この機能が大きなメリットをもたらします。今回のコラムでは、ASP.NET MVC 2.0 に組み込まれた検証サポートを紹介します。

新機能について説明する前に、まず、以前の検証方法に触れておきましょう。私は何年にもわたって ASP.NET WebForms の検証機能を利用してきました。こうした検証機能を復習し、理想的な検証フレームワークが何をもたらすかを理解することが役に立つと考えます。

検証を制御する

これまでに ASP.NET WebForms を使用したことがあれば、WebForm には比較的簡単に検証ロジックを追加できることをご存知でしょう。検証の規則は、コントロールを使用して表現します。たとえば、ユーザーがなんらかのテキストを必ず TextBox コントロールに入力するようにするには、次のように、TextBox を指している RequiredFieldValidator コントロールを追加するだけです。

<form id="form1" runat="server">
  <asp:TextBox runat="server" ID="_userName" />
  <asp:RequiredFieldValidator runat="server" ControlToValidate="_userName"
                               ErrorMessage="Please enter a username" />
  <asp:Button runat="server" ID="_submit" Text="Submit" />
</form>

RequiredFieldValidator は、ユーザーがユーザー名を必ず入力するように、クライアント側とサーバー側の両方のロジックをカプセル化します。クライアント側の検証を行うため、RequiredFieldValidator コントロールは JavaScript をクライアントのブラウザーに発行します。このスクリプトは、サーバーにフォームをポストバックする前に、ユーザーが必ずすべての検証規則を満たすようにします。

このような WebForm の検証コントロールがもたらす次のような点を考えれば、こうしたコントロールが非常に強力なものであることがわかります。

  • ページの検証規則を 1 か所で、宣言によって表現できます。
  • クライアントで検証を行うことにより、ユーザーが検証規則を満たしていない場合にサーバーとのラウンド トリップを防ぎます。
  • サーバーで検証を行うことにより、悪意のあるユーザーがクライアント スクリプトを迂回できないようにします。
  • サーバーの検証ロジックとクライアントの検証ロジックの同期が維持され、メンテナンスによって問題が生じることはありません。

しかし、ASP.NET MVC ではこのような検証コントロールを使用できないため、MVC 設計パターンの精神に忠実であり続けることができません。さいわい、フレームワークのバージョン 2 では、こうした点がいくらか改善されます。

コントロールとモデルの比較

TextBox のような WebForm コントロールは、ユーザー データの単なるコンテナーと考えることができます。コントロールでは、初期値を設定してユーザーに表示できます。さらに、ポストバック後にコントロールを調べて、ユーザーが入力または編集した値を取得できます。MVC 設計パターンを使用しているときは、M (モデル) がデータ コンテナーと同じ役割を果たします。モデルには、ユーザーに渡す必要のある情報を設定し、モデルからは更新された値がアプリケーションに返送されます。したがって、検証の規則や制約を表現するには、モデルが理想的な場所になります。

ここで、フレームワークに既定で組み込まれている検証の例を紹介します。新しく ASP.NET MVC 2.0 アプリケーションを作成すると、新しいプロジェクトには AccountController というコントローラーが含まれているのがわかります。このコントローラーには、新規ユーザーの登録要求、ログイン要求、およびパスワードの変更要求を処理する役割があります。これらの操作は、それぞれ専用のモデル オブジェクトを使用します。こうしたモデルは、Models フォルダーの AccountModels.cs ファイルにあります。たとえば、RegisterModel クラスには検証の規則がなく、次のようになります。

public class RegisterModel
{
  public string UserName { get; set; }
  public string Email { get; set; }
  public string Password { get; set; }
  public string ConfirmPassword { get; set; }
}

AccountController の Register 操作は、この RegisterModel クラスのインスタンスをパラメーターとして受け取ります。

[HttpPost]
public ActionResult Register(RegisterModel model)
{
    // ...
}

受け取ったモデルが有効であれば、Register 操作はそのモデル情報を、新しいユーザーを作成できるサービスに転送します。

RegisterModel モデルは、"ビュー固有のモデル" (ビュー モデル) の優れた例です。このモデルは、具体的なデータベース テーブル、Web サービス呼び出し、またはビジネス オブジェクトを操作するように設計されたモデルではなく、特定のビュー (Register.aspx ビュー。図 1 にその一部を示しています) を操作するように設計されたモデルです。モデルの各プロパティは、ビューの入力コントロールに対応しています。ビュー モデルでは、検証を含めて、MVC 開発の多くのシナリオが簡略化されているため、ビュー モデルを使用することをお勧めします。


図 1 情報の登録

モデルとメタデータ

ユーザーが Register ビューにアカウント情報を入力するときに、MVC フレームワークは、ユーザーが UserName と Email を必ず入力するようにします。また、Password 文字列と ConfirmPassword 文字列が必ず一致し、Password 文字列に 6 文字以上入力するようにします。ここまでをフレームワークが処理します。これは、フレームワークが、RegisterModel クラスにアタッチされているメタデータを調べ、そのメタデータに従って動作することで行われます。図 2 は、検証属性を備えた RegisterModel クラスを示します。

図 2 検証属性を備えた RegisterModel クラス

[PropertiesMustMatch("Password", "ConfirmPassword", 
  ErrorMessage = "The password and confirmation password do not match.")]
public class RegisterModel
{
  [Required]        
  public string UserName { get; set; }

  [Required]
  public string Email { get; set; }

  [Required]
  [ValidatePasswordLength]
  public string Password { get; set; }

  [Required]
  public string ConfirmPassword { get; set; }
}

Register ビューをユーザーが送信すると、ASP.NET MVC 既定のモデル バインダーが、AccountController の Register 操作にパラメーターとして渡す、RegisterModel クラスの新しいインスタンスの構築を試みます。モデル バインダーは、現在の要求に含まれる情報を取得して、RegisterModel オブジェクトに設定します。たとえば、バインダーは UserName という HTML 入力コントロールの POST 値を自動的に検索して、その値を、RegisterModel の UserName プロパティの設定に使用します。これは、バージョン 1.0 から ASP.NET MVC に備わっている動作なので、フレームワークを使用したことがある方には新しいものではありません。

バージョン 2 で新しくなったのは、RegisterModel オブジェクトに使用できるメタデータがあるかどうか、既定のモデル バインダーから "メタデータのプロバイダー" に問い合わせる方法です。この処理では最終的に ModelMetaData 派生オブジェクトが生成されます。このオブジェクトの目的は、モデルに関連付けられる検証規則だけでなく、ビューでのモデルの表示に関連する情報も記述することです。ASP.NET のチーム メンバーである Brad Wilson は、このモデル メタデータが、テンプレートを通じて、モデルの表示にどのように影響を及ぼすかを詳しく説明した一連のブログを投稿しています。最初の投稿は、bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-1-introduction.html (英語) から参照できます。

モデル バインダーは、モデルと ModelMetaData オブジェクトを関連付けたら、内部の検証メタデータを使用して、モデル オブジェクトを検証できます。既定では、ASP.NET MVC は [Required] のようなデータ注釈属性のメタデータを使用します。もちろん、ASP.NET MVC はプラグ可能かつ拡張可能なので、モデル メタデータに別のソースを考えているのであれば、独自のメタデータ プロバイダーを実装できます。Ben Scheirman の「Customizing ASP.NET MVC 2—Metadata and Validation」(ASP.NET MVC 2.0 をカスタマイズする - メタデータと検証、英語) というコラムに、このトピックに関する有益な情報があります (dotnetslackers.com/articles/aspnet/customizing-asp-net-mvc-2-metadata-and-validation.aspx、英語)。

データの注釈

少し本題から外れます。独自の検証属性を構築することはできますが (後ほど説明します)、[Required] は、System.ComponentModel.DataAnnotations アセンブリ内に存在する、数多くの標準検証属性のうちの 1 つです。図 3 は、この注釈のアセンブリに含まれる検証属性の全一覧を示しています。

図 3 注釈のアセンブリにある検証属性

属性 説明
StringLength データ フィールドに収容できる文字列の最大文字数を指定します。
Required データ フィールド値が必須であることを指定します。
RegularExpression データ フィールド値が、指定した正規表現と一致する必要があることを指定します。
Range データ フィールド値の数値範囲制約を指定します。
DataType データ フィールドに追加で関連付ける型名を指定します (EmailAddress、Url、Password など、いずれかの DataType 列挙値)。

このようなデータ注釈属性は、Microsoft .NET Framework 全体に急速に広がる傾向にあります。ASP.NET MVC アプリケーションだけでなく、ASP.NET Dynamic Data、Silverlight、および Silverlight RIA Services も、これらの属性を同様に認識します。

検証の表示

検証メタデータを適切に使用すれば、ユーザーが不適切なデータを入力したときに、エラーがビューに自動的に表示されます。図 4 では、ユーザーが情報を何も入力せずに [Register] を押したとき、Register ビューにどのように表示されるかを示しています。


図 4 検証の失敗

図 4 の表示は、ValidationMessageFor ヘルパーなどの、ASP.NET MVC 2.0 の新しい HTML ヘルパーをいくつか使用して構築しました。ValidationMessageFor は、特定のデータ フィールドで検証が失敗したときの検証メッセージの配置を制御します。図 5 は Register.aspx からの抜粋で、ValidationMessageFor ヘルパーと ValidationSummary ヘルパーの使用方法を示しています。

図 5 新しい HTML ヘルパーの使用法

<% using (Html.BeginForm()) { %>
    <%= Html.ValidationSummary(true, "Account creation was unsuccessful. " +
    "Please correct the errors and try again.") %>
    <div>
        <fieldset>
            <legend>Account Information</legend>
            
            <div class="editor-label">
                <%= Html.LabelFor(m => m.UserName) %>
            </div>
            <div class="editor-field">
                <%= Html.TextBoxFor(m => m.UserName) %>
                <%= Html.ValidationMessageFor(m => m.UserName) %>
            </div>

カスタム検証

RegisterModel クラスのすべての検証属性が、マイクロソフトのデータ注釈のアセンブリに含まれる属性というわけではありません。[PropertiesMustMatch] と [ValidatePasswordLength] は、RegisterModel クラスを保持する AccountModel.cs ファイルで定義されるカスタム属性です。カスタム検証規則を提供するだけなら、カスタム メタデータ プロバイダーや、カスタム メタデータ クラスについて考慮するは必要はなく、ValidationAttribute 抽象クラスから派生して、IsValid メソッドに実装を提供するだけでかまいません。ValidatePasswordLength 属性の実装を図 6 に示します。

図 6 ValidatePasswordLength 属性の実装

[AttributeUsage(AttributeTargets.Field | 
                AttributeTargets.Property, 
                AllowMultiple = false, 
                Inherited = true)]
public sealed class ValidatePasswordLengthAttribute 
    : ValidationAttribute
{
    private const string _defaultErrorMessage = 
        "’{0}’ must be at least {1} characters long.";

    private readonly int _minCharacters = 
        Membership.Provider.MinRequiredPasswordLength;

    public ValidatePasswordLengthAttribute()
        : base(_defaultErrorMessage)
    {
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, 
            ErrorMessageString,
            name, _minCharacters);
    }

    public override bool IsValid(object value)
    {
        string valueAsString = value as string;
        return (valueAsString != null && 
            valueAsString.Length >= _minCharacters);
    }
}

もう 1 つの属性 PropertiesMustMatch は、複数のプロパティ間の検証を実行するために、クラス レベルで適用できる検証属性の好例です。

クライアントの検証

これまで見てきた RegisterModel の検証は、すべてがサーバー側で行われます。さいわい、クライアント側でも簡単に検証を実行できます。サーバーの作業負荷をいくぶん低減できると同時に、ユーザーへのフィードバックが迅速になるため、できる限りクライアント側で検証を行うようにします。ただし、ブラウザーではスクリプトが無効になっている場合 (または、ユーザーが意図的に有害なデータをサーバーに送信しようと試みる場合) があるため、サーバー側のロジックもそのまま保持しておく必要があります。

クライアントでの検証を有効にするプロセスは、2 つの手順で構成されます。まず、最初の手順では、適切な検証スクリプトをビューに含めるようにします。必要なスクリプトはすべて、新しい MVC アプリケーションの Scripts フォルダーに含める必要があります。最初にビューに含める必要のあるスクリプトは、Microsoft AJAX ライブラリの中核である MicrosoftAjax.js スクリプトです。2 番目に含めるスクリプトは、MicrosoftMvcValidation.js です。次のように、通常、スクリプトを保持するために MVC アプリケーションのマスター ページに ContentPlaceHolder を追加します。

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat=
"server" /></title>
    <link href="../../Content/Site.css" rel="stylesheet" type=
"text/css" />

    <asp:ContentPlaceHolder ID="Scripts" runat="server">
       
    </asp:ContentPlaceHolder>
    
</head>

その後、必要なスクリプトを Content コントロールを使用してビューに含めることができます。次のコードにより、検証スクリプトが確実に存在するようになります。

<asp:Content ContentPlaceHolderID="Scripts" runat="server">
    <script src="../../Scripts/MicrosoftAjax.js" 
            type="text/javascript"></script>
    <script src="../../Scripts/MicrosoftMvcValidation.js" 
            type="text/javascript"></script>
</asp:Content>

次に、クライアント側の検証を使用して、検証サポートが必要なビュー内部から EnableClientValidation HTML ヘルパー メソッドを呼び出します。次のように、BeginForm HTML ヘルパーを使用する前にこのメソッドを呼び出すようにしてください。

<%
       Html.EnableClientValidation(); 
       using (Html.BeginForm())
        {
     %>
     
     <!-- the rest of the form ... -->
     
     <% } %>

クライアント側の検証ロジックは、組み込みの検証属性のみを操作することに注意してください。つまり、Register ビューの場合、クライアント側の検証によって必須フィールドが確実に存在するようにはできますが、パスワードの長さを検証する方法や、2 つのパスワードが一致することを確認する方法はありません。さいわい、ASP.NET MVC JavaScript 検証フレームワークにプラグインされるカスタムの JavaScript 検証ロジックを簡単に追加することができます。Phil Haack が、「ASP.NET MVC 2 Custom Validation」(ASP.NET MVC 2.0 のカスタム検証、英語) というブログ エントリでこのことを詳しく説明しています (haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx、英語)。

まとめると、共通する検証のシナリオのサポートが組み込まれていることは、ASP.NET MVC 2.0 に追加された非常に大きな特徴です。属性を使用してモデル オブジェクトに検証規則を簡単に追加できるだけでなく、検証機能自体に柔軟性があるので、簡単に拡張することが可能です。次期バージョンの ASP.NET MVC アプリケーションでこれらの機能を利用して、時間とコード行を節約してみてはいかがでしょうか。

K. Scott Allen は Pluralsight 技術スタッフのメンバーであり、OdeToCode の創設者です。彼の連絡先は scott@OdeToCode.com (英語のみ)、ブログのアドレスは odetocode.com/blogs/scott (英語) です。Twiter から連絡するには、twitter.com/OdeToCode (英語) にアクセスしてください。

この記事のレビューに協力してくれた技術スタッフの Brad Wilson に心より感謝いたします。