C# eventをIObservableに変換

eventからIObservableへの変換は、下記のRxのFromEvent関数にて行えます。
NuGet Gallery | Reactive Extensions (Rx) - Main Library 3.1.1

今回は、Rxを導入するほどでもないとき用に、シンプルなものを自前で実装してみたいと思います。

Observer、Observable、Disposable

用途に応じて継承クラスを作成するのは面倒なので、ラムダで作成できるクラスを作成します。

eventではOnCompletedとOnErrorを利用することがないので、実装していません。

ついでにラムダでSubscribe出来る拡張メソッドも定義しておきます。

public class Observer<T> : IObserver<T>
{
    Action<T> _onNext;
    public Observer(Action<T> onNext) => _onNext = onNext;
    public void OnNext(T value) => _onNext(value);
    public void OnCompleted() => throw new NotImplementedException();
    public void OnError(Exception error) => throw new NotImplementedException();
}
public class Observable<T> : IObservable<T>
{
    Func<IObserver<T>, IDisposable> _subscribe;
    public Observable(Func<IObserver<T>, IDisposable> subscribe) => _subscribe = subscribe;
    public IDisposable Subscribe(IObserver<T> observer) => _subscribe(observer);
}
public class Disposable : IDisposable
{
    Action _dispose;
    public Disposable(Action dispose) => _dispose = dispose;
    public void Dispose() => _dispose();
}
public static class EventExtensions
{
    public static IDisposable Subscribe<T>(this IObservable<T> observable, Action<T> f) =>
        observable.Subscribe(new Observer<T>(f));
}


MouseLeftButtonDownを実装してみる

FrameworkElementの拡張メソッドとして定義しています。

Subscribe時にhandlerを追加し、Dispose時にhandlerを解除するObservableを作成しています。

public static IObservable<MouseButtonEventArgs> MouseLeftButtonDownObs(this FrameworkElement element) =>
    new Observable<T>(observer =>
    {
        MouseButtonEventHandler handler = (sender, e) => observer.OnNext(e);
        element.MouseLeftButtonDown += handler;
        return new Disposable(() => element.MouseLeftButtonDown -= handler);
    });


FromEvent

少し汎用化して、オブジェクトとイベント名からIObservableを作成できるようにしてみます。

命名の仕方が固定であるなら、CallerMemberNameを利用してもいいと思います。

public static IObservable<T> FromEvent<T>(object obj, string name) =>
    new Observable<T>(observer =>
    {
        var ev = obj.GetType().GetEvent(name);
        Action<object, T> action = (sender, e) => observer.OnNext(e);
        Delegate handler = Delegate.CreateDelegate(ev.EventHandlerType, action.Target, action.Method);
        ev.AddEventHandler(obj, handler);
        return new Disposable(() => ev.RemoveEventHandler(obj, handler));
    });
public static IObservable<MouseButtonEventArgs> MouseLeftButtonDownObs(this FrameworkElement element) =>
    FromEvent<MouseButtonEventArgs>(element, nameof(element.MouseLeftButtonDown));
public static IObservable<MouseButtonEventArgs> MouseLeftButtonUpObs(this FrameworkElement element) =>
    FromEvent<MouseButtonEventArgs>(element, nameof(element.MouseLeftButtonUp));


オペレータ

ついでにSelect、Where、Mergeオペレータも作成してみます。

public static IObservable<TDst> Select<TSrc, TDst>(this IObservable<TSrc> observable, Func<TSrc, TDst> f) =>
    new Observable<TDst>(observer =>
    {
        var disposable = observable.Subscribe(x => observer.OnNext(f(x)));
        return new Disposable(disposable.Dispose);
    });
public static IObservable<T> Where<T>(this IObservable<T> observable, Func<T, bool> f) =>
    new Observable<T>(observer =>
    {
        var disposable = observable.Subscribe(x =>
        {
            if (f(x))
                observer.OnNext(x);
        });
        return new Disposable(disposable.Dispose);
    });
public static IObservable<T> Merge<T>(this IObservable<T> observable1, IObservable<T> observable2) =>
    new Observable<T>(observer =>
    {
        var disposable1 = observable1.Subscribe(observer.OnNext);
        var disposable2 = observable2.Subscribe(observer.OnNext);
        return new Disposable(() =>
        {
            disposable1.Dispose();
            disposable2.Dispose();
        });
    });


動作確認

長方形上でLeftDown、LeftUp時に座標をコンソールに表示するアプリケーションになります。

左Shift押下時はDownが、左Ctrl押下時はUpが無視されるようになっています。

Disposeボタンを押すと、一切出力されなくなります。

f:id:any-programming:20170511155239p:plain
Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="175" Width="135">
    <Grid>
        <Rectangle x:Name="rect" Fill="#FFF4F4F5" Stroke="Black" Margin="10,10,10,35"/>
        <Button x:Name="buttonDispose" Content="Dispose" Margin="10,115,10,10" />
    </Grid>
</Window>

C#

public MainWindow()
{
    InitializeComponent();

    var down = rect.MouseLeftButtonDownObs()
       .Where(x => Keyboard.IsKeyUp(Key.LeftShift))
       .Select(x => x.GetPosition(rect));
    var up = rect.MouseLeftButtonUpObs()
       .Where(x => Keyboard.IsKeyUp(Key.LeftCtrl))
       .Select(x => x.GetPosition(rect));
    var subscription = down.Merge(up).Subscribe(Console.WriteLine);
    
    buttonDispose.Click += (s, e) => subscription.Dispose();
}