Reactive Extensions: Konwersja zdarzeń .NET do RX

Po przeczytaniu poprzednich wpisów, zasada działania i zastosowanie RX powinny być już jasne. Jeśli tak nie jest, koniecznie zachęcam do przeczytania tamtych postów. Jak wspomniałem, RX to ujednolicony model, umożliwiający korzystanie z kolekcji typu “push-based” w jednakowy sposób. Dzisiaj pokażę jak sprawa wygląda dla zdarzeń czyli jak skonwertować EventHandler do IObervable.

Oczywiście kluczem do rozwiązania jest klasa Observable, która zawiera mnóstwo rozszerzeń dla interfejsów IObservable\IObserver. Znajduje się tam również metoda FromEventPattern, odpowiedzialna właśnie za konwersje zdarzenia do IObservable. Istnieje kilka sygnatur i używamy ich w zależności od tego jaki sposób bardziej odpowiada nam (od strony dobrych praktyk):

// I
var inputSource =
Observable.FromEventPattern<TextChangedEventArgs>(termTextBox, "TextChanged");

// II
var movingEvents = 
Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(h => this.MouseMove += h, h => this.MouseMove -= h); 

Sposób pierwszy polega na przekazaniu instancji obiektu zawierającego zdarzenie i nazwy tego zdarzenia w formie string. Druga sygnatura przyjmuje delegaty odpowiedzialne za dodanie oraz usunięcie zdarzenia.  Spróbujmy więc napisać pierwszy kod, który coś robi. Załóżmy, że mamy pole edycyjne w aplikacji WPF i chcemy wyświetlać komunikat, gdy wprowadzony tekst jest dłuższy niż 5 znaków:

var eventSource = Observable.
                  FromEventPattern<TextChangedEventArgs>(textbox1, "TextChanged").
                  Select(e => ((TextBox) e.Sender).Text).
                  Where(text => text.Length > 5);

eventSource.Subscribe(text=>MessageBox.Show(text));

Wywołanie FromEventPattern zwraca IObservable dla zdarzenia o nazwie TextChanged. Następnie za pomocą LINQ zwracamy tylko tekst – nie potrzebujemy informacji o Sender czy EventArgs. Za pomocą Where nakładamy filtr. Proszę zauważyć, że w taki sposób korzystanie z zdarzeń niczym nie różni się od zwykłej kolekcji. Nie musimy martwić się już o buforowanie poprzednich elementów ponieważ traktujemy serie zdarzeń jak najzwyklejszą kolekcję danych.

Kolejnym przykładem może być rysowanie linii. W dzisiejszym poście ograniczymy się do narysowania linii od punktu (0,0) do aktualnej pozycji kursora, pod warunkiem, że lewy przycisk myszy jest wciśnięty. W tym celu stworzono kontrolkę dziedziczącą po Canvas:

public class MyCanvas : Canvas
{
   private Point _endPoint;

   public MyCanvas()
   {
       var eventsSource =
               Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove").
               Where(e => e.EventArgs.LeftButton == MouseButtonState.Pressed).
               Select(e => e.EventArgs.GetPosition(this));

       eventsSource.Subscribe(pos =>
                                  {
                                      _endPoint = pos;
                                      InvalidateVisual();
                                  });
   }     
   protected override void OnRender(DrawingContext dc)
   {
       base.OnRender(dc);
       dc.DrawLine(new Pen(Brushes.Black, 1), new Point(), _endPoint);
   }
}

Efekt końcowy:

image

Miałem zamiar w dzisiejszym wpisie pokazać bardziej praktyczny mechanizm drag&drop w RX ale stwierdziłem, że musi najpierw powstać jeszcze jeden post o funkcjach służących do scalania dwóch różnych źródeł. W ten sposób będziemy mogli połączyć MouseDown z MouseMove i na końcu znów z MouseUp.

Oprócz łatwej możliwości dostępu do dowolnych zdarzeń (brak konieczności buforowania), bardzo lubię fakt, że Subscribe zwraca tak naprawdę IDisposable:

using(eventsSource.Subscribe(pos =>
                             {
                                 _endPoint = pos;
                                 InvalidateVisual();
                             }))
{
      // zdarzenie podlaczone
}
  // tutaj zdarzenie jest juz odlaczone poniewaz nastapilo wywolanie IDisposable.Dispose()

Jak widać, łatwo sprzątać po sobie zasoby. Zdarzenia są zdecydowanie w .NET zbyt trudne do usuwania i często powodują memory leak. Pisałem już kiedyś o tzw. weak event pattern, ale osobiście preferuje RX. To nie koniec przygody ze zdarzeniami w RX – za kilka postów przedstawię kolejny mechanizm, przeznaczony wyłącznie dla zdarzeń.

Warto pamiętać, że RX dostarcza oprócz OnNext, takie metody jak OnCompleted oraz OnEror co ułatwia obsługę błędów. Dzięki LINQ łatwo tworzyć skomplikowane zapytania czy transformacje typu scalenie dwóch zdarzeń (o tym w następnym wpisie).

One thought on “Reactive Extensions: Konwersja zdarzeń .NET do RX”

  1. Drobna uwaga 😉 Użycie subskrypcji w klauzuli using, jeśli nasłuchuje na eventy tak jak w tym przypadku spowoduje natychmiastowe ‘zdisposowanie’ i nie wykona się ani razu kod w subskrypcji.

    Przykład:

    using (Observable.Interval(TimeSpan.FromMilliseconds(10)).Subscribe(i => Console.WriteLine(i)))
    {
    Console.WriteLine(“Koniec…”);
    }

    Console.WriteLine(“Po usingu”);

    Wynik na konsoli to:

    Koniec…
    Po usingu

Leave a Reply

Your email address will not be published.