MVC4でのクライアント検証

Category: fx aspnet_ja

Question

MMTRS on Fri, 26 Sep 2014 11:00:23


Visual Studio Express 2013 for Web で MVC4 Web アプリケーション開発の勉強中です。
言語は VB です。

MVC3 のものですが、下記のサイトを見ながら勉強していますが、クライアント検証がうまくいかず行き詰っています。
http://www.atmarkit.co.jp/fdotnet/aspnetmvc3/index/

状況としては、上記サイトで紹介されているサンプルで、Books/Create の「出版社」の項目に、本来エラーとなるはずの内容を入力しても、クライアント側の検証が機能していないのか、エラーメッセージが表示されず、そのままサーバー側に送信すると、再度 Books/Create の画面が表示されて「出版社」の項目にカーソルが移るのでエラーの扱いにはなっているようなのですが、こちらもエラーメッセージが表示されません。

デバッグ実行をしてみると、項目への入力時に独自検証ルールの function で false が返されてはいるようです。

MVC3 と MVC4 では上記に関係するところで何か変更が必要でしょうか。
もしくは上記のサイトに書かれていない部分で注意点などありましたら、教えていただけると嬉しいです。

ソースコードについては何度も見直しましたので、jQuery のバージョンに関する記述等の一部を除いては、上記サイトを順に進めていったものと同じになっていると思いますし、どこを提示したら良いかがわからないというのもあり、必要に応じて追って提示させていただければと思います。

最低限の条件として、Web.config に以下は記述してあります。

<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />

よろしくお願いします。

Replies

SurferOnWww on Fri, 26 Sep 2014 12:00:37


具体的にどうやったのか分かりませんが(参考にされてるページを隅から隅までよく読めば分かると言わないでくださいね)、MVC4 のインターネットアプリケーションのテンプレートで作ったアプリなら必要な設定はすでにされているので、難しいことは何もないはずなんですが。

ASP.NET がレンダリングする html 要素に、クライアントサイドでの jQuery ライブラリによる検証に必要な属性(例: data-val="true" など)は追加されているでしょうか? まずそこを確認してください。

@IT の記事を読んで議論するは回答者にとって負担が大きいです。以下のページの簡単なサンプルのような短いコードで、コピペすれば動くもので話を進めませんか?

http://social.msdn.microsoft.com/Forums/ja-JP/645d38d0-5849-4866-a5cc-4027bb97eeea/aspnet-mvc?forum=aspnetja

 

MMTRS on Fri, 26 Sep 2014 12:43:28


SurferOnWwwさん、ありがとうございます。

>ASP.NET がレンダリングする html 要素に、クライアントサイドでの jQuery ライブラリによる検証に必要な属性(例: data-val="true" など)は追加されているでしょうか? まずそこを確認してください。

以下の部分でしょうか。

<div class="editor-label">
    <label for="Publish">出版社</label>
</div>
<div class="editor-field">
    <input class="text-box single-line" data-val="true" data-val-inarray="出版社は翔泳社,技術評論社,秀和システム,毎日コミュニケーションズ,日経BP社,インプレスジャパンのいずれかで指定して下さい。" data-val-inarray-opts="翔泳社,技術評論社,秀和システム,毎日コミュニケーションズ,日経BP社,インプレスジャパン" data-val-length="出版社は30文字以内で入力して下さい。" data-val-length-max="30" id="Publish" name="Publish" type="text" value="" />
    <span class="field-validation-valid" data-valmsg-for="Publish" data-valmsg-replace="true"></span>
</div>


data-val-inarray と data-val-inarray-opts がそれにあたるかと思います。

>@IT の記事を読んで議論するは回答者にとって負担が大きいです。以下のページの簡単なサンプルのような短いコードで、コピペすれば動くもので話を進めませんか?

そうですね、大変失礼しました。
以下に実際のコードを記載します。
もし足りないものがありましたら再度お願いします。

●Model(Books.vb)

Imports System.ComponentModel
Imports System.ComponentModel.DataAnnotations

Public Class Book

    <Key()>
    <DisplayName("ISBNコード")>
    <Required(ErrorMessage:="{0}は必須です。")>
    <RegularExpression("[0-9]{3}-[0-9]{1}-[0-9]{3,5}-[0-9]{3,5}-[0-9A-Z]{1}", ErrorMessage:="{0}はISBNの形式で入力して下さい。")>
    Public Property Isbn() As String                                    'ISBNコード

    <DisplayName("書名")>
    <Required(ErrorMessage:="{0}は必須です。")>
    <StringLength(100, ErrorMessage:="{0}は{1}文字以内で入力して下さい。")>
    Public Property Title() As String                                   '書名

    <DisplayName("価格")>
    <Range(100, 100000, ErrorMessage:="{0}は{1}~{2}の間で入力して下さい。")>
    Public Property Price() As Integer?                                 '価格

    <DisplayName("出版社")>
    <InArray("翔泳社,技術評論社,秀和システム,毎日コミュニケーションズ,日経BP社,インプレスジャパン")>
    <StringLength(30, ErrorMessage:="{0}は{1}文字以内で入力して下さい。")>
    Public Property Publish() As String                                 '出版社

    <DisplayName("刊行日")>
    <Required(ErrorMessage:="{0}は必須です。")>
    Public Property Published() As Date                                 '刊行日

    Public Overridable Property Reviews() As ICollection(Of Review)     'レビュー

End Class


●Controller(BooksController.vb)

Imports System.Data.Entity

Public Class BooksController
    Inherits System.Web.Mvc.Controller

    Private db As New MyMvcContext

    '
    ' GET: /Books/

    Function Index() As ActionResult
        Return View(db.Books.ToList())
    End Function

    '
    ' GET: /Books/Details/5

    Function Details(Optional ByVal id As String = Nothing) As ActionResult
        Dim book As Book = db.Books.Find(id)
        If IsNothing(book) Then
            Return HttpNotFound()
        End If
        Return View(book)
    End Function

    '
    ' GET: /Books/Create

    Function Create() As ActionResult
        Return View()
    End Function

    '
    ' POST: /Books/Create

    <HttpPost()> _
    <ValidateAntiForgeryToken()> _
    Function Create(ByVal book As Book) As ActionResult
        If ModelState.IsValid Then
            db.Books.Add(book)
            db.SaveChanges()
            Return RedirectToAction("Index")
        End If

        Return View(book)
    End Function

    '
    ' GET: /Books/Edit/5

    Function Edit(Optional ByVal id As String = Nothing) As ActionResult
        Dim book As Book = db.Books.Find(id)
        If IsNothing(book) Then
            Return HttpNotFound()
        End If
        Return View(book)
    End Function

    '
    ' POST: /Books/Edit/5

    <HttpPost()> _
    <ValidateAntiForgeryToken()> _
    Function Edit(ByVal book As Book) As ActionResult
        If ModelState.IsValid Then
            db.Entry(book).State = EntityState.Modified
            db.SaveChanges()
            Return RedirectToAction("Index")
        End If

        Return View(book)
    End Function

    '
    ' GET: /Books/Delete/5

    Function Delete(Optional ByVal id As String = Nothing) As ActionResult
        Dim book As Book = db.Books.Find(id)
        If IsNothing(book) Then
            Return HttpNotFound()
        End If
        Return View(book)
    End Function

    '
    ' POST: /Books/Delete/5

    <HttpPost()> _
    <ActionName("Delete")> _
    <ValidateAntiForgeryToken()> _
    Function DeleteConfirmed(ByVal id As String) As RedirectToRouteResult
        Dim book As Book = db.Books.Find(id)
        db.Books.Remove(book)
        db.SaveChanges()
        Return RedirectToAction("Index")
    End Function

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        db.Dispose()
        MyBase.Dispose(disposing)
    End Sub

End Class

●View(Books/Create.vbhtml)

@ModelType MvcWebApp.Book

@Code
    ViewData("Title") = "Create"
End Code

<h2>Create</h2>

<script src="@Url.Content("~/Scripts/jquery-1.8.2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/InArray.js")" type="text/javascript"></script>

@Using Html.BeginForm()
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(True)

    @<fieldset>
        <legend>Book</legend>

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

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

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

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

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

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
End Using

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@Section Scripts
    @Scripts.Render("~/bundles/jqueryval")
End Section

●検証属性定義(InArrayAttribute.vb)

Imports System.ComponentModel.DataAnnotations
Imports System.Globalization

<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False)>
Public Class InArrayAttribute
    Inherits ValidationAttribute
    Implements IClientValidatable

    Private _opts As String

    Public Sub New(ByVal opts As String)
        _opts = opts
        ErrorMessage = "{0}は{1}のいずれかで指定して下さい。"
    End Sub

    Public Overrides Function FormatErrorMessage(name As String) As String

        Return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _opts)

    End Function

    Public Overrides Function IsValid(value As Object) As Boolean

        If value Is Nothing Then Return True

        If Array.IndexOf(_opts.Split(","c), value) = -1 Then
            Return False
        End If

        Return True

    End Function

    '' クライアントに送信する検証情報の生成
    Public Function GetClientValidationRules(ByVal metadata As ModelMetadata, ByVal context As ControllerContext) As IEnumerable(Of ModelClientValidationRule) Implements IClientValidatable.GetClientValidationRules

        '' 検証ルールを準備(検証名、エラーメッセージ)
        Dim rule As New ModelClientValidationRule() With { _
            .ValidationType = "inarray", _
            .ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()) _
        }

        rule.ValidationParameters("opts") = _opts                               '検証パラメータ

        Dim list As New List(Of ModelClientValidationRule)()
        list.Add(rule)

        Return list

    End Function

End Class

●検証JavaScript(InArray.js)

// インテリセンス機能を有効化
/// <reference path="jquery-1.8.2.intellisense.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.min.js" />

// inarray検証をjQuery Validationに登録
$.validator.addMethod('inarray',
    function (value, element, param) {

        // 入力値が空の場合は検証をスキップ
        value = $.trim(value);
        if (value === '') {
            return true;
        }

        // カンマ区切りテキストを分解し、入力値valueと比較
        if ($.inArray(value, param.split(',')) === -1) {
            return false;
        }

        return true;

    }
);

// inarray検証と、そのパラメータoptsを登録
$.validator.unobtrusive.adapters.addSingleVal('inarray', 'opts');

以上です。

長くなりましたが、よろしくお願いします。

SurferOnWww on Fri, 26 Sep 2014 14:22:12


> 以下に実際のコードを記載します。
> もし足りないものがありましたら再度お願いします。

それ、コピペするだけでは動かないですよ。SQL Server データベースと EDM が必要です。

Data Annotation 検証でクライアント側の検証が動くかどうか見るだけならもっと簡単なコードで済むでしょう。先に紹介したページのような。

SurferOnWww on Sat, 27 Sep 2014 01:21:30


【追伸】
アップされたコードは検証してませんが、ざっと見た限り View が間違っているような気がします。

MVC4 ではJavaScript/CSS ファイルの縮小化と統合処理の自動機能が追加されています。詳しくは下記ページ参照。

Bundling and Minification
http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification

それと、その機能のない MVC3 のコードがごっちゃになっているようです。

縮小化と統合処理は、上のページに書いてある通り、開発中(compilation debug="true")は無効になりますが、その場合でも外部スクリプトファイルの定義は Scripts.Render メソッドでレンダリングされます。

自分の環境 VS2010 MVC4 のインターネットテンプレートで作った Web アプリの場合、マスターページ (_Layout.cshtml) で @Scripts.Render("~/bundles/jquery") は定義され、ウィザードで自動生成するビュー Create.cshtml に @Scripts.Render("~/bundles/jqueryval") が定義されます。

その結果、以下の外部スクリプトファイルの定義がレンダリングされます。

<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>

なので、自力でビューに Url.Content を書いて定義する必要はないはずです。

まずはそのあたりに注意して修正してみてください。

SurferOnWww on Mon, 29 Sep 2014 08:08:36


質問者さんが出てこないので、このスレッドは未解決で放置になってしまいそうですが、それも何なので、自分が @IT の記事(MVC3 ベース)の検証関係のコードを、VS2010 MVC4 を使って確認した結果を書いておきます。

結論から言えば、自作検証属性を含め、JavaScript / jQuery によるクライアントサイドの検証はすべて期待通り動きました。

問題は、MVC4 のインターネットテンプレートでは JavaScript ファイルの自動バンドル処理が追加されているのに、それが使えてなかったからです。

マスターページ (_Layout.cshtml)

MVC4 のインターネットテンプレートで自動生成されるマスターページは以下のようになっています。

<!DOCTYPE html>

・・・中略・・・

@Scripts.Render("~/bundles/jquery") @RenderSection("scripts", required: false) </body> </html>

@Scripts.Render("~/bundles/jquery") の位置に jquery.js の定義が、@RenderSection("scripts", required: false) の位置に検証用外部スクリプトファイルの定義が(ビューによって)レンダリングされます。


App_Start/BundleConfig.cs

自作検証用のスクリプトファイルをバンドルできるように以下のように定義を追加します。

namespace Mvc4App
{
    public class BundleConfig
    {        
        public static void RegisterBundles(BundleCollection bundles)
        {
            // 中略

            // 下記を追加
            bundles.Add(new ScriptBundle("~/bundles/inarray").Include(            
                        "~/Scripts/InArray.js"));

            // 中略
        }
    }
}

ビュー (Create.cshtml)

ビューには @Scripts.Render("~/bundles/jqueryval") というコードが自動生成されているはずです。それに "~/bundles/inarray" を追記します。以下のようになります。

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

質問者さんがアップしたビューのコードにある外部スクリプトファイルの定義は一切不要です。

MMTRS on Mon, 29 Sep 2014 10:28:11


SurferOnWwwさん、ありがとうございます。

今すぐは確認できないので、後ほど確認して改めてご報告します。

MMTRS on Mon, 29 Sep 2014 12:19:00


無事にできました。大変助かりました。

ありがとうございます。