Reactive Extensions – System.Reactive.dll

2

Ostatnio natknąłem się na coś, co… blew my mind (wydymało mi umysł?). Bardzo krótko wspomniał o tym Bartek Pampuch na ostatnim MTS i utkwiło mi w pamięci, ale dopiero kilka dni temu niechcący przyjrzałem się "owemu czemuś" bliżej. Owo cuś to biblioteka Reactive Extensions (Rx) implementująca założenia programowania reaktywnego by Microsoft.

Nie będę zagłębiał się w teorię czy omawianie szczegółów, ponieważ sam dopiero zacząłem skrobać powierzchnię tej megazajebistej koncepcji i postanowiłem jedynie podzielić się takim znaleziskiem. Tak więc krótka próbka możliwości, jakie w nasze ręce oddają twórcy Rx. Cel: wszystkie kontrolki na formatce mają być "draggable", co ma być w prosty sposób aktywowane. Najlepiej takim wywołaniem:

  1:  foreach (Control control in this.Controls)
  2:  {
  3:  	control.EnableDrag();
  4:  }

(krótka uwaga: to jest chyba najbardziej oklepany przykład zastosowania Rx i można do niego dojść wieloma drogami; podobne rozwiązanie opisał Marcin Najder w swoim poście, który przeczytałem po napisaniu tego kodu).

Ale jak dojść do takiej konstrukcji? Otóż Rx umożliwia nam spojrzenie na .NETowe zdarzenia (i nie tylko .NETowe zdarzenia) jak na źródlo danych odpytywalne przez LINQ. Służy do tego metoda Observable.FromEvent:

  1:  public static IObservable<IEvent<MouseEventArgs>> GetMouseDown(this Control _this)
  2:  {
  3:  	return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
  4:  		(
  5:  		x => new MouseEventHandler(x),
  6:  		x => _this.MouseDown += x,
  7:  		x => _this.MouseDown -= x
  8:  		);
  9:  }

Powyższy kod "konwertuje" standardowe zdarzenie MouseDown na kolekcję danych reprezentujących kliknięcie myszką. Dzięki temu możemy w inny sposób niż proste podpięcie się pod zdarzenie spowodować, że po każdym kliknięciu na kontrolkę wyświelony zostanie message box:

  1:  label.GetMouseDown().Subscribe(x => MessageBox.Show("label clicked"));

Oczywiście takie zastosowanie nie jest specjalnie imponujące, dokładnie to samo osiągnęlibyśmy najzwyklejszym handlerem bez żadnych problemów. Ale już wymaganie "pokaż msgbox tylko dla dwóch pierwszych kliknięć" nie jest tak banalne. Zwykły handler musiałby trzymać gdzieś jakiś licznik, inkrementować jego wartość, sprawdzać, odpinać się od zdarzenia… Niby głupia sprawa, a jak skomplikowana implementacja. Natomiast dzięki konwersji zdarzenia MouseDown do IObservable wystarczy taka linijka z LINQ:

  1:  label.GetMouseDown().Take(2).Subscribe(x => MessageBox.Show("label clicked"));

Po dwóch kliknięciach wrócimy do stanu sprzed subskrypcji. Fajne, nie?

Jeszcze fajniejsza jest możliwość tworzenia własnych kompozycji zdarzeń. Kiedy mogłoby być publikowane zdarzenie "Drag"? Można je opisać tak: każde zdarzenie MouseMove występujące po MouseDown spowodowanym lewym przyciskiem, ale przed MouseUp. Pewnie wielu z was zdarzało się implementować coś takiego ręcznie. Nigdy nie jest to przyjemne: ustawianie flagi na kliknięciu, sprawdzanie jej w ruchu myszki, odznaczanie jej na puszczeniu przycisku… a dzięki Rx wygląda to tak:

  1:  public static IObservable<DraggableInfo> GetDrag(this Control _this)
  2:  {
  3:  	return from down in _this.GetMouseDown()
  4:  	       where down.EventArgs.Button == MouseButtons.Left
  5:  	       from move in _this.GetMouseMove().Until(_this.GetMouseUp())
  6:  	       select new DraggableInfo(
  7:  	       	_this,
  8:  	       	new Size(down.EventArgs.Location),
  9:  	       	Point.Add(_this.Location, new Size(move.EventArgs.Location))
 10:  	       	);
 11:  }

Instrukcje LINQ dokładnie opisują to co nas interesuje: zdarzenie MouseMove występujące pomiędzy lewym MouseDown a MouseUp. Mało tego, możemy bardzo precyzyjnie określić jakie informacje z tego zdarzenia chcemy uzyskać! W tym przypadku jest to kontrolka, którą będziemy "suwać", pozycja początkowa myszki względem pozycji kontrolki oraz aktualna pozycja kursora:

  1:  public class DraggableInfo
  2:  {
  3:  	public Control Source { get; private set; }
  4:  	public Size InitialOffset { get; private set; }
  5:  	public Point NewAbsoluteLocation { get; private set; }
  6:  
  7:  	public DraggableInfo(Control source, Size initialOffset, Point newAbsoluteLocation)
  8:  	{
  9:  		Source = source;
 10:  		InitialOffset = initialOffset;
 11:  		NewAbsoluteLocation = newAbsoluteLocation;
 12:  	}
 13:  }

Z takimi danymi przesunięcie kontrolki w odpowiednim momencie do odpowiedniego miejsca jest wprost banalne:

  1:  public static void EnableDrag(this Control _this)
  2:  {
  3:  	_this.GetDrag().Subscribe(x =>
  4:  	{
  5:  		x.Source.Location = Point.Subtract(x.NewAbsoluteLocation, x.InitialOffset);
  6:  	});
  7:  }

Czy nie jest to po prostu… świetne?

A Rx oferuje dużo, dużo więcej niż ciąganie kontrolek. Na przykład taka linijka spowoduje wykonanie instrukcji co 2 sekundy przez najbliższe 15 sekund:

  1:  Observable.Interval(TimeSpan.FromSeconds(2))
  2:  	.Until(Observable.Timer(TimeSpan.FromSeconds(15)))
  3:  	.Subscribe(
  4:  	x => MessageBox.Show("2 seconds passed")
  5:  	);

Tak, to TAKIE PROSTE!

A to naprawdę jedynie wierzchołek góry lodowej. Na pewno niejednokrotnie temat ten pojawi się na moim blogu w miarę jak sukcesywnie będzie udawało mi się znajdować dla Rx nowe zastosowania.

Mam nadzieję że tą dość chaotyczną notką udało mi się zachęcić kilka osób do zainteresowanią się Reactive Extensions. Do ściągnięcia (3 różne wersje: .NET 3.5, .NET 4.0, Silverlight 3.0) na DevLabs.

Więcej materiałów:

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

2 Comments

  1. Też ostatnio natknąłem się na tę bibliotekę i również wydymała mi umysł ;] Zastanawiałem się, czy ktoś w Polsce już to opisał i trafiłem tutaj.

    Ciekawa notka. Ze swojej strony mogę gorąco polecić dwuczęściowy filmik, gdzie twórcy Rx opowiadają trochę o samej idei i jej podstawach teoretycznych (algebra abstrakcyjna, monoidy, teoria kategorii i takie tam) oraz o zastosowaniach i samej implementacji. Naprawdę warto posłuchać człowieka, który był jedną z najważniejszych osób biorących udział w projektowaniu LINQ i teraz Rx — Erika Meijera. Jego słowa na końcu filmu świetnie podsumowują jego ostatni projekt:

    "Czasem myślę sobie, że to jest tak piękne, że mógłbym już przejść na emeryturę. Że już nigdy więcej nie będzie mi dane wymyślić czegoś równie pięknego."

    Pozdrawiam,
    Marek Stój

    PS Byłbym zapomniał; tu są linki:

    http://channel9.msdn.com/shows/Going+Deep/E2E-Erik-Meijer-and-Wes-Dyer-Reactive-Framework-Rx-Under-the-Hood-1-of-2/
    http://channel9.msdn.com/shows/Going+Deep/E2E-Erik-Meijer-and-Wes-Dyer-Reactive-Framework-Rx-Under-the-Hood-2-of-2/

    PS2 Pozwoliłem sobie podlinkować ten wpis na devPytaniach; ciekaw jestem bowiem, na jakie ciekawe zastosowania tej biblioteki wpadną inni programiści: http://devpytania.pl/questions/319/reactive-extensions-zastosowania