Welcome to MSDN Blogs Sign in | Join | Help

[CLT (Community Launch Team) イベント in 札幌 follow-up] WCF と WF の連携

// 2007.12.27 変更: RTM版にあわせてソースを修正しました 

こんにちは。

先日の土曜、札幌にて、Microsoft主催による CLTイベント が開催されました。

内容は、以下の通りです。 

  1. 開発者のための WPF 入門 (赤坂玲音氏)
  2. WCF/WFとその連携 (松崎剛)
  3. 開発プラットフォームとしての 2007 Office System (スタンドアロン編) (今村丈史氏)

赤坂玲音さんによる「WPF入門」では、WPF を XAML でなく敢えて VB/C# のコードで見ることでその動きの詳細や使われているテクノロジーを理解するというセッションで、WPF の中身を解剖的に理解することのできるという点で、深みの中に分かりやすさがある、「WPFはじめての人」と「WPFを知っている人」の双方が楽しめる内容でした。(赤坂さんは こちら の本を書かれている方です。続きを知りたいという方は是非参考にしてみては如何でしょう。)
今村丈史さんの「開発プラットフォームとしての2007 Office System」では、VBA と VSTO/VSTA の技術的な相違やそれがもたらす実開発への影響、さらにはVSTAが他のオフィス製品で開発されなかった背景などの本音と建前の部分なども含め解説されていて、内部動作に加え、社員ではなかなかイベントで話せない切り口なども含んだコミュニティならではの貴重な(背景なども含めて全般的に理解できる)セッションでした。

今後も全国各地でさまざまなテーマによる開催が予定されていますので、各地の皆さんも是非楽しみにしてください。

以下に、上記の私のセッション (Windows Communication Foundation と Windows Workflow Foundation の連携) でご紹介させて頂いたソースコードを掲載しておきます。
当日はまったく時間が足りず懇親会で続きのデモを実施したのですが、懇親会へご参加されなかった方も以下にソースを載せておきますのでご参照ください。(尚、この内容は、今後、Longhorn Server の開発者向けページ のほうでもデモキャストなどの形で展開していこうと思います。)(デモを同開発者向けページに掲載しました。2007/06/07 追記)

まずは、昨日ご参加された方のために、ソースを掲載しておきます。

-------------------------------------------------------------------------

【サンプル1:WCF サービスのバックエンドで WF を使う (Visual Studio 2005版)】

Windows SDK 付属のデモでは、WorkflowCompleted イベントを別メソッドで受けて特別な返し方をしていますが、 ここでは、オペレーション実装中の変数などをそのまま使えるように内部のブロックとして作成しています。

別プロジェクトでワークフローを作成し、WCFの実装サイドもワークフローで使用するライブラリ(System.Workflow.Activities、System.Workflow.ComponentModel、System.Workflow.Runtime) とワークフロープロジェクトへの参照を追加してください。

(以下では、デモのため、Food の Name 属性のみをワークフローで解決していますが、無論、Food そのものをワークフローで設定することも可能です。)

using System;
using System.ServiceModel;
using System.Runtime.Serialization;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Workflow.ComponentModel;
using System.Collections.Generic;
using System.Workflow.Runtime.Hosting;

namespace SimpleWCF
{

    [ServiceContract()]
    public interface IDinnerService
    {
        [OperationContract()]
        Food GetFoodByID(int ID);
    }

    [DataContract]
    public class Food
    {
        private int price;
        private string name;

        [DataMember]
        public int Price
        {
            get { return price; }
            set { price = value; }
        }
        [DataMember]
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

    }

    public class DinnerService : IDinnerService
    {
        WorkflowRuntime wr = Init();

        private static WorkflowRuntime Init()
        {
            WorkflowRuntime wr = new WorkflowRuntime();
            ManualWorkflowSchedulerService mss = new ManualWorkflowSchedulerService();
            wr.AddService(mss);
            wr.StartRuntime();
            return wr;
        }

        public Food GetFoodByID(int ID)
        {
            Food food = new Food();

            Dictionary<string, object> parms = new Dictionary<string, object>();
            parms.Add("FoodID", ID);

            WorkflowInstance wi = wr.CreateWorkflow(typeof(WorkflowLibrary1.Workflow1), parms);
            wi.Start();

            EventHandler<WorkflowCompletedEventArgs> d = null;
            d = delegate(object sender, WorkflowCompletedEventArgs args)
            {
                if (args.WorkflowInstance.InstanceId == wi.InstanceId)
                {
                    food.Name = args.OutputParameters["FoodName"].ToString();
                    wr.WorkflowCompleted -= d;
                }
            };
            wr.WorkflowCompleted += d;
            ManualWorkflowSchedulerService mss = wr.GetService<ManualWorkflowSchedulerService>();
            mss.RunWorkflow(wi.InstanceId);

            return food;
        }
    }

}

-------------------------------------------------------------------------

【サンプル2:WCF サービスのバックエンドで WF を使う (Duplex 版) (Visual Studio 2005版)】

この場合は、ご説明したとおり、Duplex で応答すればよいので、デフォルトのスケジューラサービスを使ってOKです。

サンプルソースは、Microsoft Windows SDK の [Windows SDK Document] のヘルプを開き、[.NET Framework Development] - [Samples] - [Cross Technology Samples] - [Integration Samples for .NET Framework 3.0 Feature] - [Hosting Workflow in WCF with a Duplex Contract] からソースをダウンロードしてください。

-------------------------------------------------------------------------

【サンプル3:Workflow 専用のキュー (EDS) を使った WCF と WF の動的連携 (Visual Studio 2005版)】

ご紹介した EDS (External Data Exchange Service) というイベント稼動な連携シナリオを使った WCF と WF の連携方法は、Code Project の以下の記事にノウハウが掲載されています。

http://www.codeproject.com/WF/FireWorkflowEvent.asp

-------------------------------------------------------------------------

【サンプル4:Oracs のインテグレーションメカニズムを使った WCF と WF の動的連携 (Visual Studio Codename Orcas)】

以下、デモでお見せしたOrcasを使った連携です。 
このデモでは、クライアントがOKするまで注文を続け、注文が終了したら、クライアントからレシートの要求をするというサービスとクライアントを作成します。

まず、WCF Service のプロジェクトを作成し(IOrderService.cs とします)、以下の通りコントラクトのみ作成します。(ワークフローが実装そのものになりますので、コントラクトだけ作成すれば充分です。)
ここでは、注文をおこなう BuyFoodByID オペレーションと、レシート要求をする GetReceipt オペレーションのコントラクトを定義します。

using System;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace WCFWFDemo.RestaurantContract
{
    [ServiceContract]
    public interface IOrderService
    {
        [OperationContract]
        void BuyFoodByID(int orderID);
        [OperationContract]
        Receipt GetReceipt();

    }

    [DataContract]
    public class Receipt
    {
        private int price;

        [DataMember]
        public int Price
        {
            get { return price; }
            set { price = value; }
        }
    }

    [DataContract]
    public class FaultDataContract
    {
        private int id;

        [DataMember]
        public int ErrorId
        {
            get { return id; }
            set { id = value; }
        }

        private String message;

        [DataMember]
        public String ErrorMessage
        {
            get { return message; }
            set { message = value; }
        }

    }
}

つぎに、ワークフローライブラリのプロジェクトを作成し、上記の WCF Service のプロジェクトを参照して、以下の通りワークフローを作成します。 (これが、コントラクトの実装そのものになります。)
ここでは、初期化のための [Code Activity] と、[While] アクティビティ、要求を待機する [Listen] アクティビティと、Orcas から新しく追加された [ReceiveActivity] (2つ) を以下の通り設定しています。

 

このワークフローでは、BuyFoodByIDが呼ばれ続ける間ワークフローを終了せずにループし、GetReceipt が呼ばれたらワークフローを終了して While ループを抜けるという処理を作成します。

このため、まずは、GetFoodByID の [RecieveActivity] (左側) のアクティビティを選択し、以下の通りプロパティを設定します。

  • [Activity] – [ServiceOperation] プロパティ
    プロパティ画面の [...] ボタンを押して、上記で作成した IOrderService の GetFoodByID オペレーションを選択します。これにより、WCFで GetFoodByID が呼ばれると、このアクティビティに処理がディスパッチされます。
  • [Activity] – [CanCreateInstance] プロパティ
    このプロパティを true にすると、このメソッドが呼ばれると同時にワークフローインスタンスが作成され、ワークフローが開始されるようになります。よって、ここ
    true に設定しておきます。
  • [Parameters] プロパティ
    ここに変数を定義することで、入力引数や ReturnValue をワークフロー変数にバインドすることができます。(要求が呼び出されると、クライアントが入力した引数はこの変数に設定されます。)
    ここでは、入力引数である ID が表示されていますので、デモで実施したように、ここに新規に変数を割り当てます。

同様に、右側の ReceiveActivity のプロパティも以下の通り設定します。

  • [Activity] – [ServiceOperation] プロパティ
    上記同様に、IOrderService の GetReceipt オペレーションを選択します。
  • [Activity] – [CanCreateInstance] プロパティ
    ここ
     false に設定しておきます。
  • [Parameters] プロパティ
    今度は、ReturnValue が表示されていますで、ここに変数を割り当てます。(DataContract である Receipt 型の入れ物が定義されますが、ここにはインスタンスは設定されていないので、あとからコードで設定します。)

つぎに、各 ReceiveActivity に実際の処理を実装するため、アクティビティを設定していきます。(ReceiveActivity は Composite Activity ですので、中にワークフローをネストできます。)
ここでは、簡単のため、[Code Activity] を1つずつ設定してその中のハンドラで処理を記述します。 

ここまで準備ができたら、ワークフローのハンドラ等の処理を実装します。

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;

namespace WCFWFDemo.RestaurantWorkflow
{
 public sealed partial class OrderWorkflow: SequentialWorkflowActivity
 {
  public OrderWorkflow()
  {
   InitializeComponent();
  }

  ...... (中略) ......

        // 初期化の [Code Activity] のハンドラ
        private void createFood_ExecuteCode(object sender, EventArgs e)
        {
            // Receipt インスタンスを新規に作成し、上記で作成した GetReceipt の ReturnValue の変数 (GetReceipt__ReturnValue_1) に設定
            WCFWFDemo.RestaurantContract.Receipt receipt = new WCFWFDemo.RestaurantContract.Receipt();
            receipt.Price = 0;

            GetReceipt__ReturnValue_1 = receipt;
        }

        // While Activity のコードコンディションを作成
        // (フラグを作成し、フラグが立つまでループをおこなう)
        private bool CompleteFlag = false;
        private void isFinished(object sender, ConditionalEventArgs e)
        {
            e.Result = !CompleteFlag;
        }

        // GetFoodByID の [Receive Activity] に挿入した [Code Activity] のハンドラを記述
        private void SetPrice_ExecuteCode(object sender, EventArgs e)
        {
            switch (BuyFood_orderID1)
            {
                case 1:
                    GetReceipt__ReturnValue_1.Price += 3000;
                    break;
                default:
                    GetReceipt__ReturnValue_1.Price += 1000;
                    break;
            }

        }

        // GetReceipt の [Receive Activity] に挿入した [Code Activity] のハンドラを記述
        // (フラグを立てる)
        private void SetReceipt_ExecuteCode(object sender, EventArgs e)
        {
            CompleteFlag = true;
        }

    }

}

最後に、この WCF サービスをホストするアプリケーションを作成します。(ここではそのままホストしていますが、WASを使うこともできます。但し、Factory の設定など、Service.svc に特別な記述が必要になります。)
WCF と WF のインテグレーションを自動化してくれる専用の WorkflowServiceHost を使いますので、Orcas で追加された System.WorkflowServices.dll への参照を追加しておいてください。(このサービスホストが、WorkflowInstanceId をおぼえておくなど、面倒な処理のいっさいをすべて実施してくれます。)

また、以下で、WSHttpContextBinding というバインディング方法が使われていることに注意してください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using WCFWFDemo.RestaurantContract;
using WCFWFDemo.RestaurantWorkflow;

namespace WCFWFDemo.Host
{
    class Program
    {
        static void Main(string[] args)
        {
            WorkflowServiceHost sv = new WorkflowServiceHost(typeof(OrderWorkflow));
            WSHttpContextBinding contextBinding = new WSHttpContextBinding();
            sv.AddServiceEndpoint(typeof(IOrderService), contextBinding, "http://localhost:8080/OrderService");
            sv.Open();

            Console.WriteLine("If you finish, press [Enter] !");
            Console.ReadLine();
            sv.Close();
        }
    }
}

これでサービスは完了です。

つぎに、クライアントを実装します。
今回は、以下のような画面を持つアプリケーションを作成しました。

ボタンを押した際の処理を以下の通り実装します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace WCFWFDemo.Client
{
    public partial class Form1 : Form
    {
        ChannelFactory<WCFWFDemo.RestaurantContract.IOrderService> factory;
        WCFWFDemo.RestaurantContract.IOrderService proxy;

        public Form1()
        {
            InitializeComponent();

            WSHttpContextBinding binding = new WSHttpContextBinding();
            factory = new ChannelFactory<WCFWFDemo.RestaurantContract.IOrderService>(binding, "http://localhost:8080/OrderService");
            proxy = factory.CreateChannel();
        }

        // Buy ボタンを押したときの処理
        private void button1_Click(object sender, EventArgs e)
        {
            proxy.BuyFoodByID(1);
        }

        // Pay ボタンを押したときの処理
        private void button2_Click(object sender, EventArgs e)
        {
            WCFWFDemo.RestaurantContract.Receipt receipt = proxy.GetReceipt();
            textBox1.Text = receipt.Price.ToString();
        }
    }
}

-------------------------------------------------------------------------

【サンプル5:Oracs のインテグレーションメカニズムを使った WCF と WF の動的連携 (Durable版) (Visual Studio Codename Orcas)】

さいごにお見せしたデモはこちらです。

ワークフローを使ったホストの場合、Long-running ワークフローなどの永続化サービスなども考慮し、サービスとクライアントで生存状態の独立したサービス実装が必要となってくることでしょう。Orcas では、こうした点にも配慮されています。 

サービス側で従来の Persistent サービスを使った実装との組み合わせができることはもちろんですが、クライアント側からの接続においても Durable なサービス (サービスを接続をいったん終了して、後日、再度同じインスタンスにつなぎなおす実装) が可能です。

以下のように、[Save Context] ボタン、[Load Context] ボタンを作成します。

 

これらボタンに対し、以下の通り実装します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.Runtime.Serialization;

using System.ServiceModel.Channels;
using System.IO;
using System.Xml;
using System.Runtime.Serialization.Formatters.Binary;

namespace WCFWFDemo.Client
{
    public partial class Form1 : Form
    {
        ChannelFactory<WCFWFDemo.RestaurantContract.IOrderService> factory;
        WCFWFDemo.RestaurantContract.IOrderService proxy;

        private IContextManager mgr;

        public Form1()
        {
            InitializeComponent();

            WSHttpContextBinding binding = new WSHttpContextBinding();
            factory = new ChannelFactory<WCFWFDemo.RestaurantContract.IOrderService>(binding, "http://localhost:8080/OrderService");
            proxy = factory.CreateChannel();

            // .NET FX 3.5 新機能の Durable Service を使ったコンテキスト情報の保持
            mgr = ((IClientChannel) proxy).GetProperty<IContextManager>();

        }

        private void button1_Click(object sender, EventArgs e)
        {
            proxy.BuyFoodByID(1);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            WCFWFDemo.RestaurantContract.Receipt receipt = proxy.GetReceipt();
            textBox1.Text = receipt.Price.ToString();
        }

        // [Save Context] ボタンを押して、コンテキスト情報をシリアライズ
        private void button3_Click(object sender, EventArgs e)
        {
            IDictionary<string, string> ctx = mgr.GetContext(); 
            SaveContext(ctx);
        }

        // [Load Context] ボタンを押して、コンテキスト情報をデシリアライズ
        private void button4_Click(object sender, EventArgs e)
        {
            IDictionary<string, string> ctx = LoadContext();
            if (ctx != null)
            {
                mgr.SetContext(ctx);
            }
        }

        static readonly string _saveName = "ContextInfo";

        static void SaveContext(IDictionary<string, string> context)
        {
            using (FileStream fs = new FileStream(_saveName, FileMode.Create, FileAccess.Write))
            {
                BinaryFormatter fb = new BinaryFormatter();
                fb.Serialize(fs, context);
                fs.Close();
            }
        }

        static IDictionary<string, string> LoadContext()
        {
            IDictionary<string, string> ctx = null;

            using (FileStream fs = new FileStream(_saveName, FileMode.Open, FileAccess.Read))
            {
                BinaryFormatter fb = new BinaryFormatter();
                ctx = fb.Deserialize(fs) as IDictionary<string, string>;
                fs.Close();
            }

            return (ctx);
        }

    }
}

デモでお見せしたとおり、途中まで処理をおこなって (例えば、Buy ボタンを2回押して)、Sava Context でセーブをおこなって終了し、再度起動して [Load Context] ボタンでロードしなおして処理をすると、続きから処理されます。

通常の WCF サービスの場合は、サービスサイドの実装に DurableServiceBehavior、DurableOperationBehavior の設定をおこなっておきます。さらに、[DurableOperationBehavior(CanCreateInstance=true)] といったように、どのオペレーションでインスタンス化し、どこで終了するかといった方針を埋め込みます。しかし、ワークフローサービスホストを使った実装では、既にこの振る舞いがデフォルトで設定されています。
また、ホスティングの際に指定した (上述)、WSHttpContextBinding や NetTcpContextBinding などを使うことで、コンテキストを使った上記の処理が可能となっています。

今回、[Send Activity] を使ったデモを実施していませんが、[Send Activity] は、逆に WCF サービスに対して要求を投げることができるアクティビティです。さらにこのアクティビティでは、上記のようにコンテキストを使って処理おこなうことができるようになっています。
ワークフロー間でこうしたアクティビティを使って連携することで、高度な会話型の処理を実装が容易に構築できるようになっています。

 

Published Monday, May 14, 2007 3:32 AM by tsmatsuz
Filed under: ,

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

No Comments

Leave a Comment

(required) 
required 
(required) 

  
Enter Code Here: Required
 
Page view tracker