2012年2月1日水曜日

アクティビティ拡張機能の利用

Workflow Foundation ではワークフローランタイムを通して各アクティビティに拡張機能を提供する仕組みが用意されています。少々使いどころが難しい面がありますが、アクティビティ本来の処理ではないロジックを、拡張機能という形で全体的に提供することが可能です。

小難しい事は抜きにしてサンプルソースから。

   1: Public Class SampleExtension
   2:  
   3:     Friend executeList As New List(Of String)
   4:  
   5:     ''' <summary>実行履歴を登録する</summary>
   6:     Public Sub AddLog(ByVal description As String)
   7:         executeList.Add(description)
   8:     End Sub
   9:  
  10: End Class

まずは拡張機能として上記のように「アクティビティ実行時に何かしらの文字列を登録する」クラスを用意します。Workflow Foundation としては System.Activities.Hosting 名前空間に IWorkflowInstanceExtension インターフェースが用意されており、これを継承した上で拡張機能を作るのが本筋なのかも知れませんが、強制ではありません。

そしてアクティビティ側では実行時等で拡張機能を取得し、処理を行う事ができます。

   1: Imports System.Activities
   2:  
   3: Public Class UseExtensionActivity
   4:     Inherits CodeActivity(Of Integer)
   5:  
   6:     Public Property AVal As InArgument(Of Integer)
   7:     Public Property BVal As InArgument(Of Integer)
   8:  
   9:     Protected Overrides Function Execute(context As CodeActivityContext) As Integer
  10:         '拡張機能の取得
  11:         Dim ext = context.GetExtension(Of SampleExtension)()
  12:         If ext IsNot Nothing Then
  13:             '拡張機能を認識できた場合
  14:             ext.AddLog(Me.DisplayName + " を実行しました。")
  15:         Else
  16:             '拡張機能を認識できなかった場合
  17:             Console.WriteLine("拡張機能を認識できませんでした。")
  18:             Return 0
  19:         End If
  20:  
  21:         Console.WriteLine("計算結果 : " + (AVal.Get(context) * BVal.Get(context)).ToString)
  22:         Return AVal.Get(context) * BVal.Get(context)
  23:     End Function
  24: End Class

11 行目が拡張機能を取得している部分です。ワークフローランタイムは実行時コンテキストを通して拡張機能を提供してきますので、型指定したうえで取得します。

   1: Imports System.Activities
   2: Imports System.Activities.Statements
   3: Imports System.Activities.Expressions
   4:  
   5: Module Module1
   6:  
   7:     Sub Main()
   8:  
   9:         Dim sampAct As New UseExtensionActivity With {.AVal = 10, .BVal = 5}
  10:         Dim wfInv As New WorkflowInvoker(sampAct)
  11:         '拡張機能無しで実行
  12:         Console.WriteLine("(1) 拡張機能無しで実行")
  13:         wfInv.Invoke()
  14:  
  15:         '拡張機能ありで実行
  16:         Dim ext As New SampleExtension
  17:         wfInv.Extensions.Add(ext)
  18:         Console.WriteLine("(2) 拡張機能ありで実行")
  19:         wfInv.Invoke()
  20:  
  21:         Console.WriteLine("拡張機能の結果を表示")
  22:         ext.executeList.ToList.ForEach(Sub(desc)
  23:                                            Console.WriteLine(desc)
  24:                                        End Sub)
  25:  
  26:         Console.ReadKey()
  27:  
  28:     End Sub
  29:  
  30: End Module

そしてワークフローを実行するロジックはこのような形になります。 WorkfloInvoker を利用しての実行ですが、WorkflowApplication の場合でも同名の Extensions プロパティが用意されていますので同じように拡張機能を登録し利用します。実行すると次のような結果になります。

Extension1

拡張機能を利用するもう一つの方法として、アクティビティ側で登録させる方法があります。

   1: Public Class UseExtensionActivity2
   2:     Inherits UseExtensionActivity
   3:  
   4:     Protected Overrides Sub CacheMetadata(metadata As CodeActivityMetadata)
   5:         MyBase.CacheMetadata(metadata)
   6:         metadata.AddDefaultExtensionProvider(Of SampleExtension)(Function() New SampleExtension)
   7:     End Sub
   8:  
   9: End Class

先程の UseExtensionActivity と同様の Execute メソッドを利用しますので継承した形にて実装します。そしてアクティビティ側から拡張機能を登録する場合は、CacheMetadata メソッドにて登録を行います。

このような形で実装し、次のようなロジックでワークフローを実行します。

   1: Dim sampAct2 As New UseExtensionActivity2 With {.AVal = 10, .BVal = 5}
   2: Dim wfInv2 As New WorkflowInvoker(sampAct2)
   3: Console.WriteLine("(3) アクティビティ側で拡張機能を設定")
   4: wfInv2.Invoke()

最初の例と異なり、こちらではそのまま WorkflowInvoker にてワークフローを実行している点に注意してください。実行結果は次のようになります。

Extension2

このような形で拡張機能を利用する事ができます。

アクティビティ側で拡張機能の存在を把握していなければならないので、使いどころが難しい面はありますが、例えば簡易的なトレース機能や、利用制限を行う等、そのような使い道が考えられます。

3 件のコメント:

  1. こんにちは。WF面白そうで取り掛かってる初心者なんですが、今一つ感触が掴めないでいます。

    最大のヤマは、ContinueAt待ちのアクティビティがパッシブ化されたときに、実際の処理がどうなるのか良くわからない点です。
    例えば、ソケット通信に時間がかかる事が想定されるような場合、BeginRecv()みたいに非同期で処理を開始して、EnqueItem待ちにしたとします。その状態でパッシブ化され、あまつさえランタイムが停止させられた場合、非同期のソケットのような資源はどうなってしまうんでしょう?(というより、ソリャまずいからごにょごにょすると思うんですが、そこがイマイチイメージ出来ない...)

    何かベストプラクティスみたいなものはありますか?

    返信削除
  2. コメントありがとうございます。

    ベストプラクティス、と言えるほど実例を重ねていないので私もあまり大したことは言えないのですが、「生存していないといけない資源」を利用しているようなケースのワークフローでしたら、その資源が不要になるまでアンロードはしないようにするのが良いと考えます。

    WF の永続化でアンロードを行った場合、完全にメモリ上から破棄し、その後の再開等の管理は WF エンジンと永続化ストレージによって行われます。このあたりは MS エバンジェリストの松崎さんの記事が詳しいと思います。
    http://blogs.msdn.com/b/tsmatsuz/archive/2010/02/17/wf-4-workflow-extensions-persistence-tracking.aspx

    できるだけ単発の処理で事が済むように構成するのが良いのかな、とも思います。
    あまり大した事が言えなくて申し訳ありません……

    返信削除
    返信
    1. 返事ありがとうございます。

      あれから色々調べていたんですが、WF4になって、AsyncCodeActivityが導入されたんですね。Ahfさんにも記事がありました。
      で、http://msdn.microsoft.com/ja-jp/library/ee358731.aspx に「AsyncCodeActivity はアクティビティの実行期間に対して常に非永続的なブロックを作成します」とあるので、なるほどと思いました。

      試していませんが、もし、WF3の場合であれば無理やり永続化しようとすると BinaryFormatter でシリアル化するので、インスタンスグラフの末端にソケットハンドルなどの物理的なハンドルがあってシリアル化出来なくて例外が発生するような気がします。
      WF4ではインスタンスストアなる新しい永続化機構に代わっているようなので、このあたりはまだよくわかっていません。

      # エッセンシャルWFから入って、しばらく寝かせていたので、WF4でいろいろ違うところがあってorz

      > できるだけ単発の処理で事が済むように構成するのが良いのかな、とも思います。

      そうですね、非同期(でしかもランタイムが変わってしまうぐらい長期)のパターンは、WFではない世界では力技で当たり前の回避方法を取るのが普通なので、WFのようにシーケンスが連続しているというのは新鮮すぎてどうしたらいいのかパターンが見えない所です。
      実際には長い永続化を考えているというよりも、Azureで使って耐障害性を上げたいというのが目標だったりします。

      またネタを投下するかもしれませんが宜しく (^^)/

      削除