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ボタンを押すと、一切出力されなくなります。
<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>
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();
}