Cutting Edge

AJAX を使用した HTML フォームの拡張

Dino Esposito

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコード参照

目次

ポップアップ フォーム
JIT (Just-in-Time) 検証
送信回数の削減
フォームの自動保存
AJAX の拡張

AJAX を使用すると、ユーザー エクスペリエンスの向上、インタラクティビティの向上、ページのちらつきの抑制、データ転送の高速化など、さまざまな機能が向上します。特に、AJAX はテーブルに重要な変更をもたらし、テーブルでサーバーへのデータ送信を完全に制御する機能を提供します。

標準の AJAX アプリケーションでは、ブラウザからサーバーにデータを送信するときに開発者のコードが渡されます。データの送信方法と送信するタイミングは開発者が決定します。しかも、フォームを使用するかどうかも開発者が決めることができます。

データ送信プロセスをブラウザ ベースのアプリケーションでこのように高度に制御するには、多くの JavaScript コードを作成する必要があります。このコラムの以前の記事で、きわめて一般的に使われる JavaScript ライブラリであり、本格的な JavaScript コードを作成する際に大いに役立つ jQuery ライブラリの主な機能について説明しました。

今月は、AJAX アプリケーションでフォームを使用する場合のいくつかのケースについて説明します。自動保存、JIT 検証、送信回数の削減などの機能を実装するさまざまな方法を紹介します。まず、ポップアップ フォームについて説明します。

ポップアップ フォーム

先月は、jQuery ライブラリのサブセットであり、モーダルおよびモードレス ウィンドウとアコーディオンやタブなどの先進的な高機能ウィジェットを使用してリッチ ユーザー インターフェイスの作成を簡素化するための専用の jQuery UI ライブラリを紹介しました。

Web の場合、ポップアップ フォームはドリルダウン操作の結果データを表示する手段として必ずしも最適なソリューションとはいえません。通常、ポップアップ ウィンドウはブラウザ API (一般的には window.open メソッド) を使用して作成し、ユーザーがサイトに設定しているポップアップ ブロッカーのポリシーに従います。次のコードは、ブラウザで強制的にポップアップ ウィンドウを表示する方法を示しています。

window.open(url, 'Sample', 'width=400,height=300,scrollbars=yes'); 

このポップアップは URL で指定されたページを表示し、アプリケーションで他のページにデータをポストすることができます。ポップアップにデータを渡す場合にはクエリ文字列を使用します。または、ウィンドウ オブジェクトの opener プロパティを使用して親フォームの入力フィールドを取得する方法もあります。次のように、ポップアップに表示されたページ内からウィンドウ オブジェクトを呼び出すことができます。

var text = window.opener.document.Form1["TextBox1"].value;

opener プロパティから opener フォームのオブジェクト モデルにアクセスし、HTML DOM で個々のフォームのコンテンツを読み取ります。

Web 広告でポップアップおよびポップアンダー フォームが濫用されると、ブラウザ開発者はこのようなウィンドウの多用を制御する機能を取り入れるようになります。その結果、適正なポップアップの使い方でユーザーに情報を伝達しても、ユーザーの関心を得られず、情報を伝えること自体が困難になります。

最近の Web アプリケーションのポップアップ フォームは、下になる要素のクリック機能を無効にした後、多くの場合に画面の一番上にモーダル形式で表示される DIV コンテンツです。ポップアップのユーザー インターフェイスは、ページ全体を使用して表示されるとは限らず、むしろ、多くの場合はページの一部を構成します。そこで、最初は非表示の DIV タグにしておいて、適切なスクリプト コードが実行されたときに表示することも可能です。

このような擬似ポップアップ ウィンドウのコンテンツは既にページの一部になっているため、擬似ポップアップ ウィンドウからページ内の任意の要素に簡単にアクセスできます。そのため、入力要素の初期化も問題になりません。

jQuery UI などの高機能なライブラリを使用して、このようなポップアップの外観を高度に制御し、従来の Windows ダイアログ ボックスと同様の外観にすることもできます (図 1 を参照)。

ポップアップに設定したマークアップには、form タグが含まれる場合と含まれない場合があります。ホスト ページが ASP.NET ページである場合は、サーバー側の form 要素を追加することはできません。runat=server 属性が存在しない HTML フォーム要素は、ページが従来の ASP.NET であっても、ASP.NET MVC であっても挿入できます。ポップアップに form タグが含まれている場合、ブラウザの送信機能を利用して、指定したアクション URL にデータを送信できます。それ以外の場合も送信することは可能ですが、コードを作成する必要があります。

fig01.gif

図 1 jQuery UI 1.7 で作成したサンプル ダイアログ ボックス

入力フィールドをループ処理して、HTTP 要求の本体を準備し、直接 XMLHttpRequest オブジェクトを使用するか、またはその抽象型 (jQuery の $.ajax 関数や Microsoft AJAX クライアント ライブラリの Sys.Net.WebRequest オブジェクトなど) を使用して、要求を送信することができます。

ポップアップのコンテンツを ASP.NET ページの他の部分から物理的に分離しておくには、ASCX ユーザー コントロール内にたとえば次のように定義します。

<div id="DialogBox1">
  <cc1:YourUserControl runat="server"     ID="DialogBox1_Content" />
</div>

この方法は、従来の ASP.NET ページと ASP.NET MVC ページの両方で使用できます。

JIT (Just-in-Time) 検証

多くの場合、Web フォームを送信する前に、誤っている値をユーザーに通知する必要があります。ASP.NET 検証コントロールには、ユーザーに不適切な入力を通知する優れた機能があります。AJAX Control Toolkit の ValidatorCalloutExtender による無効な入力の警告は茶目っ気があります (図 2 を参照)。

fig02.gif

図 2 UI に表示された検証コントロールのエクステンダ

ASP.NET 検証コントロールは、送信時にフォームのコンテンツ全体の妥当性を確認します。ASP.NET の現在のバージョンには、クライアント側のコードで (カスタム) サーバー規則に従ってフィールドの妥当性を確認する機能を持つ非同期のカスタム検証コントロールがありません。ASP.NET の今後のバージョンで、このようなコンポーネントがリリースされる可能性があります。

簡単に言うと、誤った値を検出してユーザーに通知する方法は 2 つあります。1 つは、非同期検証コントロールを入力フィールドに関連付けて、フィールドの値が変更されたときにエラー メッセージを通知する方法です。もう 1 つは、フォームのコンテンツ全体の妥当性を確認する方法です。2 つ目の方法を実装する手段はいくつかあります。フォームを通常の方法で単純にブラウザで送信し、サーバーの応答を待機する方法や、フォームのコンテンツまたはコンテンツの一部を非同期にプログラムで送信して早期にフィードバックを得る方法があります。では、さまざまな方法を確認しながら AJAX 機能の利用方法を見ていきましょう。

ASP.NET では、フィールドのコンテンツに固定的な規則を適用して入力が有効かどうかを確認する複数の定義済み検証コントロールが用意されています。サンプル検証コントロールは、フィールドが空のままであるかどうか、データが所定の範囲内に収まっているかどうか、一部のコンテンツが所定の値であるかどうか、所定の正規表現に一致しているかどうかなどを確認します。これらの検証はすべて、一般的な妥当性確認の要件に対応しています。ただし、特に一般的な検証はサーバーで実行する必要があり、データベース接続、ワークフロー、または場合によっては Web サービスが必要です。これらのすべての場合に、ASP.NET の CustomValidator コントロールを利用できます。

CustomValidator は、値を確認し、フィードバックを UI レベルに通知する場合に、クライアントの JavaScript 関数とサーバー メソッドのどちらでも指定できる優れたコントロールです。CustomValidator コントロールを AJAX の更新パネルに適切にラップすることによって、応答を非同期に返してページ全体の更新を避けることもできます。CustomValidator の唯一の短所は、他の検証コントロールと同様に、フォームが送信されるまで値が確認されないという点です。図 3 のコードの場合を考えてみましょう。

図 3 更新パネルの実装

<asp:UpdatePanel runat="server" ID="UpdatePanel1">
  <ContentTemplate>
    <asp:TextBox ID="TextBox1" runat="server" 
                 AutoPostBack="true" />
    <asp:CustomValidator ID="CustomValidator1" runat="server" 
                 Display="Dynamic" 
                 ControlToValidate="TextBox1" 
                 SetFocusOnError="true" 
                 ErrorMessage="Invalid ID"
                 OnServerValidate=" CustomValidator1_Validate" >
   </asp:CustomValidator> 
 </ContentTemplate>
</asp:UpdatePanel>
<asp:Button ID="Button1" runat="server" Text="Submit" />

ユーザーがテキスト ボックスのコンテンツを編集すると、ページがポストバックされます。CustomValidator インスタンスをテキスト ボックスに関連付けて、コンテンツの検証方法を通知します。更新パネルは、ポストバックで部分的な更新のみが生成されるようにします。ただし、このコードでは期待どおりの結果は得られません。テキスト ボックスを編集するとページがポストバックされますが、検証は行われません。カスタム検証コントロールをトリガするには、ユーザーが送信ボタンを押す必要があります。

問題は、従来のフォーム送信のプロセスの外部には、Page クラスの Validate メソッドを呼び出すコード パスが存在しないということです。この問題を解決するには、次の方法で、非同期ポストバックが発生するかどうかを確認し、Validate を呼び出す必要があります。

protected void Page_Load(object sender,     EventArgs e)
{
    if (ScriptManager1.IsInAsyncPostBack)
    {
        this.Validate();
    }
}

Validate メソッドで、ページ内の各検証コントロールがループ処理され、ページ内の誤った値が通知されます。

入力エラーをできるだけ早期に通知することを目的とする場合、前述した方法以外の唯一の方法は、重要な入力フィールドに JavaScript コードを追加することです。以下に例を示します。

 <asp:TextBox ID="TextBox1" runat="server" 
              onchange="__doValidation(this)" />

onchange イベントは、ユーザーがテキスト ボックスに入力している間は発生せず、テキスト ボックスからフォーカスが移動したときと、ユーザーが明示的に変更をコミットした場合のみに発生します。前述のコードでは、JavaScript ハンドラが実行されて、XMLHttpRequest によるリモート検証が行われます。この JavaScript ハンドラのコードを次に示します。

<script type="text/javascript">
    function __doValidation(textBox) {
        var text = textBox.value;
        PageMethods.PerformValidation(text, onComplete);
    }

    function onComplete(results) {
        if (results == false)
            $get("Label1").innerHTML = "Invalid ID";
        else
            $get("Label1").innerHTML = "";
    } </script>

ご覧のように、この例では、ASP.NET ページのメソッドを使用して HTTP エンドポイントの呼び出しを定義しています。WCF サービス メソッド、Web サービス メソッド、REST サービスなどの場合は、このようなエンドポイントで問題ありません。HTTP エンドポイントから取得する応答は、ブール値または HTML マークアップです。応答がブール値の場合、ユーザー インターフェイスを更新するロジックはクライアントに存在するため単純な場合が多く、応答が HTML マークアップの場合、サーバー側のより複雑なロジックが必要です。

入力フィールドの妥当性を個別に確認する必要がなく、フォーム全体を送信するまでフィードバックを待機することが可能な場合は、ASP.NET の UpdatePanel コントロール内で検証コントロールを使用する方法が最も簡単です。この場合、検証コントロールと入力コントロールを同じパネルに配置する必要があります。

部分レンダリングでフォームのコンテンツをポストし、妥当性を確認する方法の代わりに、jQuery などの AJAX ライブラリをフォームや検証のプラグインと組み合わせて使用する方法もあります。具体的な例については、「JQuery のフォーム検証および AJAX の送信と ASP.NET を組み合わせる」の記事を参照してください。

一般的には、妥当性確認は個別に行うことをお勧めします。個別に確認することで、無効な値を即時にキャッチし、フォームを動的に調整して現在の入力を反映できます。当面は、JavaScript を使用するか、自動ポストバック コントロールを使用した部分レンダリングを行う必要があります。

フォーム全体の妥当性確認を非同期に行う場合、部分レンダリングで従来の ASP.NET の検証コントロールを使用する方法が最も単純です。または、フォームのコンテンツを文字列にシリアル化し、任意の AJAX ライブラリを使用して HTTP エンドポイントにその文字列を送信する方法も有効です。この後すぐに説明しますが、カスタム シリアル化とフォームの送信によって、さらに高度な機能を使用できるようになります。

送信回数の削減

AJAX では、フォームのコンテンツを Web サーバーに送信する方法は 1 つだけではなくなりました。送信ボタンを使用して明示的に送信する方法もあれば、HTML DOM Form オブジェクトの submit メソッドを使用してプログラムで送信する方法もあります。さらに重要なことは、コンテンツをサーバーに送信する場合に、一度に送信するのではなく、分割して数回に分けて送信することも可能だということです。パターンの場合、フォームのすべてのコンテンツを一度に送信するときは、Explicit Submission パターンを適用します。それ以外の場合、Submission Throttling (ST) パターンを使用して送信回数を削減します。

ST パターンは、頻繁にラウンドトリップが生じる操作など、サーバーに負荷がかかるクライアント操作の影響を緩和するために役立ちます。そのため、ST パターンはフォームの自動保存などの優れた機能を実装する場合の主要な方式になります。

簡単に言うと、ST パターンは、送信するコンテンツをブラウザ側のバッファにキャッシュし、数回に分けて送信します。標準的な例として、オート コンプリート エンジンが挙げられます。ユーザーがテキスト ボックスに入力すると、提案の要求がサーバーに送られます。この場合、ピーク時に要求が Web サーバーをヒットする回数が膨大になることは容易に察しがつきます。

そこで、アプリケーションの応答性と Web サーバーのワークロードの合理的なバランスをとるために、送信プロセスを制御する機能があると便利です。その制御方法 (スロットル) を理解するため、AJAX Control Toolkit にある Microsoft の AutoComplete エクステンダのソース コードを確認してみましょう。

オート コンプリート エクステンダは、ユーザーが入力ボックスに入力すると、提案の要求を送信します。これに基づいて、指定した Web サービスを呼び出す keypress イベントのハンドラを処理すると考えがちですが、図 4 に示すように、ソース コードをよく見ると、別の処理が行われています。

図 4 タイマによるポストバックの制御

// Handler for the textbox keydown event
onKeyDown: function(ev) 
{
    // A key has been pressed, so let's reset the timer
    this._timer.set_enabled(false);

    // Is it a special key?
    if (k === Sys.UI.Key.esc) {
       ...
    }
    else if (k === Sys.UI.Key.up) {
       ...
    }
       ...
    else {
       // It's a text key, let's start the timer again 
       this._timer.set_enabled(true);
    }     
}

エクステンダは、既定で間隔が 1 秒に設定されている内部タイマを使用します。テキスト ボックスが入力フォーカスを取得すると、タイマが起動します。キーを押し下げると、タイマは停止し、キーの分析が開始します。押されたキーが EnterESCTab などの特殊キーまたは方向キーである場合、エクステンダは固有のコードを実行します。押されたキーがテキスト ボックスのバッファに入力されたコンテンツを示す場合、タイマが起動します。

タイマは、キーが押されるたびに停止してから再起動します。1 秒間 (または設定した任意の間隔の間) キーを押さずにいると、タイマの間隔で入力フィールドのコンテンツが送信され、提案を受け取ります。この場合、ユーザーがキーを押すまでポストバックが発生しないため、ユーザーにとってはあまり違いはありませんが、サーバーにとっては大きな意味があります。ユーザーが 1 秒間操作を中断した場合、タイマは期限切れになり、ポストバックが行われます。

タイマは、非同期の動作を利用してマルチスレッドをシミュレーションする固有の機能を持つ、JavaScript の高機能ツールです。JavaScript でタイマを作成することによって、関連するコードを、今すぐではなく、しかもできるだけ早く実行することをインタープリタに通知します。コードが単純で、タイマの間隔が適切に設定されていれば、インタープリタは実行を適切にスケジュールして、複数のタスクを同時に進行できます。

フォームの自動保存

AJAX アプリケーションでは、ユーザーが Web サーバーにデータを送信しないまま、1 つのフォームの操作に長時間を費やすことが珍しくありません。Web サーバーには、クライアントで行われている操作を認識する手段がありません。したがって、しばらく要求を受け取らない状態が続くと、サーバー セッションがタイムアウトする可能性があります。その結果、ユーザーは、最終的に完了したフォームをサーバーにポストバックしたときに心外な通知 (タイムアウト メッセージ) を受けることになります。

これを避けるには、クライアント アプリケーションから XMLHttpRequest を使用して、適切な頻度でハートビート メッセージを送信します。ハートビート メッセージには、個別の状況に適した任意のフォームおよびコンテンツを含めることができます。内容は、たとえば、クライアントがまだ動作していることを示す空の要求でもかまいません。サーバー セッションを起動して実行状態を維持することだけが目的です (この場合、間隔の長さはアプリケーションに設定したセッション タイムアウトより短い任意の値に指定する必要があります)。

ハートビートには、現在のフォームのコンテンツ (またはコンテンツの一部) を含めることもできます。このようなコンテンツは定期的にアップロードされ、サーバーにキャッシュされて、適切な時間だけその処理を待機します。総体的には、開いているドキュメントを定期的にディスクに保存してデータの消失を防ぐ Microsoft Office Word の自動保存機能と大差ありません。

Web 上のフォームの自動保存には規定された動作がはるかに少なく、内部実装の大部分は開発者の創造性と要件しだいで決まります。フォームの自動保存には、コンテンツを定期的に URL に送信する機能があります。受信側 URL は、データを処理し、通常はそのデータをキャッシュして、現在のユーザーが使用できるようにします。図 5 に例を示します。

図 5 フォームの自動保存の実装

<script type="text/javascript">
    var _myTimerID = null;
    var _userID = null;
    function enableAutoSave(id) {
        _userID = id;
        _myTimerID = window.setInterval(__doAutoSave, 10000);
    }

    function __doAutoSave() {
        var body = "ID='" + _userID + "'&" +
                   "TextBox2='" + $get("TextBox2").value + "'&" +
                   "TextBox3='" + $get("TextBox3").value + "'&" +
                   "TextBox4='" + $get("TextBox4").value + "'";

        PageMethods.AutoSave(_userID, body, onComplete);
    }

    function onComplete(results) {
        $get("Msg").innerHTML = "<i>Form auto-saved for user '" + 
              _userID + "' at " + results + ".</i>";
    }
</script>

<body>
    ...
</body>

// In the code-behind class ...
protected void Button1_Click(object sender, EventArgs e)
{
    string user = TextBox1.Text;

    // Populate input fields if any previously entered 
    // data is available
    string body = AutoSaveAPI.Read(user);
    if (!String.IsNullOrEmpty(body))
    {
        Dictionary<string, object> data = body.ParseToDictionary();
        TextBox2.Text = data["TextBox2"] as string;
        TextBox3.Text = data["TextBox3"] as string;
        TextBox4.Text = data["TextBox4"] as string;
    }

    // Re-enable auto-save for this editing session
    string script = "enableAutoSave('" + user + "');";
    ScriptManager.RegisterStartupScript(this, this.GetType(), 
                  "myTimer", script, true);
}

[WebMethod]
public static string AutoSave(string id, string body)
{
    AutoSaveAPI.Save(id, body);
    return DateTime.Now.ToString();
}

ユーザーがフォームに移動すると、そのフォームに以前に入力されたデータが Codebehind クラスで読み込まれます。一般に、このデータはキャッシュまたは選択したその他のソースから読み取られます。図 5 では、ストレージは AutoSaveAPI クラスのインターフェイスに隠蔽されています。読み取られた情報はフォームの入力フィールドに設定されます。

新しいデータ エントリ セッションで自動保存を有効にするには、タイマをトリガする 1 行のスクリプトを出力します。タイマの間隔ごとに、コールバックによってさまざまなフィールドの現在の値が収集され、文字列が作成されます。この文字列は XMLHttpRequest でアップロードされます。この文字列を Web サービス、REST 方式による標準の URL、またはページ メソッドにアップロードできます。フォームの自動保存は特定のページに固有のコンポーネントであるため、ページ メソッドを使用することをお勧めします。ページ メソッドには、ページで制限されているため、効率的でコードの作成が容易だという利点があります。図 6 に、フォームの自動保存の動作を示します。

fig06.gif

図 6 フォームの自動保存の動作

AJAX の拡張

AJAX では、古い形式の HTML フォーム タグにいくつかの改変が必要です。今後登場する HTML 5.0 標準では、URL、日付、電子メールなどの新しい入力項目がブラウザでネイティブに処理されます。これによって、スクリプトの作成や検証コントロールが必要になる場面が少なくなります。このような HTML の進化により、AJAX アプリケーションで非同期に行う必要があるサーバー側の妥当性確認に専念することができるようになります。

Dino に対するご意見やご質問は cutting@microsoft.com までお送りください。

Dino Esposito は、IDesign 社のアーキテクトであり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。