WebSurfer's Home

Filter by APML

CheckBox と DropDownList の検証 (CORE)

by WebSurfer 7. March 2021 14:44

CheckBox には、例えばユーザーが条件を許諾したという意思表示のため、チェックを入れるのが必須というケースがあると思います。その検証をクライアント側とサーバー側の両方で行うためのカスタム検証属性を考えてみました。

CheckBox の検証

CheckBox の場合 RequiredAttribute ではクライアント側でもサーバー側でも検証はかかりません。RangeAttribute を使って [Range(typeof(bool), "true", "true")] というように設定するという手段がありますが、クライアント側での検証がかかりません。

というわけで、先の記事「ASP.NET Core MVC 検証属性の自作」とか「ファイルアップロード時の検証 (CORE)」で書いたようなカスタム検証属性を作って利用するのが良さそうです。

DropDownList にもそのようなカスタム検証属性を作ってみようと思いましたが、未選択で検証 NG とする場合は一番最初の option 要素が value="" となっていれば RequiredAttribute でクライアント/サーバー両方で検証がかかること、特定の項目を選ばなければならないというのは他の条件も絡むであろうから先の記事「CustomValidation 属性」で書いたようにすべきで、意味がなさそうなので止めました。

以下にそのコードをアップしておきます。上の画像を表示したものです。DropDownList 関係のコードもせっかく書いたので一緒に載せておきます。

Model とカスタム検証属性

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Models
{
    // ビューモデル
    public class Partner
    {
        [Required(ErrorMessage = "{0} is required.")]
        public string Name { get; set; }

        // Required 属性では検証はかからない
        [Required(ErrorMessage = "{0} is required.")]
        [CheckBoxValidate(true)]
        public bool PartnerAccepted { get; set; }

        [Required(ErrorMessage = "{0} is required.")]
        public string Area { get; set; }

        public SelectList AreaList { get; set; }
    }

    // DropDownList に渡す SelectList 生成用のクラス
    // AreaId は int? または string 型にしておけば、SelectList を
    // 生成する際、下の Controller のコード例のように null を設定
    // できる。結果 option value="" となり未選択で検証 NG とできる
    public class Area
    {
        public int? AreaId { set; get; }
        public string AreaName { set; get; }
    }

    // CheckBox 用の検証属性
    // true/false を引数で指定(要チェックなら true とする)
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CheckBoxValidateAttribute : ValidationAttribute, 
                                             IClientModelValidator
    {
        private readonly bool option;

        public CheckBoxValidateAttribute(bool option)
        {
            this.option = option;
            this.ErrorMessage = "{0} must be set to {1}.";
        }

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

        public override bool IsValid(object value)
        {
            // テキストボックスに入力された文字列が検証対象の場合は
            // ここで value が空白か否かをチェックして空白の場合は
            // true を返すようにしていた(空白の検証は Required だけで
            // 行い、その他の検証属性は空白の場合は true を返すのが
            // 原則なので)。チェックボックスの場合は ASP.NET MVC では
            // 必ず true/false のいずれかが返されるようになっている。
            // なので null が帰ってきた場合は何か良からぬことが起こって
            // いるかもしれないということで例外をスローすることにした
            if (value == null) 
            {
                throw new ArgumentNullException("CheckBoxValidate");
            }

            if ((bool)value == option)
            {
                return true;
            }
            return false;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = 
                FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate", 
                           errorMessage);
            MergeAttribute(context.Attributes, 
                           "data-val-checkboxvalidate-option", 
                           this.option.ToString());
        }

        // 上の AddValidation メソッドで使うヘルパーメソッド
        private bool MergeAttribute(IDictionary<string, string> attributes, 
                                    string key, 
                                    string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
            attributes.Add(key, value);
            return true;
        }
    }
}

Controller / Action Method

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using MvcCoreApp.Models;
using Microsoft.AspNetCore.Http;
using System.IO;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace MvcCoreApp.Controllers
{
    public class ValidationController : Controller
    {
        public IActionResult CheckBoxDropDown()
        {
            // DropDwonList に渡す SelectList の生成
            // AreaId を null に設定すると結果 option value="" となり
            // 未選択では RequiredAttribute による検証は NG となる
            var list = new List<Area>
            { 
                new Area { AreaId = null, AreaName = "--- Select One ---"},
                new Area { AreaId = 1, AreaName = "North" },
                new Area { AreaId = 2, AreaName = "South"},
                new Area { AreaId = 3, AreaName = "Eastt" },
                new Area { AreaId = 4, AreaName = "West" }
            };

            var partner = new Partner 
            { 
                AreaList = new SelectList(list, "AreaId", "AreaName")
            };

            return View(partner);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CheckBoxDropDown(Partner partner)
        {
            if (!ModelState.IsValid)
            {
                // 検証 NG で再表示する場合は SelectList も再生成要
                var list = new List<Area>
                {
                    new Area { AreaId = null, AreaName = "--- Select One ---"},
                    new Area { AreaId = 1, AreaName = "North" },
                    new Area { AreaId = 2, AreaName = "South"},
                    new Area { AreaId = 3, AreaName = "Eastt" },
                    new Area { AreaId = 4, AreaName = "West" }
                };

                partner.AreaList = new SelectList(list, "AreaId", "AreaName");

                return View(partner);
            }

            return RedirectToAction("Create", "Validation");
        }
    }
}

View

@model MvcCoreApp.Models.Partner

@{
    ViewData["Title"] = "CheckBoxDropDown";
}

<h1>CheckBoxDropDown</h1>

<h4>Pertner</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="CheckBoxDropDown">
            <div asp-validation-summary="ModelOnly" class="text-danger">
            </div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="PartnerAccepted" />
                    @Html.DisplayNameFor(model => model.PartnerAccepted)
                </label>
                <br />
                <span asp-validation-for="PartnerAccepted" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label asp-for="Area" class="control-label"></label>
                <select asp-for="Area" asp-items="Model.AreaList" 
                        class="form-control">
                </select>
                <span asp-validation-for="Area" class="text-danger"></span>
            </div>

            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

<script type="text/javascript">
    //<![CDATA[
    $.validator.addMethod("checkboxvalidate",
        function (value, element, parameters) {
            // チェックされてないと value は undefined となり
            // value では判定できないので注意
            var result = element.checked;
            var isTrue = (parameters.toUpperCase() == 'TRUE');
            if (result == isTrue) {
                return true;
            }
            return false;
        });

    $.validator.unobtrusive.adapters.
        addSingleVal('checkboxvalidate', 'option');

    //]]>
</script>
}

Tags: , , , ,

Validation

CustomValidator のクライアント側での検証

by WebSurfer 27. June 2020 12:03

ASP.NET Web Forms アプリでユーザー入力の検証に用いられる CustomValidator のクライアント側での検証について調べたことをまとめて備忘録として書いておきます。

CustomValidator のクライアント側での検証

CustomValidator は、プログラマが独自の検証ロジックをコーディングして検証メソッドとしてページに実装し、ユーザー入力の検証を行うために利用されます。複数の入力コントロールにまたがって検証を行うことも可能です。

また、RegularExpressionValidator などと異なり、TextBox の他に CheckBox, RadioButton, DropDownList, FileUpload などのユーザー入力コントロールの検証に利用できます。

クライアント側での検証は、JavaScript を使って検証用メソッドを自力でコーディングし、それをページに実装することで可能になります(サーバー側でなければ検証できない場合は話は別です。ajax を使う手はいろいろ問題がありそうです。詳しくは先の記事「CustomValidator で jQuery.ajax 利用」を見てください)。

この記事の下の方に TextBox, CheckBox, RadioButton, DropDownList, FileUpload を対象として、CustomValidator によるクライアント側での検証を実装したサンプルコードを書いておきます。上の画像を表示したものです。

自分的に注意が必要と思う点を以下に箇条書きにしておきます。

  1. クライアント側での検証は html 要素の change イベントでかかるようになっています。CustomValidator を change イベントで動くようにするには ControlToValidate プロパティの設定が必要です。(注: submit でも検証がかかります。というか、change で検証がかかるのはユーザビリティ向上のためで、submit 時の検証がメインです)
  2. CheckBox, RadioButton コントロールに対しては CustomValidator の ControlToValidate プロパティを設定できません。設定すると HttpException がスローされ、例えば CheckBox の場合は「'CustomValidator' の ControlToValidate プロパティで参照されたコントロール 'CheckBox' を検証できません。」というエラーメッセージが表示されます。

    エラーとなる直接の理由は、ASP.NET 内部で CheckControlValidationProperty メソッドによる検証対象コントロールのチェックを行っていますが、CheckBox RadioButton コントロールには ValidationPropertyAttribute 属性が付与されてないためないためです。

    そもそもの理由は、Microsoft のドキュメントによると「ControlToValidate プロパティを設定せずに CustomValidator コントロールを使用することもできます。 これは、複数の入力コントロールを検証する場合や、CheckBox コントロールなどの検証コントロールで使用できない入力コントロールを検証する場合に一般的に行われます」とのことで、もともと CheckBox や RadioButton は検証コントロールを使う対象外のように読めます。
  3. TextBox, DropDownList, FileUpload コントロールについては、CustomValidator の ControlToValidate プロパティを検証対象コントロールの ID に設定すれば change イベントで検証がかかります。

    なお、ControlToValidate プロパティを設定しなくても submit で検証はかかりますので、change イベントでいちいち検証がかかるのは煩わしいという場合は設定しない方がよさそうです。(RequiredFieldValidator など他の検証コントロールは ControlToValidate プロパティを設定しないとエラーになりますので注意してください。CustomValidator だけ特別です)。
  4. どういう html 要素がどのタイミングで change イベントを発生させるかについては MDN の記事 HTMLElement: change event を見てください。その記事に書いてある通り、TextBox はユーザーが入力してフォーカスを外した時、DropDownList はユーザーが選択を変更したとき、FileUpload はユーザーがファイルを選択したとき change イベントが発生し、CustomValidator の ControlToValidate プロパティが設定されていれば検証がかかります。

    CheckBox (input type="checkbox"), RadioButton (input type="radio") も change イベントは発生しますが、上に述べたように CustomValidator の ControlToValidate プロパティを設定できないので、change イベントでは CustomValidator による検証はかかりません(submit で検証されます)。
  5. クライアント側での検証用 JavaScript のメソッドは CustomValidator の ClientValidationFunction プロパティに設定します。メソッド名が例えば Validate(sender, args) とすると、sender には CustomValidator が html に変換された span 要素が渡されます。args には IsValid, Value プロパティを持つ JavaScript オブジェクトが渡されます。

    CustomValidator の ControlToValidate プロパティが検証対象コントロールに対して設定してある場合は、args.Value には検証対象の入力コントロールが html に変換された input 要素の value 属性の値が渡されます。ControlToValidate プロパティが設定されてない場合は args.Value は空 "" になります。

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="WebForm6.aspx.cs" 
    Inherits="WebApplication1.WebForm6" %>

<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" 
    runat="server">

    <script type="text/javascript">
        //<![CDATA[

        // ControlToValidate が設定されてない場合、引数 args には
        // テキストボックスの値が渡されないので注意。args に頼らず
        // 以下のようにしておくのがよさそう
        function TextBoxValidate(sender, args) {
            var tb =
                document.getElementById('<%= TextBox1.ClientID%>');

            var membership = tb.value.toLowerCase();
            if (membership === "gold" || membership === "silver") {
                args.IsValid = true;
            } else {
                args.IsValid = false;
            }
        }

        function CheckBoxValidate(sender, args) {
            var cb =
                document.getElementById('<%= CheckBox1.ClientID%>');

            if (cb.checked == true) {
                args.IsValid = false;
            } else {
                args.IsValid = true;
            }
        }

        function RadioButtonValidate(sender, args) {
            var rb =
                document.getElementById('<%= RadioButton1.ClientID%>');

            if (rb.checked == true) {
                args.IsValid = false;
            } else {
                args.IsValid = true;
            }
        }

        // ControlToValidate が設定されてない場合、引数 args には
        // テキストボックスの値が渡されないので注意。args に頼らず
        // 以下のようにしておくのがよさそう
        function DropDwonListValidate(sender, args) {
            var ddl =
                document.getElementById('<%= DropDownList1.ClientID%>');

           if (ddl.value == "2") {
                args.IsValid = false;
            } else {
                args.IsValid = true;
            }
        }

        function FileUploadValidate(sender, args) {
            if (window.File && window.FileList) {
                var fileUpload =
                    document.getElementById("<%=FileUpload1.ClientID%>");

                if (fileUpload.files[0] == null) {
                    args.IsValid = false;
                    return;
                }

                if (fileUpload.files[0].type != "image/jpeg") {
                    args.IsValid = false;
                    return;
                }


            } else {
                args.IsValid = true;
            }
            
        }

        //]]>
    </script>

</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" 
    runat="server">

    <h1>CustomValidator</h1>
    <p>CustomValidator のクライアント側での検証のタイミング</p>

    <table>
        <tr>
            <td>
                TextBox
            </td>
            <td>
                <asp:TextBox ID="TextBox1" runat="server">
                </asp:TextBox>
            </td>
            <td>
                <asp:CustomValidator ID="CustomValidator1" 
                    runat="server" 
                    ForeColor="Red"
                    Display="Dynamic"
                    ErrorMessage="Gold または Silver でない" 
                    ClientValidationFunction="TextBoxValidate" 
                    ControlToValidate="TextBox1">
                </asp:CustomValidator>
            </td>
        </tr>
        <tr>
            <td>
                CheckBox
            </td>
            <td>
                <asp:CheckBox ID="CheckBox1" runat="server" />
            </td>
            <td>
                <%--ControlToValidate="CheckBox1" を設定すると
                    HttpException がスローされる。理由は CheckBox
                    には ValidationProperty 属性が付与されてない
                    から。なので submit しないと検証はかからない
                    --%>
                <asp:CustomValidator ID="CustomValidator2" 
                    runat="server"
                    ForeColor="Red"
                    Display="Dynamic"
                    ErrorMessage="チェック不可" 
                    ClientValidationFunction="CheckBoxValidate">
                </asp:CustomValidator>
            </td>
        </tr>
        <tr>
            <td>
                RadioButton
            </td>
            <td>
                <asp:RadioButton ID="RadioButton1" runat="server" />
            </td>
            <td>
                <%--ControlToValidate="RadioButton1" を設定すると
                    HttpException がスローされる。理由は RadioButton
                    には ValidationProperty 属性が付与されてないから。
                    なので submit しないと検証はかからない
                    --%>
                <asp:CustomValidator ID="CustomValidator3" 
                    runat="server"
                    ForeColor="Red"
                    Display="Dynamic"
                    ErrorMessage="選択不可" 
                    ClientValidationFunction="RadioButtonValidate">
                </asp:CustomValidator>
            </td>
        </tr>
        <tr>
            <td>
                DropDownList
            </td>
            <td>
                <asp:DropDownList ID="DropDownList1" runat="server">
                    <asp:ListItem>0</asp:ListItem>
                    <asp:ListItem>1</asp:ListItem>
                    <asp:ListItem>2</asp:ListItem>
                </asp:DropDownList>
            </td>
            <td>
                <asp:CustomValidator ID="CustomValidator4" 
                    runat="server"
                    ForeColor="Red"
                    Display="Dynamic"
                    ErrorMessage="2 は選択不可" 
                    ClientValidationFunction="DropDwonListValidate" 
                    ControlToValidate="DropDownList1">
                </asp:CustomValidator>
            </td>
        </tr>
        <tr>
            <td>
                FileUpload
            </td>
            <td>
                <asp:FileUpload ID="FileUpload1" runat="server" />
            </td>
            <td>
                <asp:CustomValidator ID="CustomValidator5" 
                    runat="server"
                    ForeColor="Red"
                    Display="Dynamic"
                    ErrorMessage="jpg ファイル以外不可" 
                    ClientValidationFunction="FileUploadValidate" 
                    ControlToValidate="FileUpload1">
                </asp:CustomValidator>
            </td>
        </tr>
    </table>
    <asp:Button ID="Button1" runat="server" Text="Submit" />

</asp:Content>

上のコードはマスターページを利用しています。先の記事「ASP.NET 4.5 ScriptManager」で書きましたように、ASP.NET 4.5 以降でクライアントスクリプトを利用するサーバーコントロールが正しく機能するには、必要なクライアントスクリプトの ScriptManager への登録と全ページでの ScriptManager の配置が必要です。マスターページを使ってそのあたりを解決しています。

Tags: , , ,

Validation

複数の CheckBox の状態を取得

by WebSurfer 2. July 2017 17:02

ASP.NET MVC アプリで、複数(数は不定)の CheckBox をレンダリングし、ユーザーがチェックを入れて POST したとき、どのチェックボックスがチェックされているかの状態を取得する方法を備忘録として書いておきます。

CheckBoxList

ユーザーがチェックを入れた CkeckBox からは true を、チェックしてない CheckBox からは false を取得できるようにするのが条件です。

ASP.NET Web Forms アプリですと CheckBoxList というサーバーコントロールがあって、それを使えば複数の CheckBox を実装するのは容易ですが、サーバーコントロールのない ASP.NET MVC アプリでは少々工夫が必要なようです。

html の input type="checkbox" ではチェックされた項目の name=value しか送ってこないのでかえって使いにくいです。Html.EditorFor を使うのがお勧めです。

List<bool> 型のオブジェクトをメンバーとして持つ Model を View に型付け、それから Html.EditorFor を使って CheckBox をレンダリングできます。

その CheckBox は html では input type="checkbox" だけでなく、それとペアで同じ name 属性を持つ隠しフィールド input type="hidden" value="false" も生成されます。(input type="hidden" を使うテクニックは MDN のドキュメント <input type="checkbox"> のメモ欄にも書かれています)

ユーザーがチェックを入れて POST すると、例えば name 属性が Item[0] だった場合、Items[0]=true&Items[0]=false というデータがフォームに含まれて送信されます。(チェックを入れない場合は隠しフィールドの Items[0]=false のみ)

ASP.NET は、モデルバインディングの際、それを見て List<bool> 型のオブジェクトの各要素を true / false に設定しているようです。

以下に、上の画像を表示したサンプルのコードとその説明を書いておきます。

Model と Controller

以下のサンプルではチェックされた項目は ture が、チェックされて無い項目は false がアクションメソッドの引数にバインドされるようにしています。(注:Model と Controller が一緒になっているのは単に分けるのが面倒だったからです)

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

namespace Mvc4App.Controllers
{
  // Model
  public class CheckboxListModel
  {
    public CheckboxListModel()
    {
      Items = new List<bool>();
    }

    public IList<bool> Items { get; set; }
  }
    
  // Controller
  public class ComplexController : Controller
  {        
    [HttpGet]
    public ActionResult Checkboxes()
    {
      // この例では CheckBox を 5 つ作成。初期値は false とし
      // CheckBox はチェックされてない状態とする。
      bool[] defaultItems = 
          new bool[] { false, false, false, false, false };
      CheckboxListModel model = 
          new CheckboxListModel() { Items = defaultItems };
      return View(model);
    }

    [HttpPost]
    // POST されてきたデータは引数の m, items のいずれにもモデル
    // バインドされる。items の方は Model のプロパティ名と一致さ
    // せる必要がある。モデルバインディングの際は大文字・小文字
    // が区別されないので引数名には小文字を使ってプロパティ 
    // Items に以下のように代入できる。
    public ActionResult Checkboxes(CheckboxListModel m, 
                                        IList<bool> items)
    {
      CheckboxListModel model = 
          new CheckboxListModel() { Items = items };
      return View(model);
    }
  }
}

View

EditorFor の引数で m => m.Items[i] としているところがキモです。そうするとレンダリングされる html 要素の name 属性が連番のインデックスを含むようになり、モデルバインディングがうまくいきます。

@model Mvc4App.Controllers.CheckboxListModel

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

<h2>Checkboxes</h2>

@using (Html.BeginForm()) {
    for (int i = 0; i < Model.Items.Count; i++)
    {
        @Html.EditorFor(m => m.Items[i])
        <br />
    }
    <p>
        <input type="submit" value="Send" />
    </p> 
}

Tags:

MVC

About this blog

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

Calendar

<<  December 2024  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar