by WebSurfer
20. October 2016 23:18
ASP.NET Web Forms アプリで、ユーザー入力の検証に CustomValidator と jQuery.ajax を用い、クライアント側での検証を行うというコードを書いたときに、無知な自分がハマったことを忘れないように備忘録として書いておきます。
シナリオは、ユーザー入力が有効かどうかサーバー側でなければ確認できない場合、クライアント側から 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 以降は自分的に備忘録として残しておいた方がよさそうだと思って書きました。下のサンプルコードを参照しながら読んでください。
-
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 にコールバックを書けと言うことですのでそうしてあります。
-
jQuery.ajax によるサーバーへの要求が 2 回出てしまうケースがある。
TextBox にアルファベット 5 文字を入力してから(正規表現による検証はパスさせてから)Button をクリックすると 2 回要求が出てしまいます(1 回無駄)。理由は、Button クリックで TextBox からフォーカスが外れて検証がかかり、Button クリックでもう一度検証かかかるからです。
もともとそのようなケースでは検証が 2 回かかってしまうのが仕様(?)のようですが、今回のようにサーバーのデータベースへの問合せが行われており、それが 1 回余分になるのは何とかした方がよさそうな気がします。回避方法は調査中です。(見つからないかも)
-
ユーザーが未入力の場合の検証は RequiredFieldValidator を利用しています。
先の記事「CustomValidator と RequiredFieldValidator」で書きましたが、CustomValidator で未入力の検証も可能です。しかし、検証コントロールのエラーメッセージは固定的なので、CustomValidator 一つで済ませる場合、エラーメッセージは "未入力もしくはデータベースに存在しません" というようにせざるを得ません。
それより、RequiredFieldValidator と CustomValidator を併用して、未入力の時は "未入力です" というエラーメッセージを、入力はあるが無効な場合は "データベースに存在しません" とした方がユーザーフレンドリーだと思います。
-
上で「検証コントロールのエラーメッセージは固定的」と書きましたが、サーバー側での検証メソッドでは検証内容に応じてエラーメッセージを書き換えることができます。
しかしながら、先の記事「FileUpload と CustomValidator」でもいろいろ考えましたが、クライアント側でのエラーメッセージを書き換える方法が分かりません。それも RequiredFieldValidator と CustomValidator を併用した理由です。
-
RegularExpressionValidator と併用すると表示が 2 重になる。
最初、RequiredFieldValidator で未入力の検証、RegularExpressionValidator でアルファベット 5 文字であることの検証、CustomValidator でデータベースにデータがあるかの検証を行うつもりでした。
しかしユーザー入力があって RequiredFieldValidator での検証をパスした場合、RegularExpressionValidator と CustomValidator の両方の検証がかかり、両方のエラーメッセージ(例えば、「アルファベット 5 文字としてください」と「データベースに存在しません」)が出てしまいます。
それが、以下のサンプルコードで、CustomValidator でアルファベット 5 文字とデータベースの有無の両方の検証をすることにした理由です。
-
上の画像のエラーメッセージ "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>