WF (ワークフロー) を使った Web ページ の画面遷移 : 実践編 (Part 3)


環境:
Internet Information Services 7.0
Visual Studio 2008 Beta 2

こんにちは。

Part1 : アクティビティ (Activity) の実装
Part2 : フロー (Workflow) の実装
Part3 : IHttpHandler を使ったホストの実装

今日は、上記の Part 3 として、IHttpHandler を使用したホスト側 (IIS 上で動く、実際の画面遷移の実行側) の処理を作成します。

ここでは、前回までに作成したワークフローと連携して画面の遷移を実行します。処理は IHttpHandler を継承させたカスタムのハンドラとして作成し、IIS にカスタムハンドラとして登録することで実行します。(なお、今回は IIS 7 を使用しています。)
そして遷移の際には、Request オブジェクト (組み込みオブジェクト) の Redirect メソッドを使用して画面を飛ばします。その際、その他の引数などもわたるように、受け取った Request の内容 (Form や QueryString) をそのままつぎのページに渡します。(前回も記載した通り、今回はファイルなど大量データの受け渡しはないという前提で GET で渡すことにします。)

では、さっそく、そのハンドラ側を作成していきましょう。

Visual Studio から Web サイトを新規に作成し、App_Code の中に、以下のクラスたちを実装していきます (こうすることで、bin フォルダに dll を入れたときと同じようにハンドラを組み込むことができます)。ハンドラでは、実装コードとして、ProcessRequest と IsReusable を実装する必要があります。実際の処理は ProcessRequest の中に記述していきます。

using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using BuyStepflow; // <-- 今回作成したワークフローの名前空間です
using System.Collections;
using System.Workflow.Runtime.Hosting;

/// <summary>
/// 画面遷移をおこなうハンドラです (中身は、これから実装します、、、)
/// </summary>
public class BuyWorkflowController : IHttpHandler
{
 public BuyWorkflowController()
 {
 }

    #region IHttpHandler メンバ

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
    }

    #endregion
}

さて、ProcessRequest の中身を記述する前に、前回 (Part1, Part2) までに記述したデータ交換サービス (EDS) を使ったデータ交換のホスト側の準備をしておきましょう。
前回までに作成した  IBuyStepExchange インタフェースを実装して、このホスト専用の実装を定義します。前回までに記述したように、この中の DoProceed メソッドが、ワークフローインタスタンス側から CallExternalMethod により呼ばれるメソッドでした。そして、この中で受け取った action パラメータの値に応じて "Select" (Workflow1.ActionTypeSelect) なら商品選択画面へ飛ばし、"Payment" (Workflow1.ActionTypePayment) なら支払い画面へ飛ばす、というように実装することになっていました。

ここで、まず 1 つめのポイントがあります。それは、ワークフローインスタンス ID (Workflow Instance ID) です。
この ID を使用して、次回の画面遷移をおこなう際にどのインスタンスを起動するか指定することで、画面遷移中の 1 インスタンスがワークフローの 1 インスタンスと紐付けられます。そこでこの ID は、遷移のあいだ中、どこかにおぼえておく必要があります。
インスタンスの作成時に作成されたインスタンスの識別子 (ワークフローインスタンス ID) をどこかに保管して画面遷移時に渡しても良いですが、今回は DoProceed 関数で遷移するので、このデータ交換サービスのメソッド (DoProceed) が渡すワークフローインスタンス ID をそのまま画面に渡すことにしましょう。また画面の側では、hidden などで入れておき、次回実行時に今度は Form 引数としてハンドラ側にこの workflowId を渡すようにしておきましょう。(Session を使う方法などいろいろ考えられますが、今回はこうしておきます。)

さらに、前述の通り、受け取ったその他の引数を URL へ入れて飛ばす必要があります。ハンドラ側に渡してくる Form や QueryString など一連の引数達は、後ほど ProcessRequest の中で取得して Hashtable に入れて使用するので (ワークフロー側にもこれらの情報を渡す必要があります)、その Hashtable を下記の BuyStepExchangeService のコンストラクタで渡して、BuyStepExchangeService 内ではこの渡された Hashtable を使用しましょう。

データ交換サービスのインタフェースの実装コードは以下の通りになります。

public class BuyStepExchangeService : IBuyStepExchange
{
    private HttpContext ctx;
    private Hashtable requests;

    public BuyStepExchangeService(HttpContext pContext, Hashtable pRequests)
    {
        ctx = pContext;
        requests = pRequests;
    }

    #region IBuyStepExchange メンバ

    public void DoProceed(Guid id, string action)
    {
        string redirectUrl = String.Empty;

        switch (action)
        {
            case Workflow1.ActionTypeSelect:
                redirectUrl = "~/start.php?workflowId=" + id.ToString();
                break;
            case Workflow1.ActionTypePayment:
                redirectUrl = "~/payment.php?workflowId=" + id.ToString();
                break;
            case Workflow1.ActionTypeConfirm:
                redirectUrl = "~/confirm.php?workflowId=" + id.ToString();
                break;
        }

        foreach (string var in requests.Keys)
        {
            if(var != "workflowId")
                redirectUrl += "&" + HttpUtility.UrlEncode(var) + "=" + HttpUtility.UrlEncode(requests[var].ToString());
        }

        ctx.Response.Redirect(redirectUrl, false);
    }

    public event EventHandler<BuyStepEventArgs> Processed;

    #endregion

}

また、逆に、ホスト側からワークフローインスタンス側へ渡すイベントですが、上記ではイベントハンドラの定義のみをおこなっていますので、このイベントハンドラを使用して処理をするメソッドも以下の通り作成しておきましょう。

public class BuyStepExchangeService : IBuyStepExchange
{

    ....... 途中省略

    public void FireProcess(Guid pId, Hashtable ht)
    {
        EventHandler<BuyStepEventArgs> processed = this.Processed;
        processed(null, new BuyStepEventArgs(pId, ht));
    }
}

では、本題の ProcessRequest の処理に入っていきましょう。

まず、基本的な流れは簡単です。

  1. ワークフローランタイム (WorkflowRuntime) オブジェクトを作成する
  2. 上記で作成したデータ交換サービス用の BuyStepExchangeService をランタイムに設定する
  3. 上記のランタイム上で、WorkflowId が渡されていなければインスタンスを新規作成し、WorkflowId が渡されていればそのインスタンスに対して上記で作成した FireProcess メソッドを呼び出す

本来は、これだけで終了です。
これにより、前回までに作成したワークフロー側で HandleExternelEvent が呼び起こされて (新規作成の場合はフローの処理がはじまって)、ワークフローが進んでいき、ワークフローの中 (厳密には Part1 で作成した BuyActivity の中) の CallExternalMethod で今度はホスト側で作成した BuyStepExchangeService.DoProceed が呼ばれて、上記のコードの通り、action に応じてページにリダイレクト (Redirect) されます。

と一見うまくいくように思えますが、Part2 の最後で記載したとおり、1 つ重大な問題があります。
それは、ワークフローインスタンスは、ホストとは別のスレッドで実行されるということです。もしこうした点を放置してそのまま処理を進めると、別のマネージスレッド (Maneged Thread) から Redirect が呼び出され、例外が発生するか、何も起こらないといったところがオチでしょう。(やってみたことはありませんが、きっとそんな感じです、、、)
この解決策は、Windows Server 2008 の Web キャスト集をみていただいた方なら、「そういう場合は、いつものあれでしょ!」という勘が身についていると思いますが、ManualWorkflowSchedulerService というものを使用します。
これは、別スレッドで実行してくれる既存の DefaultWorkflowSchedulerService を変更し、その名の通り、すべて手動で実行を制御するというものです。このため、呼び出し元とスレッドも同一スレッドで実行されます (もしスレッドをわけたければ、開発者がわざわざ自分でわけて実行させる必要があります)。また、RunWorkflow メソッドを使って、自分でスケジュールを実行させる必要もあります。これを使用すると、制御はワークフローインスタンス側に渡され、その後は CallExternalMethod から DoProceed が呼ばれても呼び出し元と同じスレッドで入ってきます (つまり、スレッド上は、ホスト側から別の関数を普通に呼び出していることと同じ動作になります)。また HandleExternalEvent のようなアイドルを伴う処理に来ると、スケジュール (Scheduling) は停止され、制御が RunWorkflow メソッド直後に戻ります。これで一件落着です。

そしてもう 1 つポイントがあります。
これも Windows Server 2008 の Web キャスト集をみていただいた方ならお分かりと思いますが、ワークフローインスタンスは処理終了と共にメモリから消えてしまい、次回実行時にインスタンスは跡形もなくなっていることでしょう。
これを避けるには、永続化サービスを使用します。今回は、コードを複雑にしないため、WF が組み込みで持っている SqlWorkflowPersistenceService を使用しましょう。この際、データベース側もあらかじめ作成しておきましょう。(データベースの作成は 5 分で終わります。詳細は、Windows Server 2008 の開発者向け Web キャストを参照してください。)
また、永続化サービスが確実にアンロードをおこなうように、StopRuntime も忘れずに実行しておきましょう。

ここまでのことを守って実装すると、コードは以下の通りになるでしょう。

public void ProcessRequest(HttpContext context)
{
    // ブラウザが渡してきた変数を取得し Hashtable に入れる
    Hashtable ht = new Hashtable();
    foreach (string qryVar in context.Request.QueryString)
    {
        ht[qryVar] = context.Request.QueryString[qryVar];
    }
    foreach (string formVar in context.Request.Form)
    {
        ht[formVar] = context.Request.Form[formVar];
    }

    // ワークフローランタイムのプロビジョニング
    WorkflowRuntime wr = new WorkflowRuntime();

    ManualWorkflowSchedulerService mss = new ManualWorkflowSchedulerService();
    wr.AddService(mss);

    ExternalDataExchangeService exServ = new ExternalDataExchangeService();
    wr.AddService(exServ);
    BuyStepExchangeService buyServ = new BuyStepExchangeService(context, ht);
    exServ.AddService(buyServ);

    SqlWorkflowPersistenceService perServ = new SqlWorkflowPersistenceService(@"data source=.SQLEXPRESS;Initial Catalog=buywfpersistdb;Integrated Security=SSPI");
    wr.AddService(perServ);

    wr.StartRuntime();

    // DB に眠っているインスタンスをたたき起こす! (または新規作成)
    Guid workflowId;
    if (ht.Contains("workflowId"))
    {
        workflowId = new Guid(ht["workflowId"].ToString());
        buyServ.FireProcess(workflowId, ht);
    }
    else
    {
        WorkflowInstance wi;
        wi = wr.CreateWorkflow(typeof(Workflow1));
        wi.Start();
        workflowId = wi.InstanceId;
    }
    mss.RunWorkflow(workflowId);

    // データベースにシリアライズ (次回の呼び出しで使用)
    wr.StopRuntime();
}

あとは、ハンドラを登録するだけです。IIS 7 なら web.config を手で編集してもらっても結構ですし、画面から設定したい方は、IIS Manager を立ち上げて [ハンドラ マッピング] (英語版の場合は [Handler Mapping]) のモジュールを選択して、[マネージハンドラの追加] をクリックして以下の通り入力します。

要求パス: sellingflow.wf
種類: (上記で作成したハンドラを選択)
名前: (適当に一意な名前を入力)

すると、sellingflow.wf にアクセスすると、ワークフローインスタンスが開始され、画面の遷移がはじまります。
飛び先のページ内 (htm, php, aspx, 等々) では、画面遷移の要求時に再度 sellingflow.wf へ飛ばし、少なくとも workflowId だけは受け取った値を渡すように作成しておきましょう。

またデバッグ方法ですが、IHttpHandler などのデバッグは、web.config で

<compilation debug="true">

を設定して、w3wp.exe のプロセスへアタッチすることでおこなうことができます。(Visual Studio の [デバッグ] - [プロセスにアタッチ] で、[すべてのセッションのプロセスを表示する] にチェックを付け、w3wp.exe を選択します。尚、使用するアプリケーションプールが一度でも使用されないとワーカープロセスは生成されないので、一度ページにアクセスしてから選択してください。またもう 1 つコツをお教えしておくと、アプリケーションプールがワークフローのアセンブリをキャッシュするので、ワークフロー側にバグがあって修正した場合は、使用しているアプリケーションプールも再起動させる必要があります。)

といった具合ですが、初回にも記述しましたが、これは仕組みを理解するためだけの粗末なサンプルであるという点は忘れないでください。

例えば、ホスト側のこの処理は、クラスで隠蔽しておくともっと美しくなるはずです。(今回は、ホスト側も、ワークフロー側の実装を相当意識して作成してしまっています。)

また、話を複雑にしないために SqlWorkflowPersistenceService を使いましたが、ここは、現実の開発ではもっと考慮されるべきでしょう。
例えば、永続化サービスを使用しているので、遷移の最中で画面を閉じるとデータベースに内容が残ってしまいます。また、画面遷移の状態の記憶のためだけに専用の複雑なデータベースを設けるというのも少々やりすぎな感があります。もっと軽量なカスタムの永続化サービスで充分でしょう。(カスタム永続化サービスの実装は簡単です。こちら からもご紹介しているように SDK のサンプルとして入っています。)
ただし、軽くしすぎてローカルのファイルへの保存などは避けたほうが良いでしょう。負荷分散 (load balancing) やファーム構成に配慮し、データベースもしくはそれに準じた仕組みを使うことは必要です。
そうした視点から考えると、実際、セッションが似たコンセプトで機能提供していますので、Session にシリアライズするカスタムの永続化 (persistence service) というのも良いかもしれません。(その場合、Session が肥大化しないように、取得した Hashtable の変数の使用後のクリーンアップなども配慮する必要があるでしょう。)

ハンドラのマッピングについても、上記のようにスタティックなページでなく、例えば、ハンドラは拡張子だけで登録 (*.wf で登録) しておき、URL によって呼び出すワークフローを変えるという方法にするとさらに汎用性が高くなるでしょう。(例: payment.wf の場合は payment ワークフローを実行し、receive.wf の場合は receive ワークフローを実行する、など)

また、管理者やエンドユーザに画面を管理させたいならば、ワークフローデザイナーのリホスティングも有効でしょう。

このように、もっと美しく完成度の高いエンジンに進化させて、コミュニティリソースや、フリーウェア/シェアウェア、プロダクトなどの形で汎用化してもおもしろいかもしれません。Microsoft では思いもつかないような『粋』なモジュール達がコミュニティなど多くのリソースから登場してくると、さらに面白い世界が広がっていくことでしょう。

Longhorn には、そうした可能性がたくさんつまっています。

Skip to main content