WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

クロスドメインの WCF サービス

by WebSurfer 2016年12月27日 21:58

先の記事「WCF と jQuery AJAX」で書いた ASP.NET がホストする WCF サービスを、クロスドメインで利用する方法について書きます。

プリフライトリクエスト

MSDN Blog の記事「JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意」では以下の 3 つの方法が紹介されています。それぞれの概要が説明されていて分かりやすいので一読されるといいと思います。

  1. Cross Document Messaging (XDM)
  2. Cross-Origin Resource Sharing (CORS)
  3. JSONP

ここでは 2 番目の Cross-Origin Resource Sharing (CORS) という方法を使います。上の画像は CORS でのプリフライトリクエストとそれに対する応答を Fiddler で見たものです。

(最初は jQuery.ajax を利用した JSONP が一番簡単かと思ったのですが、jQuery.ajax では応答のスクリプトにコールバック jQuery1830...26( ... ) で囲んだ JSON 文字列を期待しているのに、WCF サービスは JSON 文字列しか返さないので使えませんでした)

CORS は HTML 5 の仕様なので依然として使えないブラウザがありますが、対応ブラウザが増えるにつれ今後の本命になるだろうということです。(ちなみに、IE9 は対応してませんでした。上に紹介した MSDN Blog の記事によると独自の XDomainRequest オブジェクトを使用するという回避方法はあるそうですが)

まず、この記事を書くに当たって自分が参考にさせていただいた記事へのリンクを張っておきます。私の説明では分からない場合はそちらを読まれるといいと思います。手抜きでスミマセン。(汗)

  1. HTTP アクセス制御 (CORS)
  2. CORS(Cross-Origin Resource Sharing)について整理してみた

ブラウザが CORS をサポートしていれば、クライアント側の処理はブラウザが自動的に行いますので、開発者はクライアント側の対応は何もする必要がないです。

サーバー側すなわち ASP.NET Web アプリで CORS に必要な応答ヘッダ返せるようにするだけで対応できます。基本的には Global.asax ファイルに以下のコードを記述してやれば可能になります。(厳格なセキュリティを要求せず無条件で応答するというような場合はもっと簡単になります)

protected void Application_BeginRequest(object sender, 
                                                 EventArgs e)
{
  CrossOriginResourceSharingSetting();
}

private void CrossOriginResourceSharingSetting()
{
  HttpRequest request = HttpContext.Current.Request;
  HttpResponse response = HttpContext.Current.Response;

  // プリフライトリクエストの場合
  if (request.HttpMethod == "OPTIONS")
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った
    // 方がいいかもしれないが省略。要求ヘッダの origin フィ
    // ールドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {
      string method = 
       request.Headers["Access-Control-Request-Method"];
      string header = 
        request.Headers["Access-Control-Request-Headers"];

      // 要求ヘッダに Access-Control-Request-Method および
      // Access-Control-Request-Headers がなければ何もしない
      if (!string.IsNullOrEmpty(method) && 
          !string.IsNullOrEmpty(header))
      {
        // method が GET または POST 以外は対応しない
        // header もチェックした方がいいかもしれない・・・
        if (method.Contains("GET") || method.Contains("POST"))
        {                            
          response.
            AddHeader("Access-Control-Allow-Origin", origin);                            
          response.
            AddHeader("Access-Control-Allow-Methods", method);                            
          response.
            AddHeader("Access-Control-Allow-Headers", header);

          // Access-Control-Max-Age ヘッダも任意で追加できる。
          // 指定した時間(秒)プリフライトの結果をキャッシュで
          // きるので、実際に運用に移す際は追加する方がよさそう
          //response.AddHeader("Access-Control-Max-Age", "600");

          response.End();
        }
      }
    }                
  }
  else
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った方
    // がいいかもしれないが省略。要求ヘッダの origin フィール
    // ドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {                    
      response.AddHeader("Access-Control-Allow-Origin", origin);
    }
  }            
}

上記のコードが何をしているかと言うと、要求が「プリフライトリクエスト」になる場合と「シンプルなリクエスト」になる場合とに処置を分けて、CORS に必要な応答ヘッダを設定しているだけです。詳しくはコード中のコメントに書きましたのでそれを見てください。

CORS をサポートしているブラウザがクロスドメインでどのような処置を行うかは、要求が「シンプルなリクエスト」になるか否かによって異なってきます。要求が「シンプルなリクエスト」になる条件は、上に紹介した記事「HTTP アクセス制御 (CORS)」を読んでください。

例えば、先の記事「WCF と jQuery AJAX」で言うと「(3) WCF AJAX サービスへのアクセス」のコードで getAllCars メソッドを使う方が「シンプルなリクエスト」になります。

getCarsByDoors メソッドを使う方は要求ヘッダに contentType: "application/json; charset=utf-8" が含まれるので「シンプルなリクエスト」にはならず、「プリフライトリクエスト」が事前に行われます。

シンプルなリクエスト

要求が「シンプルなリクエスト」になる場合、CORS をサポートしているブラウザからは origin: http://<要求元のドメイン> を追加する以外には普通にサーバーに要求を出します。(ただし、IE9 では要求さえ出ませんので注意)

サーバー側では origin フィールドを見て要求側のドメインがクロスドメインアクセスを許可する対象であるかを判定し、受け入れの判断をすることができます。

ただし、上の Global.asax のコードでは要求側のドメインのチェックは行っていません。要求ヘッダの origin フィールドのドメインをそのまま応答ヘッダの Access-Control-Allow-Origin に設定して返しています。それだけでも、* を設定するよりはよさそうです。

ブラウザは応答ヘッダの Access-Control-Allow-Origin: http://<求元のドメイン> を見てクロスドメインアクセスに成功したと判断し、応答に含まれる JSON 文字列などのコンテンツを処置します。

プリフライトリクエスト

要求が「シンプルなリクエスト」にならない場合、CORS をサポートしているブラウザからは実際のリクエストを送信しても安全かを確かめるための「プリフライトリクエスト」が事前に行われます。

上の画像はその要求と応答を Fiddler で見たものです。

左下のウィンドウはブラウザがクロスドメインに対する要求でかつ「シンプルなリクエスト」にならないと判定して自動的にプリフライトリクエストを要求した要求ヘッダの内容です。

赤枠で囲った部分を見てください。HTTP メソッドの OPTIONS、要求ヘッダに含まれる origin, Access-Control-Request-Method, Access-Control-Request-Headers に注目です。

サーバーがクロスドメインからのリクエストを受け、それがプリフライトリクエストであるかどうかを判断するにはそれらの情報をチェックします。

そして、プリフライトリクエストであると判断した場合は上の画像の右下のウィンドウの赤枠で囲ったような応答ヘッダを返します。ここでは、Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers は要求ヘッダで受けたものと同じ内容に設定しています。

ブラウザはそれらの情報を見て実際のリクエストを送信しても安全かを確認し、サーバーに要求を出して(上の画像で #3 がそれ)応答を取得し、コンテンツに含まれる JSON 文字列などの情報を処置します。

Tags: , , ,

AJAX

UpdatePanel と ListView

by WebSurfer 2016年11月20日 17:24

先に今回の事例から学んだ教訓を書いておきます。ちょっと今さらながらの感がありますが。

ASP.NET 4 以降では、UpdatePanel を使って非同期ポストバックを行う場合、ポストバックのトリガとするコントロールの ClientIDMode プロパティはデフォルトではなく AutoID にしておくこと。

UpdatePanel に LinkButton を含む ListView を配置した場合、LinkButton をクリックしても非同期ポストバックとならず、同期ポストバックになることからこの問題に気がつきました。(注: GridView の場合は非同期ポストバックがかかります・・・理由後述)

UpdatePanel と ListView

これは LinkButton に限らず、ASP.NET が自動生成するスクリプトの __doPostBack メソッドを利用してポストバックをかけるコントロールすべてに同様です。例えば TextBox の AutoPostBack プロパティを true にしたような場合も非同期ポストバックはかかりません。

そもそもの原因は ASP.NET サイトの記事 ASP.NET 4 Breaking Changes / ClientIDMode Changes にあるように、ASP.NET 4 からは ClientID の命名がデフォルトで Predictable となったからです。(正確に言うと、コントロールの ClientIDMode はデフォルトで Inherit だが、ページの ClientIDMode はデフォルトで Predictable なので、それを継承して結果的に Predictable になるということです)

ScriptManager が生成するスクリプトは前の方法、すなわち AutoID モードでの ClientID を期待しているので、デフォルトの Predictable では非同期ポストバックのトリガとなるコントロールとしては認識されません。

具体的にどういう話かというと、以下の通りです。

上の画像のように ListView 内に LinkButton を配置すると、ASP.NET がそれを html ソースにレンダリングする時、ClientIDMode が Predictable の場合と AutoID の場合とで id は以下のように異なります。上が Predictable、下が AutoID の場合です。

<a id="ListView1_LinkButton1_0" 
 href="javascript:__doPostBack('ListView1$ctrl0$LinkButton1','')">
 LinkButton
</a>

<a id="ListView1_ctrl0_LinkButton1"
 href="javascript:__doPostBack('ListView1$ctrl0$LinkButton1','')">
 LinkButton
</a>

__doPostBack メソッドの第一引数にはコントロールの UniqueID が設定されます。UniqueID の命名規則は、親名前付けコンテナーの ID 値とコントロールの ID 値を '$' で連結するもので、上の例では 'ListView1$ctrl0$LinkButton1' となります。

AutoID の場合、UniqueID の '$' を '_' に置き換えた文字列が id の文字列と同じになるとことに注目してください。Predictable ではそうはなりません。

ScriptManager がページに配置されると、それが生成する JavaScript のコードでポストバックを制御するようになりますが、その時 UniqueID を使ってトリガとなった html 要素を探し、同期 or 非同期どちらのトリガになるかを判定します。

具体的には、ScriptManager が生成する _doPostBack という名前のメソッドの中で、ポストバックのトリガとなったコントロールの UniqueID の '$' を '_' に変換した id で getElementById メソッドを使って要素を探しに行きます。

なので、ClientIDMode がデフォルトでは見つからない、AutoID の場合は見つかるということになります。

上のステップで html 要素が見つかった場合は非同期ポストバックのトリガと判定され、直ちに非同期ポストバックがかかるようになっています。

上のステップで直接当該要素を見つけられなかった場合は、_findNearestElement というメソッドを使って上位の要素を探しに行きます。上の例の html ソースの UniqueID の場合は最終的に ListView1 という名前の id の要素を探しに行きます。

ところが ID="ListView1" という ListView コントロールから ASP.NET が生成する html 要素(table)の id は "ListView1_itemPlaceholderContainer" になり、ListView1 という名前の id の要素は見つからないという結果になります。

直接見つからず、その親も見つからなかった場合は、当該要素は非同期ポストバックのトリガとは見なされず、同期ポストパックがかかるという結果になります。

ただし、ListView に代えて GirdView を使った場合は、その中に配置した LinkButton クリックでも非同期ポストパックがかかります。その理由は、例えばサーバー側での ID を "GridView1" とした場合、html 要素(table)の id は同じく "GridView1" になるので、_findNearestElement メソッドで親が見つかるからです。

しかし、やはり正解は、ListView、GridView に関係なく、UpdatePanel を使って非同期ポストバックを行う場合トリガとするコントロールの ClientIDMode プロパティは AutoID にしておくことだと思います。

Tags: , ,

AJAX

進捗状況の表示

by WebSurfer 2015年12月28日 18:07

ASP.NET Web Forms アプリで、サーバー側にて非同期で行われている処置の進捗状況を Label コントロールや jQuery UI の Progressbar を使って表示する方法を書きます。

進捗状況の表示

ScriptManager, UpdatePanel, UpdateProgress を使えば非同期要求をかけて応答が戻ってくるまでの間、サーバーで処理中という情報をユーザーに示すことはできます。具体的な方法は MDSN ライブラリの記事UpdateProgress コントロールの概要を見てください。

しかしながらサーバー側での処置が何 % 終わったかというような進捗状況を表示することはできません。何故ならサーバー側での処理が終わるまでサーバーからは何の応答も返ってこないからです。

サーバー側での処理の進捗状況をブラウザに表示する場合、サーバー側での処理は別スレッドで行うようにし、ブラウザからタイマーを使って定期的に AJAX で進捗状況をサーバーに問合せ、サーバーからその応答をもらって表示するということは可能です。

時間のかかる処理をこのような方法で行うのが適切か、Web ファーム / ガーデン対応やワーカープロセスのリサイクル対応はどうするかという話はちょっと置いておいて、こうすれば何とかできるという具体的な例を以下に書きます。

(Thread を使うのは "don't even think about it" という意見もありますので注意してください。詳しくはこの記事の下の方の 2015/12/29 追記を見てください)

まず「時間のかかる処理」ですが、処置を行うだけでは当然ダメで、何らかの方法で進捗状況を取得できる必要があります。(進捗状況が取得できないような処理もあるかと思いますが、そういう場合はお手上げです)

具体例は以下のコードを見て下さい。DoTask メソッドで処置を行うと同時に進捗(以下の例では 0 ~ 100 まで)を記録し、Progress プロパティで進捗状況を取得できるようにしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;

public class Task
{
    public int Progress { get; set; }
    
    public Task()
    {
        this.Progress = 0;
    }
    
    public void DoTask()
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(100);
            this.Progress = i + 1;
        }
    }    
}

ASP.NET Web Forms アプリのページで上のコードの処理を別スレッドで実行します。Timer を使って 1 秒毎に非同期要求をかけ、Progress プロパティで進捗(上のコード例では 0 ~ 100 まで)を取得し、Label コントロールと jQuery UI の Progressbar に表示するようにしています。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Threading" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  // Task は App_Code に定義したカスタムクラス
  // その中の DoTask メソッドで処理を行う
  // 進捗状況は Progress プロパティで取得する
  private Task task = null;

  protected void Page_Load(object sender, EventArgs e)
  {
    // Session["Task"] が null でなければ処理中
    object obj = Session["Task"];
    if (obj != null)
    {
      task = (Task)obj;
    }
  }

  protected void Button1_Click(object sender, EventArgs e)
  {
    // 処理中の場合は何もしないで return
    if (task != null)
    {
      return;
    }

    // Task クラスを初期化して Session に保持
    task = new Task();
    Session["Task"] = task;
        
    // 別スレッドで処理を実行
    Thread newThread = new Thread(task.DoTask);
    newThread.Start();
  }

  // Timer.Interval プロパティに設定したインターバル
  // (この例では 1 秒)で非同期呼び出しがかかって
  // Page_Load メソッドのあとこのメソッドが実行される
  protected void Timer1_Tick(object sender, EventArgs e)
  {
    if (task != null)
    {
      if (task.Progress == 100)
      {
        Label1.Text = "完了";
                
        // ラベルに表示しない場合もこれだけは必要
        Session.Remove("Task");
      }
      else
      {
        Label1.Text = task.Progress.ToString() + "%";
      }

      // Progress Bar に表示する進捗データを設定
      if (ScriptManager.GetCurrent(this).IsInAsyncPostBack)
      {
        ScriptManager.GetCurrent(this).
          RegisterDataItem(this, task.Progress.ToString());
      }
    }
    else
    {
      Label1.Text = "処置は行われていません";
    }        
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
  <script src="/jquery.js" type="text/javascript"></script>
  <script src="/jquery-ui.js" type="text/javascript"></script>
  <link href="/jquery-ui.css" rel="stylesheet" type="text/css" />
  <style type="text/css">
    .ui-progressbar {
        position: relative;
    }

    .progress-label {
        position: absolute;
        left: 50%;
        top: 4px;
        font-weight: bold;
        text-shadow: 1px 1px 0 #fff;
    }
  </style>
  <script type="text/javascript">
  //<![CDATA[

    function pageLoad(sender, args) {
      if (args.get_isPartialLoad() === false) {
        var manager = 
          Sys.WebForms.PageRequestManager.getInstance();
        manager.add_endRequest(OnEndRequest);
      }
    }

    // Timer で非同期要求がかかり応答が戻ってくるたびに以下の
    // メソッドが実行される
    function OnEndRequest(sender, args) {
      // サーバー側の Timer1_Tick メソッドで設定した進捗
      // データをクライアント側で取得
      var progress = args.get_dataItems()["__Page"];

      // 進捗データを Progress Bar に表示する
      if (progress) {
        $("#progressbar").
          progressbar("value", Number(progress));
      }
    }

    // jQuery UI のデモのコードを借用
    $(function () {
      var progressbar = $("#progressbar"),
          progressLabel = $(".progress-label");

      progressbar.progressbar({
        value: false,
        change: function () {
          progressLabel.
            text(progressbar.progressbar("value") + "%");
        },
        complete: function () {
          progressLabel.text("Complete!");
        }
      });
    });
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <asp:ScriptManager ID="ScriptManager1" runat="server">
  </asp:ScriptManager>

  <%--これが ProgressBar になる
  jQuery UI のデモのコードを借用--%>
  <div id="progressbar">
    <div class="progress-label">Loading...</div>
  </div>

  <asp:Button ID="Button1" runat="server" 
    Text="Start Task" OnClick="Button1_Click" />

  <asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
      <asp:Label ID="Label1" runat="server" />
    </ContentTemplate>
    <Triggers>
      <asp:AsyncPostBackTrigger ControlID="Timer1" 
        EventName="Tick" />
    </Triggers>
  </asp:UpdatePanel>

  <asp:Timer ID="Timer1" runat="server" 
    Interval="1000" OnTick="Timer1_Tick">
  </asp:Timer>
  </form>
</body>
</html>

上記のコードを Chrome から呼んで、[Start Task]ボタンクリックで処置を開始し、その進捗状況を表示したのが上の画像です。

-------- 2015/12/29 追記 --------

MSDN Blog の記事 Performing Asynchronous Work, or Tasks, in ASP.NET Applications によると、Thread を使うのは、その記事を書いた Microsoft の開発者よりはるかにスマートに実装できるのでなければ "don't even think about it" だそうです。(汗)

理由はその記事の FAQ の 4 番目に書いてありますが、以下に概略を書いておきます(誤訳はあるかも)。

  1. CLR ThreadPool を使用するのに比べて非常にコストが高い。
  2. 自分で作った Thread に I/O 要求が残ってないか終了前にチェックしなければならない。
  3. システムのパフォーマンスを保つには実行されている Thread の数が適切でなければならないが、自分で Thread を作るのであればパフォーマンスを保つのは自分の責任になる。

暇なサイトならともかく、要求が多いサイトの場合はやはり問題になりそうです。

Tags:

AJAX

About this blog

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

Calendar

<<  2017年3月  >>
2627281234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar