2011年4月28日木曜日

Workflow サービスを Workflow 上で利用する

前回のように作成したワークフローサービスを、別のワークフロー上で利用する事も当然できます。動作原理は WCF によるサービスですので、当然コードのみでも利用できますし、VS 上で Web 参照を追加して利用する事もできます。ワークフロー上で利用する場合は、前回のサービス作成と逆のアクティビティを利用します。

UseWFService1

これが全体図です。前回は 受信→返却送信、という流れで作成しましたが、サービスを利用する側となると 送信→返却受信という流れになります。やはり 1 セットで設置できるアクティビティが用意されており、それが SendAndReceiveReply アクティビティとなります。デザイナ上にドロップすると、Send アクティビティと ReceiveReply アクティビティが設置されます。リクエストを送信するための Send アクティビティではサービスを呼び出すための設定が必要です。

  • EndpointAddress:サービスのアドレスをUriクラスのインスタンスとして設定
  • Content:サービスへ送信する値やパラメータを設定
  • ServiceContractName:サービス側で設定されている同名のプロパティと設定を合わせる

サービスからの結果を受信する ReceiveReply アクティビティでは Content プロパティに対して受け取る値やパラメータの設定だけが必要です。

このようにワークフローを設定し実行すると、前回作成した適当な値を返すワークフローサービスが動作し結果を返却してくれます。

UseWFService2

WCF で提供されるサービスを利用する事自体かなり簡略化されていますが、ワークフローから利用する場合はさらに簡単で、一切のコーディング作業が必要ないことがわかると思います。またサービス自体もコーディング全く無しで準備可能ですので、開発者でなくともサービスを作成・提供することが可能です。

特に現在は Windows Azure にて IIS を利用する事が可能となっています。Azure Platform を利用する事で更に WF を有効に活用できるのではないでしょうか。

IIS による Workflow サービス公開

WF4 を利用して外部にサービスとして公開する方法としては何通りかありますが、その中でも比較的簡単にサービス公開する方法である、IIS を利用した方法です。IIS を利用してワークフローをサービス公開するには大きく分けて二つの作業が必要になります。

  • ワークフローサービスとして公開するワークフローの作成
  • IIS の設定

ワークフローサービスとしてのワークフローですが、実際には今まで利用しているワークフローと殆ど違いがありません。IIS でホスティングを行う場合があるので、拡張子が xaml ではなく xamlx を利用しているだけで、内部に記述されている内容には殆ど違いがありません。唯一の違いはルートレベルに利用するアクティビティを WorkflowService アクティビティにするだけです。

service0

WorkflowService アクティビティにはプロパティが 3 つ公開されています。この部分でサービス名や config ファイル上の設定などを指定します。今回はサンプルという事で CodeRecipe で公開されているサンプルに近いものを作成します。サービスの処理内容としては、「適当な文字列を受け取って、適当に編集して、適当に返却する」というサンプルでしかありえない簡易な物とします。

サービスとして公開している場合、リクエストを受けるクチと返事を返すクチが必要となります。受け口は Receive アクティビティ、返却口は SendReply アクティビティとなります。これらは個別にデザイナ上にドロップしても構いませんが、大体は面倒なので 1 セットになっている ReceiveAndSendReply アクティビティを利用する事が多いと思います。また必要となる一部の設定を自動で行ってくれますので、できるだけ利用するのが楽だと思います。

service1

Receive アクティビティでは多くのプロパティが公開されているので少々戸惑いますが、今回のように簡易なサービスを利用する場合にはそれほど設定箇所はありません。

  • OperationName プロパティ:サービスのメソッド名
  • CanCreateInstance プロパティ:リクエスト受信時に新規インスタンスを生成するか
  • Content プロパティ:リクエストで受け付ける引数の設定

OperationName にメソッド名を定義し、CanCreateInstance にはチェックをつけておきます。そしてメソッドの引数を定義するために CorrelatesOn プロパティを設定します。プロパティダイアログにボタンが表示されているのでそれをクリックすると、コンテントエディタが表示されます。

service1-2

このエディタで受け取るリクエストの引数を設定します。メッセージとして設定する場合は単一の値、または WCF で利用される DataContract 属性を付けたクラスを指定します。

そして適当な処理の後では何かしらの値を返却するケースがあります。その場合に SendReply アクティビティを利用します。

service2

Receive アクティビティとプロパティは内容が近いです。こちらで必要なのは返却する内容を Content コレクションプロパティで設定します。

service2-2

今回は適当に編集した値を一つだけ返却しますのでこのように設定します。なおここでは変数を利用していますので、ワークフローの変数としても定義は行っています。

これでサービスとして提供するワークフロー自体は完成です。xamlx として保存します。もう一つ必要なファイルが IIS 上で動作する際の設定ファイルとなる web.config ですが、今回は WCF における既定の構成を利用しますので、以下の内容をそのまま利用します。

1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3:   <system.web>
4:     <compilation debug="true" targetFramework="4.0" />
5:   </system.web>
6:   <system.serviceModel>
7:     <behaviors>
8:       <serviceBehaviors>
9:         <behavior>
10:           <serviceMetadata httpGetEnabled="true"/>
11:           <serviceDebug includeExceptionDetailInFaults="false"/>
12:         </behavior>
13:       </serviceBehaviors>
14:     </behaviors>
15:   </system.serviceModel>
16:   <system.webServer>
17:     <modules runAllManagedModulesForAllRequests="true"/>
18:   </system.webServer>
19: </configuration>

この 2 つのファイルを IIS 上に用意した仮想フォルダにコピーするだけでワークフローサービスとしては動作します。動作しない場合、ファイアウォールで通信を許可しているか、IIS で利用するアプリケーションプールの設定が .NET4 用になっているか、等を確認してみてください。

今回は動作確認に、WCF テストクライアントを利用しました。

service3

このように通常と殆ど変りのないワークフローな xamlx ファイルを IIS に配置しただけでサービスとして利用できているのがわかると思います。外部公開するサービス、と言われると難しいイメージを覚えられることもあると思いますが、WF4 を利用した場合のサービス公開はこのように非常に簡単なものとなっています。

2011年4月27日水曜日

SQL Server のテーブルを全て CSV 出力するワークフロー

SQL Server でテーブルの内容を CSV 出力する事はそれなりに頻度があるかと思います。大体は BCP 等のユーティリティを利用して行うとか、それをスクリプトにまとめておくとか、そのような形ではないでしょうか。同じことをワークフローでも表現してみました。

TableToCsv1

今回は次のような流れのワークフローです。

  1. データベースからテーブル一覧を取得
  2. テーブルごとにデータを抽出
  3. 抽出したデータを CSV 出力

テーブル一覧を取得する部分は今回新規に用意したアクティビティですが、それ以外は今までの記事で利用したものとなっています。ロジックはこのような感じです。

1: Imports System.Activities
2: 
3: Public Class GetSQLTableListActivity
4:     Inherits AsyncCodeActivity
5: 
6:     Private Const SQL_GETTABLE = "SELECT * FROM SYS.TABLES"
7: 
8:     Property ConnectionString As InArgument(Of String)
9:     Property Condition As String = ""
10:     Property TableList As OutArgument(Of String())
11: 
12:     Public Sub New()
13:         Me.DisplayName = "SQL Server のテーブル一覧を取得"
14:     End Sub
15: 
16:     Private Delegate Function GetSQLTableListDelegate(ByVal conString As String, ByVal conditionStrings As String) As String()
17: 
18:     Protected Overrides Function BeginExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal callback As System.AsyncCallback, ByVal state As Object) As System.IAsyncResult
19:         Dim conStr = context.GetValue(Me.ConnectionString)
20:         Dim asyncGetSQLTableListDel = New GetSQLTableListDelegate(AddressOf GetSQLTableList)
21:         context.UserState = asyncGetSQLTableListDel
22: 
23:         Return asyncGetSQLTableListDel.BeginInvoke(conStr, Me.Condition, callback, state)
24:     End Function
25: 
26:     Protected Overrides Sub EndExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal result As System.IAsyncResult)
27:         Dim asyncGetSQLTableListDel = TryCast(context.UserState, GetSQLTableListDelegate)
28:         Dim resultStrings = asyncGetSQLTableListDel.EndInvoke(result)
29:         TableList.Set(context, resultStrings)
30:     End Sub
31: 
32:     Private Function GetSQLTableList(ByVal conString As String, ByVal conditionStrings As String) As String()
33:         Dim result As String()
34: 
35:         Dim tabList As New DataSet
36:         Try
37:             Using sqlConn As New SqlClient.SqlConnection(conString)
38:                 sqlConn.Open()
39:                 'テーブル一覧の取得
40:                 Dim execSql As String = SQL_GETTABLE
41:                 If conditionStrings.Trim <> "" Then
42:                     execSql += " WHERE name like '%" + conditionStrings + "%'"
43:                 End If
44:                 execSql += " ORDER BY name "
45: 
46:                 Dim dataAdapter As New SqlClient.SqlDataAdapter
47:                 dataAdapter.SelectCommand = New SqlClient.SqlCommand(execSql, sqlConn)
48:                 dataAdapter.MissingSchemaAction = MissingSchemaAction.AddWithKey
49:                 dataAdapter.Fill(tabList)
50:                 sqlConn.Close()
51:             End Using
52:         Catch ex As Exception
53: 
54:         End Try
55:         '結果を String() に出力
56:         If tabList.Tables.Count > 0 Then
57:             Dim resultList As New List(Of String)
58:             For Each childRow In tabList.Tables(0).Rows
59:                 resultList.Add(childRow.item("name").ToString)
60:             Next
61:             result = resultList.ToArray
62:         Else
63:             result = Nothing
64:         End If
65: 
66:         Return result
67:     End Function
68: 
69: End Class

SQL Server  では SELECT * FROM SYS.TABLES とすることでテーブル一覧が取得できますので、その結果を今回は String の配列として返却するようにしています。

そうして得たテーブル一覧に対して ForEach アクティビティでループ、その内部では取得したテーブル名を元にデータを取得、その結果を利用して ForEach アクティビティで更にループ、以前利用した CSVOutput なアクティビティを利用して1行ずつファイルへと出力しています。

このようにワークフローとして処理を記述すると、スクリプトやツールの知識がなくとも行われている処理内容が非常にイメージしやすくなります。このあたりが Workflow の醍醐味なのではないか、と個人的には思っています。

Bookmark を利用したワークフローとの連携

Workflow では実行されるワークフローと、ホストするアプリケーション等外部アプリと連携するための仕組みとしてブックマークがあります。ただし連携する方向は、ホストアプリケーション→ワークフロー、という一方のみです。CodeRecipe を参考に、動作サンプルとしてブックマークを作成しホストアプリケーションから値を受け取るアクティビティを作成します。

1: Imports System.Activities
2: 
3: Public Class CooperationHostActivity
4:     Inherits NativeActivity
5: 
6:     Public Property BookmarkName As String
7:     Public Property HostedValue As OutArgument(Of String)
8: 
9:     Protected Overrides ReadOnly Property CanInduceIdle As Boolean
10:         Get
11:             Return True
12:         End Get
13:     End Property
14: 
15:     Protected Overloads Overrides Sub Execute(ByVal context As System.Activities.NativeActivityContext)
16:         If Me.BookmarkName.Trim = "" Then Return
17:         context.CreateBookmark(Me.BookmarkName, New BookmarkCallback(AddressOf OnResume))
18:     End Sub
19: 
20:     Private Sub OnResume(ByVal context As NativeActivityContext, ByVal bookmark As Bookmark, ByVal value As Object)
21:         context.SetValue(Me.HostedValue, value)
22:     End Sub
23: 
24: End Class

継承元が NativeActivity となっているのを除けば、今までと大差ない内容です。ブックマークを Execute メソッドにて作成していますが、この際ブックマークが呼び出された際のメソッドをコールバックとして登録しておく必要があります。このアクティビティを利用して次のようなワークフローを作成します。

Bookmark0

ブックマークを作成しホスト側より値を受け取り、その内容を表示する単純なワークフローです。今回は Writeline アクティビティを利用したため、ホストアプリケーションはコンソールアプリケーションとして用意しました。

1: Imports System.Threading
2: Imports System.Activities
3: Imports System.Activities.XamlIntegration
4: 
5: Module Module1
6: 
7:     Sub Main()
8:         Dim resetEvent As New AutoResetEvent(False)
9:         Dim wfFile = ActivityXamlServices.Load("sample.xaml")
10:         Dim _wfApps As New System.Activities.WorkflowApplication(wfFile)
11:         'ワークフロー実行完了時に待機状態を終了する
12:         _wfApps.Completed = Sub(e As WorkflowApplicationCompletedEventArgs)
13:                                 resetEvent.Set()
14:                             End Sub
15:         _wfApps.Run()
16: 
17:         Console.WriteLine("何か値を入力してください")
18:         _wfApps.ResumeBookmark("TestBookmark", Console.ReadLine())
19: 
20:         resetEvent.WaitOne()
21:         Console.ReadLine()
22: 
23:     End Sub
24: 
25: End Module

ホスト側アプリケーションはこのような形で用意しました。先に用意したワークフローを呼び出し実行、コンソールで適当な値を入力させ、その値をブックマークに対して送り出しています。今回は文字列にて呼び出すブックマーク名を設定していますが、ここは Bookmark クラスのインスタンスを設定する事も可能です。

image

実行するとこのようになります。最終行の文言はワークフロー側から表示が行われていますが、そこまでの 2 行はホスト側アプリケーションによる表示です。このようにブックマークを利用する事で、ワークフローとアプリケーションとの間で連携を取ることができるようになります。ワークフローエンジン上で重複禁止なので、ブックマークの名前付けには気を付ける必要がありますが・・・

2011年4月26日火曜日

BingAPI を利用した翻訳アクティビティ

ふらふらと Code Recipe をさまよっていると見慣れないサンプルがあったので、それをそのままアクティビティ化してみました。本当にそのままですw

1: Imports System.Activities
2: Imports System.Net
3: Imports System.Text
4: 
5: Public Class BingTranslaterActivity
6:     Inherits AsyncCodeActivity
7: 
8:     Public Property OriginalStrings As InArgument(Of String)
9:     Public Property EncodeFrom As String = "ja"
10:     Public Property EncodeTo As String = "en"
11: 
12:     Public Property TranslatedStrings As OutArgument(Of String)
13: 
14:     Private Delegate Function translateUseBingDelegate(ByVal original As String) As String
15: 
16:     Public Sub New()
17:         Me.DisplayName = "Bing で翻訳"
18:     End Sub
19: 
20:     Protected Overrides Function BeginExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal callback As System.AsyncCallback, ByVal state As Object) As System.IAsyncResult
21:         Dim inputStrings = context.GetValue(Me.OriginalStrings)
22:         Dim asyncTranslateDel = New translateUseBingDelegate(AddressOf TranslateUseBing)
23:         context.UserState = asyncTranslateDel
24: 
25:         Return asyncTranslateDel.BeginInvoke(inputStrings, callback, state)
26:     End Function
27: 
28:     Protected Overrides Sub EndExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal result As System.IAsyncResult)
29:         Dim asyncTranslateDel = TryCast(context.UserState, translateUseBingDelegate)
30:         Dim resultStrings = asyncTranslateDel.EndInvoke(result)
31:         TranslatedStrings.Set(context, resultStrings)
32:     End Sub
33: 
34:     Private Function TranslateUseBing(ByVal original As String) As String
35:         Dim bingApiKey = "取得したBingAPIキー"
36:         Dim result = ""
37:         Dim escStrings = Uri.EscapeDataString(original)
38: 
39:         Using webCl As New WebClient
40:             webCl.Encoding = Encoding.UTF8
41:             Dim bingAddress As String = "http://api.microsofttranslator.com/v2/Http.svc/Translate?appId={0}&text={1}&from={2}&to={3}"
42:             Dim callAddress As String = String.Format(bingAddress, bingApiKey, escStrings, Me.EncodeFrom, Me.EncodeTo)
43: 
44:             Try
45:                 Dim resultBody = webCl.DownloadString(callAddress)
46:                 result = XDocument.Parse(resultBody).Elements.First.Value
47:             Catch ex As Exception
48: 
49:             End Try
50:         End Using
51: 
52:         Return result
53:     End Function
54: 
55: End Class

以前に書いた非同期での Web 通信方法をまったくそのまま利用し、このような形で BingAPI を呼び出します。

Translate1

このような形でワークフローにアクティビティを配置、プロパティを設定し・・・

Translate2

実行するとこのように翻訳結果が得られます。ワークフローにおけるアクティビティは別にそれほど難しいことをやるものを用意する必要はなく、今回のように一つ一つは簡単な処理だけども、組み合わせて利用する事で色々面白いことができるのがキモだと思っています。ただここでどの程度の粒度でアクティビティにするか、そこが非常にセンスの問われるところだと。

永続化における Persist と Unload の違い

以前の記事で永続化する際には WorkflowApplication.PersistableIdle イベントで None 以外を返却するようにコーディングする必要がある事を書きました。None 外に指定できるのは、Persist と Uload の二つがありますが、この二つはその後の挙動に大きな違いが現れます。

Persist を指定した場合は永続化ストアに永続化されますが、ワークフローのインスタンスはそのまま残ります。Unload を指定した場合は、永続化されなおかつワークフローのインスタンスもアンロードされます。そのため単純に Unload を行った場合、一時停止状態で保存されているような扱いとなりワークフロー自体の実行も一時停止されます。Persist の場合は動作は継続されます。

Unload にてインスタンスごとアンロードしたワークフローを再開するには、WorkflowApplication.Load メソッドを用いて永続化ストアより読み込む必要があるのと、永続化時にブックマークを作成しておく必要があります。また再度読み込む際にワークフロー固有の ID 値( GUID )が必要ですので、実行時のワークフローの ID を WorkflowApplication.Id プロパティより保持しておく必要があります。

ワークフロー再開時のロジックは次のような形になります。

1: 'ワークフローの実行
2: _wfApps.Run()
3: _executionWorkflowId = _wfApps.Id 'ID の保存
4: '※実行するワークフローのアクティビティにてブックマークを作成しておく
5: 
6: 'ワークフローの再読込
7: _wfApps.Load(_executionWorkflowId)
8: 'ブックマークから再開
9: _wrApps.ResumeBookmark([ブックマーク],[アクティビティに渡す値])

このようなロジックにする事で、永続化に Unload を指定した場合の処理を再開する事が出来ます。ただし実際に利用する場合は、何かしらのアクションを受けて初めてワークフローを再開する事になると思いますが、その部分は自前で用意する必要があります。

2011年4月25日月曜日

SQL Server で永続化を行った際のデータ

前回の記事で SqlWorkflowInstanceStore クラスを利用し SQL Server 上に永続化を行う方法を扱いました。その際にどのようにデータが記録されているのかは次のようになります。

永続化1

テスト用にこのようなワークフローを用意しました。最初に適当な値を入力させ、Delay アクティビティで少し待機、その後値を表示させるというサンプル以外の何物でもないワークフローです。まず、値を入力させる段階ではアイドル状態にさせるようにはコーディングしていませんので、入力せずに放置していても何も値は記録されません。何かしらの値を入力し、Delayアクティビティに遷移した際に初めて SQL Server に記録されます。

永続化2

ワークフローがアイドル状態になった際に、上記のような結果となります。InstanceTable と LockOwnersTable にデータが記録されているのがわかると思います。InstanceTable には現在実行中のワークフローの情報が、LockOwnersTable にはワークフローの実行者(実行している環境)の情報が記録され、あるワークフローがどこで実行されているかを把握する事ができます。

永続化3

Delay アクティビティが終了し値の表示が終わり、ワークフロー自体が終了した場合には次のようになります。InstanceTable から値が削除され実行中ワークフロー情報がなくなります。もう一つの LockOwnersTable には情報が残りますが、これは次に何かしらのワークフローを実行した際にも利用されるデータとなっています。毎回データを新規生成する無駄が省かれている形です。

このような形でワークフローの永続化時にデータの保持が行われます。前回の記事で書いた事以外にはコード上での対応はありません。いかに簡単に永続化が行う事ができるかがわかると思います。

2011年4月23日土曜日

永続化を行う理由と Persist アクティビティ

Workflow Foundation における永続化は記憶領域にワークフローの状態を(もしくはインスタンス自体も)保存することです。そもそも何故永続化が必要になるか、というところではワークフローが通常のアプリケーションと異なり、実行状態となる時間が長くなる可能性が高いところにあります。

実行中の状態が継続している場合、考えなくてはならない点に「実行環境の障害対策」が出てきます。例えば実行中にハードウェアな障害が発生したので、その際に実行していたワークフローは全てリセットされました、では非常に困るわけです。それを回避するためにも意識的に永続化を行い、ある時点の状態にまで復元できるようにする必要があります。

ワークフローを永続化するためには条件があり、「ワークフローがアイドル(待機)状態であること」というのを適えている事が必要です。このアイドル状態というのは Delay アクティビティで明示的な待機状態にしている場合や、カスタマイズしたアクティビティ内部で「現在は待機状態です」というステータスを返却した場合などがあります。またワークフローを実行する際には何通りか方法があることは前述しましたが、WorkflowInvoker を利用している場合は永続化が行えません。

それらを踏まえた上で永続化に対応するには次の点に対応する必要があります。

  • インスタンスストアの指定

WorkflowApplication 等で永続化を行うには WorkflowApplication.InstanceStore プロパティに永続化を行うためのクラスを指定する必要があります。SQL Server で永続化を行う場合、SqlWorkflowInstanceStore クラスがあらかじめ用意されているのでこれを利用します。

1: Dim persistInstance As DurableInstancing.SqlWorkflowInstanceStore = Nothing
2: Dim conBuild As New System.Data.SqlClient.SqlConnectionStringBuilder
3: conBuild.DataSource = "127.0.0.1\SQLEXPRESS"
4: conBuild.InitialCatalog = "WF4"
5: conBuild.IntegratedSecurity = True
6: persistInstance = New DurableInstancing.SqlWorkflowInstanceStore(conBuild.ToString)
7: 

大体このような感じで利用できます。

  • WorkflowApplication.PersistableIdle イベントの対応

PersistableIdle イベントは WorkflowApplication にて実行中のワークフローで永続化可能なアイドル状態が発生した際に通知されるイベントです。このイベントが発生した際に、永続化を行うステータスを返却します。

1: Private Function PersistableIdle(ByVal e As WorkflowApplicationIdleEventArgs) As PersistableIdleAction
2:     Return PersistableIdleAction.Persist
3: End Function

ここで返却できる値は Persist の他に None (永続化しない)、Unload (インスタンスも永続化する) があります。それらの挙動についてはまた別の機会で書こうと思います。このメソッドを WorkflowApplication.PersistableIdle プロパティに設定します。

  • ワークフローでアイドル状態を発生させる

先程の Delay アクティビティとかもそうですが、もう一つ明示的に永続化を指定するために Persist アクティビティがあります。

Persist

見ていただくとわかるのですが、ワークフロー上に配置するだけという簡素なアクティビティです。制御がこのアクティビティに来たタイミングで PersistableIdle イベントが発生し、そこからの戻り値により永続化が WorkflowApplication.InstanceStore プロパティに設定されているクラスに指示されます。

永続化を利用するには、これだけの処理で可能となります。

2011年4月22日金曜日

SQL Server での Workflow 4 永続化サービス利用の準備

長い時間動作する Workflow には永続化する仕組みが最初から用意されています。またカスタマイズも行えるようになっていますので、ローカルファイルへ~とか行うのも可能です。.NET Framework  4 でも標準で SQL Server にて永続化を行うための仕組みが提供されているので、下準備の方法を書いておきます。

  1. 永続化用のデータベースを作成する
  2. スキーマ定義 SQL を実行する
  3. ロジック定義 SQL を実行する

データベースを作成して提供されている SQL を実行するだけの簡単なお仕事です。

実行するSQLは次のフォルダにあります。
[ Windowsフォルダ ] \ Microsoft.NET \ [ Framework または Framework64 ] \ [ バージョン番号 ] \ SQL \ ja

  • スキーマ定義用:SqlWorkflowInstanceStoreSchema.sql
  • ロジック定義用:SqlWorkflowInstanceStoreLogic.sql

MSDN では機械翻訳のままのせいなのか SQL \ en フォルダの~と書かれていますが ja フォルダのものを利用して大丈夫です。実行すると 8 個のテーブル、3 個のビュー、19 個のプロシージャ、他にも色々作成されます。

これだけで SQL Server を利用した永続化の下準備は完了です。なお、先日の Platform Update 1 より永続化対象に SQL Azure も加わりました。環境が用意できればここも試してみたいと思います。

Twitter から最新ツイートを取得するアクティビティ

非常に簡単ですが Web からデータを取得するアクティビティのサンプルです。この類は原則非同期アクティビティ ( AsyncCodeActivity とか) で実装する必要があるのを除けば、本当に単純なロジックとなっています。

1: Imports System.Activities
2: Imports System.Net
3: Imports System.Text
4: Imports System.IO
5: Imports System.Xml
6: 
7: Public Class GetTwitterActivity
8:     Inherits AsyncCodeActivity
9: 
10:     Public Property UserID As InArgument(Of String)
11:     Public Property Results As OutArgument(Of String)
12: 
13:     Private Delegate Function asyncGetTweetDelegate(ByVal userID As String) As String
14: 
15:     Protected Overrides Function BeginExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal callback As System.AsyncCallback, ByVal state As Object) As System.IAsyncResult
16:         Dim targetUser = context.GetValue(Me.UserID)
17:         Dim asyncGetTweetDelegate = New asyncGetTweetDelegate(AddressOf GetTwitterTweetInPrivate)
18:         context.UserState = asyncGetTweetDelegate
19: 
20:         Return asyncGetTweetDelegate.BeginInvoke(targetUser, callback, state)
21:     End Function
22: 
23:     Protected Overrides Sub EndExecute(ByVal context As System.Activities.AsyncCodeActivityContext, ByVal result As System.IAsyncResult)
24:         Dim asyncGetTweetDelegate = TryCast(context.UserState, asyncGetTweetDelegate)
25:         Dim resultStrings = asyncGetTweetDelegate.EndInvoke(result)
26:         Dim returnValue = ""
27:         Try
28:             'LINQ to XML で発言のみを抽出
29:             Dim resultXDoc = XDocument.Parse(resultStrings)
30:             Dim resGetTweet = From x In resultXDoc.Descendants
31:                               Where x.Name = "text"
32:                               Select x.Value
33: 
34:             If (resGetTweet IsNot Nothing) AndAlso
35:                 (Not String.IsNullOrEmpty(resGetTweet.First)) Then
36:                 returnValue = resGetTweet.First
37:             End If
38:         Catch ex As Exception
39:         End Try
40: 
41:         context.SetValue(Me.Results, returnValue)
42:     End Sub
43: 
44:     Private Function GetTwitterTweetInPrivate(ByVal userID As String) As String
45:         Dim result = ""
46:         Try
47:             Using webCl As New WebClient
48:                 '最新の発言のみを取得
49:                 Dim targetSite As String = "http://twitter.com/statuses/user_timeline/" + userID + ".xml?count=1"
50:                 Using resStream = webCl.OpenRead(targetSite)
51:                     Dim enc = Encoding.GetEncoding("UTF-8")
52:                     Using xmlStream = New StreamReader(resStream, enc)
53:                         result = xmlStream.ReadToEnd
54:                     End Using
55:                 End Using
56:             End Using
57:         Catch ex As Exception
58:         End Try
59: 
60:         Return result
61:     End Function
62: 
63: End Class

このような形で Twitter から UserID プロパティで指定したユーザーの発言直近 1 件を取得し、その発言内容だけを Result プロパティとして返却しています。

これをベースにしてもらえば web からのデータ取得はある程度対応できると思います。実際には特殊ケースがある(対象サイトが一定時間で JavaScript で遷移させるような挙動を用意している場合とか)のですが、そのような場合を除いてこのような形でさくっと拾って構わないと思います。

ちなみに上記レアケースでは WebBrowser コントロールを利用するしか今のところ対処はないんじゃないかな、と思っています・・・

2011年4月21日木曜日

StateMachine アクティビティでの設定

StateMachine1

StateMachine アクティビティを利用する場合には最低限必要な構成があります。

  • 一つの InitializeState アクティビティ
  • 一つの FinalState アクティビティ
  • 一つ以上の State アクティビティ

実際にデザイナ上にドロップした際には InitializeState アクティビティが自動で配置されます。FinalState アクティビティや一つ以上の State アクティビティは実際のところ無くてもいいのですが、それでは StateMachine アクティビティの意味がないというかw

これらの中で設定が必要になる箇所も複数あります。

StateMachine2

まずは各 State アクティビティを結ぶ Transition アクティビティ。プロパティダイアログ上では Condition プロパティと DisplayName プロパティしか表示されていません。Condition プロパティはこの遷移を行うための条件、DisplayName プロパティはデザイナ上で表示する文字を設定します。しかし本当に必要なのはここではなく、Transition アクティビティをダブルクリックした際に表示される詳細内容の方となります。

StateMachine5

ダブルクリックするとこのような表示に切り替わります。アクティビティデザイナ上には Trigger、Condition、Action の 3 つが用意されています。この中で Condition については先程プロパティダイアログで設定するものと同一です。

Action には Condition を満たした場合に実行する処理を、Trigger にはこの遷移を行う際に実施する処理内容を設定します。しかしここに注意点があります。State から複数の Transition を引き動作の確認を行ってもらうとわかるのですが、Trigger に設定した内容は「非同期で必ず実行される」挙動になります。

動作確認として次のようなサンプル StateMachine ワークフローを用意して見ました。

StateTrigger1

これがワークフローの全体図です。最初の State アクティビティで「 2 回ループしたら終わる」的な処理を行おうとしたものです。

StateTrigger2

State アクティビティの内部ではこのように単純にコンソールへログを出力するだけとしました。

StateTrigger3

これがループさせる側の Transition アクティビティの設定です。Condition プロパティではループ回数を表す変数が2未満、と設定しています。Action 部分でカウンタのインクリメントを行い、Trigger 部ではログを出力した後に少し Delay アクティビティで待たせることを設定しました。

StateTrigger4

もう片方の FainlState アクティビティへと接続された Transition アクティビティではこのように設定を行います。Condition プロパティは空欄とし、Action も Trigger もログを出力するだけとします。この状態のStateMachineワークフローを実行すると次のように出力されました。

StateMachineExecResult

このように Transition アクティビティの Trigger 部に設定した処理は Condition プロパティの内容に関わらず「必ず」実行されます。2度ほど繰り返した後に、FinalState アクティビティに接続されている側の Transition アクティビティの Action 部に設定された処理が実行されています。その直前で ループ側 Transition アクティビティの Trigger が実行されているのは謎ですが、恐らく正式版では解消されているものと思います。

Condition プロパティが満たされた場合に Action 部に設定された処理が実行される、のですがこれも先程の結果を見ていただけるとわかる通り、もう少し追加条件があります。
ある State アクティビティから発生している各 Trasition プロパティの Condition をチェックし、全て False であった場合に Condition 未設定の Transition アクティビティが呼び出される
わかりにくいと思いますが、大体このような挙動です。

StateMachine 関連アクティビティ

先日 .NET Framework 4 Platform Update 1 がリリースされ、今まで CodePlex で公開されていた Statemachine アクティビティ関係が正式に採用されました。同時に UnitTesting 関係のライブラリも採用されていたりと、Workflow 使いにはなかなか嬉しい更新です。

・・・が、自分の環境ではそれらのパッチが適用されてくれないので実物には触れてません。

調べているとどうやら CodePlex のものをベースに名前空間が変わった点と、機能強化されている点が追加されているとのことですので、今までの CTP 版を触っておくことも無意味ではないと信じたいのもあり、少しばかり触ってみました。

まずCTP版をリホスティング環境で利用するには少々事前準備が必要です。関係するdllの参照設定はいいとして、メタデータの更新が必要になります。

  1: Dim smReg As New Microsoft.Activities.Statements.Design.DesignerMetadata
  2: smReg.Register()


ロジックとしては上記の通りになります。そしてToolBoxItemとして登録するクラスも少々注意が必要です。




  • .Microsoft.Activities.Statements.Design.ToolboxItems.StateMachineWithInitialStateFactory:StateMachine アクティビティ


  • Microsoft.Activities.Statements.State:State アクティビティ


  • Microsoft.Activities.Statements.Design.ToolboxItems.FinalState:FinalState アクティビティ



StateMachine アクティビティと FinalState アクティビティのみ特殊になります。



大雑把にいうと、StateMachine アクティビティは本体、State アクティビティはその内部で状態毎に用意するアクティビティ、FinalState アクティビティは終了点です。そしてツールボックス上には用意できませんが、Transition アクティビティというものがあります。これは State アクティビティより別アクティビティに対して引かれる線になります。それぞれの設定については次回以降の記事に書きますが、利用シーンとしてこのような形が考えられる、というサンプルを貼っておきます。



StateMachineSample



状態とその遷移を上手くまとめられると、非常に使い出のある Workflow になると感じています。

2011年4月20日水曜日

FlowDecision と FlowSwitch アクティビティ

前述したように FLowchart アクティビティの子としてのみ利用できる特殊なアクティビティとして、FlowDecision と FlowSwitch の二つが用意されています。どちらも条件分岐を行うのですが、FlowDecision アクティビティが「式の結果による二分岐」なのに対して、FlowSwitch アクティビティは「対象に対しての複数分岐」と微妙に設定内容が異なります。ただこれも、プログラミングを行われている方であればイメージしやすく、FlowDecision アクティビティは IF 文、FlowSwitch アクティビティは Switch ( VB であれば Select Case ) 文による多分岐となります。

FlowDecision

FlowDecision アクティビティでは Condition プロパティに判断を行う「式」を設定します。この式は結果が True か False になる必要があります。サンプル例では [ 変数 var1 が “9” かどうか ] という判断を行い、True であれば向かって左側に、False であれば向かって右側に制御が遷移していきます。なお FlowDecision アクティビティから出ている線上に「True」「False」とありますが、ここの表示される文言は TrueLabel、FalseLabel プロパティで設定できます。

FlowSwitch

FlowSwitch アクティビティは対象となる変数等を Expression プロパティに設定します。サンプルでは var2 という変数を設定しています。これは直前のアクティビティで適当に値を入力させた結果を設定しています。

FlowSwitch2

そして FlowSwitch アクティビティから分岐するアクティビティへと線を伸ばして結合させます。その「線」に対して条件を設定する形になります。Case プロパティでは FlowSwitch の Expression プロパティに対しての値を設定し、その条件が満たされた場合に接続先のアクティビティが実行されます。IsDefaultCase プロパティは「他条件が満たされない場合」に遷移する線かどうか、を設定します。これは FlowSwitch アクティビティ1つに対して1本だけ存在できます。Select Case 文で言えば Case Else、Switch 文で言えばそのまま Default になります。

ちなみに Select Case True、として Case A = B などという設定についてですが、今のところはできません。カスタマイズが必要になると思いますが、ワークフローを使うのであれば、そういった微妙な技は使わない方がいいと思います・・・。