読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Rx入門 (26) - .NETイベントへの変換

以前、Rx入門 (11) - イベントのシーケンス化で.NETネイティブなイベントをIObservable<T>に変換する方法について触れました。「.NETイベントをIObservable<T>に変換できるなら、その逆もできていいんじゃないか?」と思った方、正解です!今回はIObservable<T>を.NETイベントに変換する方法について説明します。

.NETイベントをIObservable<T>に変換したときは、FromEventメソッドもしくはFromEventPatternメソッドを利用しました。今回はその逆なので、メソッド名もToEventメソッドToEventPatternメソッドです。覚えやすいですね!

Action<T>型のイベントを発行する

ひとつめは、Action<T>型のイベントに変換する場合に利用するObservable.ToEventメソッドです。早速、簡単なサンプルを見てみましょう。今回もまたデジタル時計を作ってみました。

using System;
using System.Reactive;
using System.Reactive.Linq;
 
namespace Sample45_ToEvent
{
    class Clock
    {
        //--- OnNextのタイミングでAction<T>型のイベントを発行するインターフェース
        private readonly IEventSource<DateTime> source = null;
 
        public Clock()
        {
            this.source = Observable
                        .Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1))
                        .Select(_ => DateTime.Now)
                        .ToEvent(); //--- IEventSource<T>に変換
        }
 
        public event Action<DateTime> TimeChanged
        {
            add    { this.source.OnNext += value; } //--- OnNextイベントへの
            remove { this.source.OnNext -= value; } //--- 登録/解除をラップする
        }
    }
}

まず、コンストラクタObservable.Timerメソッドを利用して1秒おきに現在の時刻を発行しています。その最後にToEventメソッドIEventSource<T>に変換しているのが今回のポイントです。IEventSource<T>はAction<T>型のOnNextイベントを持っているだけのインターフェースで、値がOnNextメソッドで通知されたのに呼応してイベントを発行するという機能を持っています。つまり、IEventSource<T>がIObservable<T>と.NETイベントとの間を取り持ってくれるアダプターになっているということです。Clockクラスの外部には標準的な.NETイベントとして公開したいので、TimeChangedイベントとしてラップしています。

準備ができたので、これを利用してみます。

using System;
 
namespace Sample45_ToEvent
{
    class Program
    {
        static void Main()
        {
            //--- 内部でRxを使ってるなんて外からは分かりません!
            new Clock().TimeChanged += now =>
            {
                Console.Clear();
                Console.WriteLine(now.ToString());
            };
            Console.ReadLine(); //--- Enter押したらオシマイ
        }
    }
}

特に何という事もなくイベントハンドラを追加して時間を表示しているだけです。ポイントは、Clockクラスの外部から見た場合に内部でRxが利用されていることが隠蔽されていて見えないことです。内部ではガリガリ/ゴリゴリRxを使っていても、公開するインターフェースはイベントという形でシンプルにできます。上手く使いこなして美しいクラス設計をしたいですね!

EventHandler<T>型のイベントを発行する

もうひとつ、EventHandler<T>型のイベントを発行するObservable.ToEventPatternメソッドを紹介します。イベントの型が異なるだけで、基本的にはObservable.ToEventメソッドと同じです。

以下にサンプルを示します。今回の例題は、イベントが呼び出された回数を通知するという簡単なものです。まず、イベントデータ型を定義します。

using System;
 
namespace Sample46_ToEventPattern
{
    class RaisedCountEventArgs : EventArgs
    {
        public uint Count{ get; private set; }
        public RaisedCountEventArgs(uint count)
        {
            this.Count = count;
        }
    }
}

次に、イベントを発行するクラスを定義します。

using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
 
namespace Sample46_ToEventPattern
{
    class RaisedCounter
    {
        private uint count = 0;
        private readonly Subject<uint> subject = new Subject<uint>();
        private readonly IEventPatternSource<RaisedCountEventArgs> source = null;
 
        public RaisedCounter()
        {
            this.source = this.subject
                        .Select(count => new RaisedCountEventArgs(count))
                        .Select(e => new EventPattern<RaisedCountEventArgs>(this, e))
                        .ToEventPattern(); //--- IEventPatternSource<T>に変換
        }
 
        public event EventHandler<RaisedCountEventArgs> NotifyCount
        {
            add    { this.source.OnNext += value; } //--- OnNextイベントへの
            remove { this.source.OnNext -= value; } //--- 登録/解除をラップする
        }
 
        public void RaiseEvent()
        {
            //--- 発火!
            this.subject.OnNext(++this.count);
        }
    }
}

今回も前のサンプルと同様にコンストラクタToEventPatternメソッドを呼び出し、IObservable<T>と.NETイベントのアダプターであるIEventPatternSource<T>に変換しています。ToEventPatternメソッドに与えられる型はIObservable<EventPattern<TEventArgs>>です。EventPattern<T>は以前Rx入門 (11) - イベントのシーケンス化でもチラッと出てきましたが、イベントの送り主とイベントデータをまとめるためのクラスです。RaiseEventメソッドで発行回数をインクリメントしながら値を通知し、通知が行くとコンストラクタで定義した変換が行われ、最終的にはNotifyCountイベントが発行される、という流れになります。

最後に、これらを使って実行してみます。ちゃんとイベントが発行され、回数が通知されていることが確認できますね。

using System;
 
namespace Sample46_ToEventPattern
{
    class Program
    {
        static void Main()
        {
            var counter = new RaisedCounter();
            counter.NotifyCount += (sender, e) => Console.WriteLine("{0}回目", e.Count);
            counter.RaiseEvent();
            counter.RaiseEvent();
            counter.RaiseEvent();
        }
    }
}
 
//----- 結果
/*
1回目
2回目
3回目
*/

ここでは、イベントの型としてEventHandler<T>型を利用しました。場合によってはEventHandler型を利用したいケースがあるかもしれませんが、RxにはEventHandler型に直接変換するためのメソッドが用意されていません。もし必要であるならば、EventHandler<EventArgs>として対応してください。