かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

非同期ワークフローとメッセンジャーのコールバック

ちょっとした小ネタです。F#には非同期ワークフローという非同期処理を同期的に書けるという素敵な機能があります。
MSDN非同期ワークフローのページからコードを拝借しますが、以下のようなコードが書けます。

open System.Net
open Microsoft.FSharp.Control.WebExtensions

let urlList = [ "Microsoft.com", "http://www.microsoft.com/"
                "MSDN", "http://msdn.microsoft.com/"
                "Bing", "http://www.bing.com"
              ]

let fetchAsync(name, url:string) =
    async { 
        try
            let uri = new System.Uri(url)
            let webClient = new WebClient()
            let! html = webClient.AsyncDownloadString(uri)
            printfn "Read %d characters for %s" html.Length name
        with
            | ex -> printfn "%s" (ex.Message);
    }

let runAll() =
    urlList
    |> Seq.map fetchAsync
    |> Async.Parallel 
    |> Async.RunSynchronously
    |> ignore

runAll()

非同期にURLのデータを読み取って出力するという処理です。非同期の処理をしてるのに同期的な処理のコードと同じように書けるのが特徴です。素敵!!ということで、これを使うとMVVM的にどんなおいしいことがあるのかというと・・・ViewModelからViewを操作するためのMessengerパターンで何か使えそうです。

MVVMをサポートするライブラリの1つPrismではInteractionRequestのRaiseメソッドにコールバックを渡すことで、Viewの処理結果を受け取って続きの処理を行うことが出来ます。コールバックは、ネストが深くなればなるほど嫌な感じになるので、出来れば避けたいところです。因みに実際に画面を用意するのはめんどくさいのでコンソールアプリケーションで作ったコールバックバージョンのコードを以下に示します。

open System
open System.Windows
open Microsoft.Practices.Prism.Interactivity.InteractionRequest
open Microsoft.Practices.Prism.Interactivity

let r = InteractionRequest<Confirmation>()

// View側ではRaisedイベントを購読して処理をしているので、ちょっとエミュレート。
r.Raised.Add <| fun e ->
    // メッセージボックスを表示してOKが押されたらConfirmedにtrueを設定する。
    let result = MessageBox.Show(string e.Context.Content, e.Context.Title, MessageBoxButton.OKCancel)
    let confirmation = e.Context :?> Confirmation
    confirmation.Confirmed <- (result = MessageBoxResult.OK)
    e.Callback.Invoke()

// Raiseした後の続きの処理はコールバックで行う
r.Raise(
    Confirmation(Title = "メッセージ", Content = "お試し"),
    fun result -> 
        match result.Confirmed with
        | true -> printfn "OKが押されました"
        | false -> printfn "Cancelが押されました")

Console.ReadKey() |> ignore

このコードを実行するとダイアログが表示されOKを押すと「OKが押されました」Cancelを押すと「Cancelが押されました」と表示されます。コメントにもありますがRaiseメソッドを呼び出しているところでコールバックを指定しているので、ちょっと処理が読みにくくなっています。今回はコールバック内の処理は短いですが、ちょっと長くなったり、別メソッドに切り出したりすると、とたんに処理の流れを追うのが大変になります。

Raiseメソッドの非同期ワークフロー対応

ということで以下のようにInteractionRequestクラスを拡張して非同期ワークフロー対応にしてみました。

open System
open System.Windows
open Microsoft.Practices.Prism.Interactivity.InteractionRequest
open Microsoft.Practices.Prism.Interactivity

// InteractionRequest<T>のメソッドを拡張
type InteractionRequest<'a when 'a :> Notification> with
    /// 非同期ワークフロー対応のRaiseメソッド
    member x.RaiseAsync (n : 'a) =
        Async.FromContinuations (fun (cont, econt, ccont) ->
                // Raiseのコールバックでcont
                x.Raise(n, fun r -> cont(r))
            )

RaiseAsyncメソッドを定義して、FromContinuationsメソッドを使ってAsyncを返すようにしています。このようにしておくと、以下のように非同期ワークフロー内でRaiseメソッドが呼び出せます。

// 非同期ワークフローを使うとコールバックが消えてすっきり
async {
    let! result = r.RaiseAsync(Confirmation(Title = "メッセージ", Content = "お試し"))
    match result.Confirmed with
    | true -> printfn "OKが押されました"
    | false -> printfn "Cancelが押されました"
} |> Async.Start

コールバックを排除できたので、まるで一連の流の処理のようにコードを書けるようになりました。とても素敵ですね!

非同期ワークフロー対応版のコードの全体を以下に示します。

open System
open System.Windows
open Microsoft.Practices.Prism.Interactivity.InteractionRequest
open Microsoft.Practices.Prism.Interactivity

// InteractionRequest<T>のメソッドを拡張
type InteractionRequest<'a when 'a :> Notification> with
    /// 非同期ワークフロー対応のRaiseメソッド
    member x.RaiseAsync (n : 'a) =
        Async.FromContinuations (fun (cont, econt, ccont) ->
                // Raiseのコールバックでcont
                x.Raise(n, fun r -> cont(r))
            )

let r = InteractionRequest<Confirmation>()

// View側ではRaisedイベントを購読して処理をしているので、ちょっとエミュレート。
r.Raised.Add <| fun e ->
    // メッセージボックスを表示してOKが押されたらConfirmedにtrueを設定する。
    let result = MessageBox.Show(string e.Context.Content, e.Context.Title, MessageBoxButton.OKCancel)
    let confirmation = e.Context :?> Confirmation
    confirmation.Confirmed <- (result = MessageBoxResult.OK)
    e.Callback.Invoke()

// 非同期ワークフローを使うとコールバックが消えてすっきり
async {
    let! result = r.RaiseAsync(Confirmation(Title = "メッセージ", Content = "お試し"))
    match result.Confirmed with
    | true -> printfn "OKが押されました"
    | false -> printfn "Cancelが押されました"
} |> Async.Start

Console.ReadKey() |> ignore

これは、ViewModelをF#で書くことを真面目に考えてもいいかも?