WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

jQuery.ajax を使っての事前検証

by WebSurfer 6. February 2022 15:56

ASP.NET Web Forms アプリで jQuery Ajax を使ってポストバックの際にサーバー側で行われる処理が可能かどうかを事前に調べ、可能と分かったら JavaScript で Button クリックしてポストバックする、可能ではないと分かった場合はポストバックせずメッセージを表示してユーザーに知らせるという話です。

ポストバックして処理完了

元の話は Teratail のスレッド「asp:button 押下前にサーバサイドが応答しているか確認したいためPingのようなことを行いたい」のものです。実際のそのような検証が必要なケースはあまりなさそうですが、せっかく考えた話ですし jQuery Ajax の timeout の使い方が今後の参考になるかもしれないと思ったので備忘録として書いておくことにしました。

Button クリックでポストバックしてサーバー側でユーザーが送信したデータを受けて、データベースサーバーにクエリを投げる等の処理をすることはよくあると思います。

その際、Button クリックでいきなりポストバックするのではなく、その前に jQuery Ajax を使って事前にサーバー側での処理が可能かどうかを調べるというストーリーです。

(いきなりポストバックしても結果をきちんとユーザーに知らせることはできるし、やりすぎるとサーバーに無駄な負荷を増やすし軽くすると検証の意味が薄れるということで、そのような検証の必要性は低いとは思いますがそこは置いときます)

jQuery Ajax で呼び出してサーバー側で事前検証を行う HTTP ジェネリックハンドラを作ります。以下のコードがそのサンプルです。

HTTP ジェネリックハンドラ Handler1.ashx

using System.Web;

namespace WebForms1
{
    public class Handler1 : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            // Button クリックでポストバックされた時にサーバー
            // で行われる処理が可能か事前に検証する。処理に最
            // 長 3 秒かかるとして、ここでは単純に 3 待つ
            System.Threading.Thread.Sleep(3000);
            
            // 検証に成功したら "検証成功" という文字列を返す
            var validationResult = "検証成功";

            context.Response.ContentType = "text/plain";
            context.Response.Write(validationResult);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上の Handler1.ashx を jQuery Ajax を使って呼び出します。Handler1.ashx は検証に成功すると "検証成功" という文字列を返すので、それを受けたら JavaScript で Button をクリックしてポストバックします。その結果がこの記事の一番上にある画像です。

Handler1.ashx を呼び出す JavaScript を含む Web Form ページのサンプルコードは以下の通りです。

WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs"
    Inherits="WebForms1.WebForm1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="Scripts/jquery-3.4.1.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#btnSec").on("click", function () {
                $.ajax({
                    type: "get",
                    url: "Handler1.ashx",
                    timeout: 5000
                }).done(function (data) {
                    // 呼び出し先 Handler1.ashx で検証が成功すると "検証成功" 
                    // という文字列が返ってきて引数 data に代入される
                    if (data == "検証成功") {
                        var hiddenbutton = document.getElementById("Button1");
                        // Button をクリック
                        hiddenbutton.click();
                    } else {
                        $("#Label1").text("処理できません - 検証結果 NG");
                    }
                }).fail(function (jqXHR, textStatus, errorThrown) {
                    $("#Label1").text("処理できません - " + textStatus);
                });
            });
        });
    </script>
    <style type="text/css">
        .style1
        {
            display: none;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <h2>WebForm1 - jQuery.Ajax を使っての事前検証</h2>
            
            <input id="btnSec" type="button" value="反映" />

            <%--ポストバックを行う Button コントロール。上の style1 で
                隠しボタンに設定。クリックは JavaScript で行う。クリ
                ックされるとポストバックが発生し、サーバー側のイベント
                ハンドラ ClickBtnSection で必要な処理が行われる--%>
            <asp:Button ID="Button1" runat="server" 
                OnClick="ClickBtnSection" CssClass="style1" />

            <asp:Label ID="Label1" runat="server">
                Label1 初期値
            </asp:Label>
        </div>
    </form>
</body>
</html>

WebForm1.aspx.cs

using System;

namespace WebForms1
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void ClickBtnSection(object sender, EventArgs e)
        {
            // サーバー側での必要な処理

            Label1.Text = "Button1 がクリックされました";
        }
    }
}

Handler1.ashx を呼び出して、そこでの処理中にサーバーエラーが発生した場合は以下のようになります。

サーバーエラー

Handler1.ashx を呼び出した後、jQuery Ajax の timeout オプションに設定した時間(上のコード例では 5 秒)待っても応答が返ってこない場合は以下のようになります。

timeout

Handler1.ashx から帰ってきた文字列が "検証成功" 以外の場合は(実際にそういうケースはないかもしれませんが)以下のようになります。

検証結果 NG

Tags: , , ,

Validation

非同期 HTTP ハンドラ (2)

by WebSurfer 8. March 2018 18:56

先の記事「非同期 HTTP ハンドラ」で、Web サービスに非同期でアクセスする HTTP ジェネリックハンドラのサンプルを書きました。

その HTTP ハンドラは IHttpAsyncHandler インターフェイスを継承して BeginProcessRequest, EndProcessRequest メソッドを実装するという旧来の方法を使っており、記事に書いてありますように非常に複雑なコードになります。

ここまで複雑なことをしなければならないのなら、HTTP 503 エラー (Server Too Busy) が頻発して困っているというような事情がなければ、従来通り同期版のままでもいいかなって気もします。(笑)

ですが、.NET 4.5 から簡単に非同期呼び出しが実装できるようになったそうです。どのぐらい簡単にできるのか���同じ機能を async / await を利用して実装してみました。

非同期 HTTP ハンドラ

確かにはるかに簡単でした。前のように、何がどうなっているのかを調べて、悩んで、時間をかけて実装するという手間は大幅に減っています。

上の画像は、この記事に書いた方法で作成した HTTP ハンドラを、ブラウザから直接呼び出した結果です。具体的にどのように実装したかは以下に書きます。

HelloWorldWcfService.cs

先の記事の Web サービスは WCF サービスに変更しました。HelloWorld メソッドの実装は先の記事の Web サービスのものと全く同じです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class HelloWorldWcfService : IHelloWorldWcfService
{
    public string HelloWorld(int callDuration)
    {
        Thread.Sleep(callDuration);
        return String.Format(
          "Hello World from WcfService. Call Time: {0}",
          callDuration);
    }
}

WCF サービスに変更したのは、Web サービスが Legacy Technology だからということもありますが、一番の理由はサービス参照の追加を行うと自動生成されるサービスプロキシに含まれる非同期版メソッドが利用できるからです。

(非同期版メソッドについて、詳しくは先に記事「WCF サービスの非同期呼び出し」を見てください)

HelloWorldAsyncHnadler.ashx

Visual Studio で、既存の ASP.NET Web Forms アプリにジェネリックハンドラーを追加します。.ashx ファイルには何も手を加える必要はありません。

<%@ WebHandler Language="C#"
    CodeBehind="HelloWorldAsyncHandler.ashx.cs"
    Class="WebFormsApp.HelloWorldAsyncHandler" %>

Web アプリケーションプロジェクトの場合、.ashx ファイルとそのコードビハインドの .ashx.cs ファイルは別になります。Web サイトプロジェクトの場合、すべて .ashx ファイルに含まれるという違いがあります。

HelloWorldAsyncHandler.ahsx.cs

自動生成されたコードは同期版ハンドラのベースとなるものです。これを HttpTaskAsyncHandler クラスを継承したクラスに書き換えます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using WebFormsApp.HelloWorldServiceReference;

namespace WebFormsApp
{
    public class HelloWorldAsyncHandler : HttpTaskAsyncHandler
    {
        HelloWorldWcfServiceClient client;

        // これが呼び出されることはない。万一呼び出されたら例外
        // をスローして自爆する
        public override void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException();
        }
        
        public override async Task ProcessRequestAsync(
                                            HttpContext context)
        {
            context.Response.Write(
                "<p>Before await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            client = new HelloWorldWcfServiceClient();

            string result = await client.HelloWorldAsync(3000);

            context.Response.Write(
                "<p>After await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            context.Response.Write("<p>" + result + "</p>");
        }

        public override bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上のコードの WebFormsApp.HelloWorldServiceReference はサービスプロキシの名前空間です。HelloWorldWcfServiceClient コンストラクタ、HelloWorldAsync メソッドの定義は自動生成される Reference.cs ファイルにあります。

結局は先の記事「非同期 HTTP ハンドラ」と同様なことを行っていて、先の記事のコードの BeginProcessRequest メソッドが await キーワードの前に、EndProcessRequest メソッドが await の後に実行されているようです。

BeginProcessRequest, EndProcessRequest メソッドをプログラマがコードを書いて実装しなくて済むよう、コンパイラが肩代わりしているという感じです。

Tags: , ,

ASP.NET

HTTP ハンドラでキャッシュコントロール

by WebSurfer 19. May 2015 17:55

このブログでは、アプリケーションのフォルダに保存した画像やスクリプトファイルなどを応答として返すのに HTTP ハンドラを使っています。

F5 キーを押すなどしてページをリロードすると、下の画像のように HTTP 304 Not Modified 応答を返しますが、これを HTTP ハンドラで実現するにはどのようにするかという話をメインに、HTTP ハンドラを利用したキャッシュコントロールについて書きます。

HTTP 304 応答

HTTP ハンドラを使わないで、以下のように直接 img 要素の src 属性に画像ファイルの URL を設定した場合は、IIS が ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めて返してくれます。

<img alt="" src="/images/sample2.jpg" />

クライアントが F5 キーなどでページをリロードすると、再度ブラウザが src 属性に設定された URL の画像を要求しますが、そのとき If-Modified-Since と If-None-Match を要求ヘッダに含めて送ってくれます。

要求を受けた IIS は要求ヘッダに含まれる If-Modified-Since と If-None-Match を見て、条件を満たせば(即ち、画像が変更されてなければ) HTTP 304 Not Modified 応答を返します(ヘッダのみでコンテンツは送信しない。ブラウザのキャッシュを使えという指示と同じ)。

ちなみに、ブラウザのキャッシュを削除すると、画像を要求する際 If-Modified-Since と If-None-Match は要求ヘッダには含まれなくなます。なので、キャッシュを削除してから F5 キーを押せば、IIS は画像のコンテンツを含めて HTTP 200 応答を返します。

src 属性に HTTP ハンドラを指定する(HTTP ハンドラ経由でファイルをダウンロードする)場合は、IIS が自動的に行ってくれる上記の処置を、自力で HTTP ハンドラにコーディングすることになります。

簡単に書くと、(1) ブラウザ送られてくる要求ヘッダの中の If-None-Match, If-Modified-Since の値を取得し、その値に応じて HTTP 200 応答(コンテンツを含む)または HTTP 304 応答(コンテンツ無し)のどちらかを返す。(2) 要求を受けたら毎回、応答ヘッダの ETag, Last-Modified に設定する値を当該ファイルから取得し、それらを応答ヘッダに含めてブラウザへ返す・・・という処置をコーディングすることになります。

さらに、せっかく HTTP ハンドラを使って細かくキャッシュコントロールのためのコードが書けるのですから、上記 (1), (2) だけでなく(ETag, Last Modified の応答ヘッダへの設定だけでなく)、Cache-Control, Expires 等の応答ヘッダへの設定も行って、プロキシやブラウザのキャッシュをきちんとコントロールした方がよさそうです。

また、サーバー側でのキャッシュもコントロールすることが可能になりますので、サーバーのキャッシュが利用できればそれを使わない手はないですよね。

ということで、例として、画像ファイル用とスクリプトファイル用それぞれの HTTP ハンドラを、Web サイトプロジェクトの ジェネリックハンドラ(.ashx ファイル)の形で書いたコードを以下に紹介します。ベースは BlogEngine 1.6.1.0 のものです。

(1) 画像用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ImageHandler" %>

using System;
using System.Web;
using System.IO;
using System.Net;

public class _0114_ImageHandler : IHttpHandler {
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;
        
    // ファイル名はクエリ文字列で指定。
    string fileName = request.QueryString["picture"];
        
    // 画像ファイルはアプリケーションルート直下の
    // images という名前のフォルダにあることが前提。
    string folder = "~/images/";

    if (!String.IsNullOrEmpty(fileName))
    {            
      string path = 
          context.Server.MapPath(folder + fileName);
      FileInfo fileInfo = new FileInfo(path);
      if (fileInfo.Exists)
      {            
        // BlogEngine では何故か作成日時 (CreationTimeUtc) が
        // 使われていたが、IIS が設定するのと同様に、更新日時
        // (LastWriteTimeUtc) を使用するように変更。
        DateTime lastWriteTime = fileInfo.LastWriteTimeUtc;

        // ETag は更新日時のタイマ刻み数。(IIS とは異なる)
        string etag = "\"" + lastWriteTime.Ticks + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];

        DateTime ifModifiedSince = DateTime.MinValue;
        DateTime.TryParse(
            request.Headers["If-Modified-Since"], 
            out ifModifiedSince);

        response.Clear();
        // ブラウザとプロキシにキャッシュを許可
        response.Cache.SetCacheability(HttpCacheability.Public);
        // 有効期限を、要求を受けた日時から 1 年に設定。
        response.Cache.SetExpires(DateTime.Now.AddYears(1));
        // Last-Modified を応答ヘッダに設定。
        response.Cache.SetLastModified(lastWriteTime);
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);

        // 要求ヘッダの If-None-Match, If-Modified-Since と、
        // 上のコードで取得した etag, lastWriteTime を比較し、
        // どちらかが一致したら HTTP 304 応答を返して終了。
        if (ifNoneMatch == etag || 
            ifModifiedSince == lastWriteTime)
        {
          response.StatusCode = 
              (int)HttpStatusCode.NotModified;
          return;
        }

        int index = fileName.LastIndexOf(".") + 1;
        string extension = 
            fileName.Substring(index).ToLowerInvariant();

        // IE は image/jpg を認識しないことへの対応
        if (extension == "jpg")
        {
          context.Response.ContentType = "image/jpeg";
        }
        else
        {
          context.Response.ContentType = "image/" + extension;
        }
                
        response.TransmitFile(fileInfo.FullName);
      }
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ETag, Last Modified, Expires の違いについては以下の stackoverflow の記事が参考になると思います。

(2) スクリプト用の HTTP ハンドラ

<%@ WebHandler Language="C#" Class="_0114_ScriptHandler" %>

using System;
using System.Web;
using System.IO;
using System.Security;
using System.Web.Caching;
using System.Net;

public class _0114_ScriptHandler : IHttpHandler 
{
    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;

    // ファイル  名はクエリ文字列で指定。
    string fileName = request.QueryString["filename"];

    // スクリプトファイルはアプリケーションルート直下の
    // scripts と言う名前のフォルダにあることが前提。
    string folder = "~/scripts/";

    if (!String.IsNullOrEmpty(fileName))
    {
      string script = null;
            
      // まずサーバーのCacheをチェック。なければファイルに
      // アクセスしてスクリプトを取得。
      if (context.Cache[fileName] == null)
      {
        if (!fileName.EndsWith(".js",
            StringComparison.OrdinalIgnoreCase))
        {
          throw new SecurityException("アクセス不可");
        }

        string path =
            context.Server.MapPath(folder + fileName);
        FileInfo fileInfo = new FileInfo(path);

        if (fileInfo.Exists)
        {
          using (StreamReader reader = 
                              new StreamReader(path))
          {
            script = reader.ReadToEnd();
                        
            // Minify は割愛。
                        
            // サーバーのキャッシュに保持。
            context.Cache.Insert(
                        fileName,
                        script,
                        new CacheDependency(path));
          }
        }
      }
      else
      {
        script = (string)context.Cache[fileName];
      }

      if (!string.IsNullOrEmpty(script))
      {
        response.Clear();
        response.ContentType = "text/javascript";

        // Vary: Accept-Encoding をヘッダに設定。
        response.Cache.VaryByHeaders["Accept-Encoding"] = true;

        // 有効期限 (Expires ヘッダ) を 7 日に設定。
        response.Cache.SetExpires(
                DateTime.Now.ToUniversalTime().AddDays(7));
        // Cache-Control ヘッダの max-age を 7 日(604800 秒)
        // に設定。
        response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
        // Cache-Control ヘッダに must-revalidate を設定。
        response.Cache.SetRevalidation(
                HttpCacheRevalidation.AllCaches);

        int hash = script.GetHashCode();
                
        string etag = "\"" + hash.ToString() + "\"";
        string ifNoneMatch = request.Headers["If-None-Match"];
                
        // ETag を応答ヘッダに設定。
        response.Cache.SetETag(etag);
        // Cache-Control ヘッダに public を設定(ブラウザと
        // プロキシにキャッシュを許可)
        response.Cache.SetCacheability(HttpCacheability.Public);
                
        // 要求ヘッダの If-None-Match と上のコードで取得した
        // etag を比較し一致したら HTTP 304 応答を返して終了。
        // サーバーの Cache を利用する関係で Last Modified
        // は使用しない。
        if (ifNoneMatch == etag)
        {
          response.StatusCode = 
                      (int)HttpStatusCode.NotModified;
          return;
        }

        response.Write(script);
      }            
    }
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

ファイルから取得したスクリプトの文字列をサーバーのキャッシュに保持するようにし、2 回目以降の要求ではキャッシュから取得するようにしています。

キャッシュから取得する場合はスクリプトファイルの更新日時を取得できないので、Last Modified は使わず ETag のみを応答ヘッダに含めています。

ETag の値は、スクリプトの文字列から String.GetHashCode メソッドでハッシュコードを取得し、それを設定しています。

Tags:

Cache

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  July 2022  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar