SilverlightからのWCF呼び出しをReactive Extensionsとして取り扱うメソッドを作ってみた。

過去にSilverlightからWCFサービスを呼ぶ処理をRxに挑戦したことがありました。その記事から@neueccさんに反応もらって喜んだりしたのがほぼ一年前。ときが経つのは早いものです。

@neueccさんの記事では、サービス参照の追加で作ったプロキシクラスをRxに変換する方法が書かれているのですが、サービスメソッドごとに処理を記述しなければいけないのがちょっと面倒に思えます。そこで、今回はプロキシクラスの非同期メソッドをRx化する汎用メソッドを作ってみました。

Reactive Extensionsのインストー

Reactive Extensionsをダウンロードしてインストールしましょう。

今のところ以下のURLから安定版の最新を落とせるようです。

インストールが完了したらSilverlightプロジェクトに以下のアセンブリを追加しておいてください。

  • System.Reactive.dll

余談ですが、アセンブリ名とかクラス名とかがちょこちょこ変わっていてびっくりしました。

WCFサービスの作成

まずは簡単なWCFサービスを作成します。

[ServiceContract]
public interface IService1
{
    [OperationContract]
    string Hello(string name);
}

public class Service1 : IService1
{
    public string Hello(string name)
    {
        return name + "さん。こんにちは";
    }
}

ただ、名前を受け取り挨拶を返すだけです。

普通にWCFコール

普通にSilverlightから上記のWCFサービスをコールする場合、以下のようなコードになります。

var client = new Service1Client();
client.HelloCompleted += (s, e) =>
{
    System.Diagnostics.Debug.WriteLine(e.Result);
};
client.HelloAsync("Normal");

Rx化してWCFコール

今回作成するRx化メソッドを使用した場合こんな感じになります。

// ExeAsyncメソッドは
// IObservable<EventPattern<HelloCompletedEventArgs>>を返す。
ExeAsync<Service1Client, HelloCompletedEventArgs>(c => c.HelloAsync("Reactive Extensions"))
    .Subscribe(
    e =>
    {
        System.Diagnostics.Debug.WriteLine(e.EventArgs.Result);
    });

ExeAsyncメソッドの実行イメージはこんな感じです。

Rx化メソッド

で、肝心のRx化するメソッドはこんな実装になりました。

// メソッド名は保留中。良い名前が思いつかない。
private IObservable<EventPattern<TResult>> ExeAsync<TClient, TResult>(
    Expression<Action<TClient>> callService)
    where TClient : class, ICommunicationObject
    where TResult : AsyncCompletedEventArgs
{
    // MethodCallExpressionじゃないとだめ
    if (!(callService.Body is MethodCallExpression))
    {
        throw new ArgumentException("callMethodExpresson");
    }

    var client = (TClient)Activator.CreateInstance(typeof(TClient));

    // サフィックスのAsyncを外したサービスメソッド名を取得
    var methodCallEx = callService.Body as MethodCallExpression;
    string asyncMethodName = methodCallEx.Method.Name; // メソッド名は「○○Async」となっているはず
    int lastIndex = asyncMethodName.LastIndexOf("Async");
    string methodName = asyncMethodName.Substring(0, lastIndex);

    // Completedイベント名を取得
    string completedEventName = methodName + "Completed";

    // oはIObservable<EventPattern<TResult>>型
    var o = Observable.FromEventPattern<TResult>(client, completedEventName);

    // connectableはIConnectableObservable<EventPattern<TResult>>型
    var connectable
        = o.Take(1) // 実行は一回のみ(これしとかないとイベントハンドラの解除がされないし、そもそもPublishLastが動かなくなる)
           .PublishLast(); // AsyncSubjectに変換

    // detacherはIDisposable型
    var detacher = connectable.Connect(); // コネクト(イベント登録実行)

    // finalはIObservable<EventPattern<TResult>>型
    var final = connectable.Finally(() => detacher.Dispose());

    // callMethodはAction<TClient>型
    var callMethod = callService.Compile();
    callMethod(client); // サービスコールを実行

    return final;
}

基本@neueccさんの実装方法を参考にさせていただきました。このメソッドのポイントは式木を使ってWCFサービスのメソッド名を取得しているところでしょうか。
個人的には、「もうちょっと呼び出す側が簡潔になるとうれしいなぁ」と思っているのですが…。削れる情報がなさそうなので、これで良しとすることにしました。