RX extensions w przykładach

RxExtension – to biblioteka od Microsoftu ułatwiająca programowanie asynchroniczne. Opiera się na istniejących interfejsach IObservable oraz IObserver.
W RX wiadomości są traktowane jako strumienie danych, do których należy się przypiąć i reagować na pojawienie się nowej wiadomości. Najnowsza wersja ma już cyferkę 2, ale nie jest jeszcze oznaczona jako stabilna. Ja do nauki wykorzystałem wersję 1 oraz książeczkę dostępną na stronie RX – Dev Labs Hands On. Na Channel9 są jakieś filmy na temat RX. Poniżej pokaże kilka przykładów i opis jak korzystać z RX, źródła pochodzą z książki, będą dostępne razem z książką i projektem MSVC. Drobna uwaga co do książki i źródeł: w książce podane są dwa pliki do których należny dodać referencję, aktualna wersja RX dostarcza tylko jednej biblioteki – System.Reactive, natomiast System.CoreEx jest już w systemie. Ja korzystałem z .net 4.0. Podobnie kilka metod wymienionych w książce nie jest już dostępne w związku z czym zostały zamienione na inne dostępne i spełniające te same wymagania.

Zaczynamy od poznania interfejsów:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable.Empty<int>();
   4:      IObserver<int> handler = null;
   5:   
   6:      IDisposable subscription = source.Subscribe();
   7:      Console.WriteLine("Press ENTER to unsubscribe and dispose");
   8:      Console.ReadLine();
   9:   
  10:      subscription.Dispose();
  11:  }

RX opiera się o dwa interfejsy IObservable oraz IObserver. Klasa Observable pochodzi System.Reactive.Linq. Kod powyżej nie jest zbytnio porywający, ale pokazuje, że po subskrypcji do źródła danych otrzymujemy instancję IDisposable, którą trzeba będzie wywalić do kosza po zakończeniu pracy. Później będzie to zrobione przy użyciu mechanizmu using.

Interfejs IObserver definiuje trzy metody, które muszę zostać zdefiniowane:

   1:  public interface IObserver<in T>
   2:  {
   3:      void OnCompleted();
   4:      void OnError(Exception error);
   5:      void OnNext(T value);
   6:  }
  • OnCompleted: źródełko wyschło
  • OnNext:  nowa informacja
  • OnError: coś poszło nie tak

Podczas dopisywania się do obiektu, który ma dostarczać informacji można wykorzystać wyżej wymieniony interfejs lub skorzystać z wyrażeń lambda. To drugie podejście jest prostsze, dodatkowo nie wymagane jest definiowanie dla wszystkich metod z interfejsu.
Na początek pełna deklaracja:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable.Empty<int>();
   4:   
   5:      IDisposable subscription = source.Subscribe(
   6:          x=> Console.WriteLine("Has new value {0}", x),
   7:          ex=>Console.WriteLine("Exception caught {0}", ex.Message),
   8:          ()=>Console.WriteLine("No more items")
   9:          );
  10:   
  11:      Console.WriteLine("ENTER to dispose");
  12:      subscription.Dispose();
  13:  }

W tym przypadku od razu zostanie wywołana metoda OnCompleted, ponieważ źródło danych jest puste. Po zakończeniu pracy zwalniamy zasoby przez wywołanie Dispose na obiekcie zwróconym po subskrypcji. Co ważne, aby móc korzystać z rozszerzeń dla RX należy dodać referencję oraz using do System.Reactive.Linq.

Jednoelementowa kolekcja intów:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable.Return(42);
   4:   
   5:      var subscriber = source.Subscribe(
   6:          x=>Console.WriteLine("Value: {0}",x),
   7:          ex=>Console.WriteLine("Exception: {0}", ex.Message),
   8:          ()=>Console.WriteLine("end!")
   9:          );
  10:   
  11:      Console.WriteLine("ENTER to dispose");
  12:      subscriber.Dispose();
  13:  }

Kod powyżej zwróci raz wartość 42 (sens życia), a następnie zakończy poprzez wywołanie OnCompleted. Następnie zwolnienie zasobów. Ponownie wykorzystanie Reactive.Linq do stworzenia jednoelementowej kolekcji.

Aby zrobić coś ciekawszego można podmienić kod definiujący źródło na coś takiego:

   1:  IObservable<int> source = Observable.Range(5, 7);

Spowoduje do wygenerowanie cyfr od 5 do 11, następnie zakończy przez OnCompleted.

Aby nie zanudzać prostymi przykładami, RX umożliwia stworzenie obserwowanej pętli for, robi się to w taki sposób:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable
   4:          .Generate(0,          // initial state
   5:          i => i < 10,    // condition
   6:          i => i + 1,        // iteration step
   7:          i => i * i);       // iteration operation
   8:   
   9:      using (var s = source.Subscribe(
  10:          x => Console.WriteLine(x)    // only the working stuff will be handled
  11:                                          // no errors and no exceptions
  12:                                          // no information about sequence finish either
  13:          )) { };
  14:  }

Coraz ciekawiej co nie?
Zaczynamy od 0, następnie warunkiem jest i mniejsze od 10, w każdym kroku i będzie zwiększane co 1, a wynikiem operacji ma być i*i, wynik mnożenia nie jest zapisywany do i, tylko zwracany jako wynik. W przeciwnym wypadku pętla skończyła by się za wcześnie.
Podczas dopinania się do źródła definiujemy tylko metodę OnNext, która przyjmuje jeden parametr, w ten sposób nie zostaniemy powiadomieni o skończeniu się danych lub o wystąpieniu błędu. Dodatkowo wykorzystany zostanie mechanizm using, na który spadnie odpowiedzialność zwolnienia zasobów z IDisposable.

Jeśli zastanawialiście się jak to możliwe że pętla nie zakończy skoro w klamrach using nic nie ma, nie dziwcie się, dla mnie na początku to także było dziwne. Ale jeśli odpalić debuggera i sprawdzić wątki, to wszystkie wywołania są dokonywane z głównego wątku. Dopiero po skończeniu się zasobów w IObservable, zostanie wykonany kod z klamerek.

Żeby nie było nudno, teraz przykład z normalniejszym kodem, takim który jest w klamerkach:

   1:  static void Main(string[] args)
   2:  {
   3:      var source = Observable.Generate(
   4:          0,
   5:          i => i < 10,
   6:          i => i + 1,
   7:          i => i * i,
   8:          i => TimeSpan.FromSeconds(i)
   9:          );
  10:   
  11:      using (var s = source.Subscribe(
  12:          x => Console.WriteLine("next: {0}", x),
  13:          ex => Console.WriteLine("exception: {0}", ex.Message),
  14:          () => Console.WriteLine("no more")))
  15:      {
  16:          Console.WriteLine("ENTER");
  17:          Console.ReadLine();
  18:      }
  19:  }

Dwie zmiany zostały wprowadzone; dodałem 1 sekundowe opóźnienie w generowaniu wartości i, co powoduje że wywoływania zaczynają wreszcie być prawdziwie asynchroniczne. To powoduje, potrzebę umieszczenie Console.ReadLine w ciele using, w przeciwnym wypadku, nie zdążymy odczytać nawet pierwszej wartości. Walnięcie ENTER w trakcie działania dema, spowoduje jego zakończenie.
Okno wątków w debuggerze, pokazują że podczas wołania OnNext, wykorzystywany jest dodatkowy Worker Thread, stworzony przez RX.

Teraz drobna zmiana, do naszej konsoli dołożona zostanie prosta forma (WinForms – przykłady z książki są na tym oparte, więc i ja z tego skorzystałem). Będzie służyć za pośrednika do źródła danych, załóżmy sobie że jest taki kod:

   1:  static void Main(string[] args)
   2:  {
   3:      var label = new Label();
   4:      var form = new Form
   5:      {
   6:          Controls = { label }
   7:      };
   8:   
   9:      var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
  10:   
  11:      using (moves
  12:          .Subscribe(
  13:              x => label.Text = x.EventArgs.Location.ToString(),
  14:              ex => label.Text = ex.Message,
  15:              () => label.Text = "Mouse is over?!" ) )
  16:      {
  17:          Application.Run(form);
  18:   
  19:      }
  20:  }

Pojawia się tutaj prosta forma z tekstem do którego będziemy zapisywać informacje o aktualnym położeniu myszy. To co interesujące znajduje się w linijce 9, gdzie tworzymy źródło danych na podstawie przychodzących zdarzeń z formy, następnie zapisujemy się do nich. Ponieważ w tym przykładnie nie stosowane są żadne specjalne opóźnienia (o nich poniżej), nie potrzeba tutaj wykorzystywać Dispatchera czy obiekty synchronizacji pomiędzy wątkami. W okienku wątków widać, że każde wywołanie metody OnNext realizowane jest w głównym wątku UI, dlatego bezpieczna jest modyfikacja labelki.

   1:  static void Main(string[] args)
   2:  {
   3:      var label = new Label();
   4:      var form = new Form
   5:      {
   6:          Controls = { label }
   7:      };
   8:   
   9:      var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
  10:   
  11:      using (moves
  12:          .Subscribe(
  13:              x => label.Text = x.EventArgs.Location.ToString(),
  14:              ex => label.Text = ex.Message,
  15:              () => label.Text = "Mouse is over?!" ) )
  16:      {
  17:          Application.Run(form);
  18:      }
  19:  }

Najciekawsza jest linia 9, gdzie wydarzenia generowane przez formę/mysz są definiowane jako źródło danych, a następnie w linii 13 wartość ta zostaje wyłuskana z argumentów oraz zapisana w labelce na formatce.

Kolejny przykład będzie rozwinięciem tego powyżej:

   1:  static void Main(string[] args)
   2:  {
   3:      var textbox = new TextBox();
   4:      var form = new Form
   5:      {
   6:          Controls = { textbox }
   7:      };
   8:   
   9:      var moves = Observable
  10:          .FromEventPattern<MouseEventArgs>(form, "MouseMove")
  11:          .Select(e => e.EventArgs.Location);
  12:   
  13:      var texts = Observable
  14:          .FromEventPattern<EventArgs>(textbox, "TextChanged")
  15:          .Select(e => (e.Sender as TextBox).Text);
  16:   
  17:      var msubs = moves.Subscribe(x => Console.WriteLine("mouse position: {0}", x));
  18:      var ksubs = texts.Subscribe(x => Console.WriteLine("Textbox text: {0}", x));
  19:   
  20:      using (new CompositeDisposable(msubs, ksubs))
  21:      {
  22:          Application.Run(form);
  23:      }
  24:  }

Jak widać tutaj ponownie nasłuchujemy na wiadomości generowane przez ruch myszy, ale dodatkowo oczekujemy na wpisywany przez użytkownika tekst w formatce. Warto zauważyć, że w przypadku tekstu, źródłem danych nie jest formatka, ale sama kontrolka. W tym przykładzie pod uwagę bierzemy tylko pozytywne wywołania OnNext. Dodatkowo subskrypcja wykorzystuje projekcję (Select) i ostatecznej rozgrywce (17 i 18) otrzymujemy tylko takie wartości, które są dla nas najbardziej interesujące. Na sam koniec przykładu, wykorzystujemy klasę CompositeDisposable, która umożliwia wykorzystanie mechanizmu using na więcej niż jednym obiekcie IDisposable.

Kolejnym ciekawy rozszerzenie dla RX jest mechanizm Distinct, który wyśle powiadomienie tylko wtedy, gdy jest ono różne od poprzedniego.

   1:  var texts = Observable
   2:                .FromEventPattern<EventArgs>(textbox, "TextChanged")
   3:                .Select(e => (e.Sender as TextBox).Text)
   4:                .Do(e => Console.WriteLine("Before DistinctUntilChanged: {0}", e))
   5:                .DistinctUntilChanged();

Cały kod jest taki sam jak poprzednio, zmienia się tylko definicja źródła danych dla tekstu. Linia 4 pokazywać będzie, że wiadomości są przesyłane – metoda Do. Natomiast linia 5 zatrzyma ich dalszą propagację, gdy będę one takie same jak poprzednio otrzymane. Należy zwrócić uwagę podczas używania DistinctUntilChanged na kolejność jego użycia. Jeśli zostanie wywołany za wcześnie, kolejne eventy mogą nie zostać przesłane. W ramach ćwiczeń można wstawić go przed Select i zobaczyć co się dzieje. W tym przykładzie, pod uwagę brana będzie zawartości kontrolki w textboxie. A więc, gdy zaznaczona zostanie literka i zostanie zastąpiona przez taką samą, obiekt nasłuchujący nie zostanie powiadomiony o takie zmianie.

Następne rozszerzenie dla RX to Throttle, który powoduje że powiadomienia są przesyłane z pewnym opóźnieniem, każda nowa aktualizacja na źródle resetuje ten licznik. Dla przykładu, gdy chcemy udostępnić mechanizm który działa jak słownik z podpowiedziami, aby nie włączać wyszukiwania dla każdej wpisanej literki, można dodać małe opóźnienie rzędu kilkudziesięciu milisekund, które dla użytkownika będzie nie zauważalne, a jego wykorzystanie spowoduje zmniejszenie wykorzystania zasobów, ponieważ ilość generowanych zapytać będzie mniejsza. Poniżej przykład definicji takiego źródła danych:

   1:  var moves = Observable
   2:            .FromEventPattern<MouseEventArgs>(form, "MouseMove")
   3:            .Select(e => e.EventArgs.Location)
   4:            .Throttle(TimeSpan.FromMilliseconds(100));
   5:   
   6:  var texts = Observable
   7:            .FromEventPattern<EventArgs>(textbox, "TextChanged")
   8:            .Select(e => (e.Sender as TextBox).Text)
   9:            .Throttle(TimeSpan.FromMilliseconds(500))
  10:            .DistinctUntilChanged();

W pierwszy przypadku, informację o pozycji myszy otrzymamy gdy użytkownik okres około 100 ms nie będzie poruszać myszą. Należy być świadomym, że nie dostaniemy wszystkich wartości pośrednich, tylko ostatnią pozycję myszy. W drugim przypadku, jest dodatkowe założenie, użytkownik nie może zmieniać wartości tekstu przez około 500ms, a dodatkowo wartość musi być inna niż poprzednia – modyfikator DistinctUntilChanged. W bardzo prosty sposób ograniczyć można ilość powiadomień, które obsłużyć musi obiekt obsługujący kontrolkę.

Wcześniej mówiłem o tym, że będzie o pełniejszej asynchroniczności oraz o kontrolkach, czas najwyższy na rozwiązanie tej tajemnicy. Jeśli wiemy, że obsługa zdarzeń będzie wymagała interakcji z kontrolkami (lub kolekcjami – np. ObservableCollection w WPF) wymagającymi synchronizami z głównym wątkiem UI należy wykorzystać odpowiednie rozszerzenie, które umożliwia obserwację zdarzeń wykonywaną na głównym wątku aplikacji – zamieszane? Kod was oświeci:

   1:  static void Main(string[] args)
   2:  {
   3:      TextBox t1 = new TextBox();
   4:      Label l1 = new Label { Left = t1.Width + 20 };
   5:      Form f1 = new Form
   6:      {
   7:          Controls = { t1, l1 }
   8:      };
   9:   
  10:      var source = Observable
  11:          .FromEventPattern(t1, "TextChanged")
  12:          .Throttle(TimeSpan.FromMilliseconds(250))
  13:          .Select(x=>(x.Sender as TextBox).Text)
  14:          .DistinctUntilChanged();
  15:   
  16:      using (source
  17:          .ObserveOn(WindowsFormsSynchronizationContext.Current)
  18:          .Subscribe(x => l1.Text = x))
  19:      {
  20:          Application.Run(f1);
  21:      }
  22:  }

Jak widać, źródło danych działa teraz prawdziwie asynchronicznie (wykorzystanie Throttle), aby móc wykonać kod zdefiniowany w OnNext (linia 18) należy wcześniej powiedzieć że obsługa (obserwacja) ma nastąpić w WindowsFormsSynchronizationContext.Current (dla WPF to będzie Application.Current.Dispatcher lub Dispatcher.Current) – lub w implementacji wykorzystać mechanizm synchronizacji, jak zawsze to zależy od potrzeb implementacji. W takiej sytuacji źródło działa w osobnym wątku, ale powiadomienia i ich obsługa nastąpi w wątku głównym, gdzie można modyfikować UI.

Następny przykład jest trochę dłuższy, pokazuje jak wykorzystać RX w połączeniu z serwisami implementowanymi w WCF, dokładniejsze omówienie poniżej:

   1:  private static void Main()
   2:  {
   3:      var service = new DictServiceSoapClient("DictServiceSoap");
   4:      Func<string, string, string, IObservable<DictionaryWord[]>> match = Observable
   5:          .FromAsyncPattern<string, string, string, DictionaryWord[]>(
   6:              service.BeginMatchInDict, service.EndMatchInDict);
   7:   
   8:      Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => match("wn", term, "prefix");
   9:   
  10:   
  11:      var t1 = new TextBox();
  12:      var f1 = new Form
  13:                   {
  14:                       Controls = {t1}
  15:                   };
  16:   
  17:   
  18:      IObservable<string> textSource = Observable
  19:          .FromEventPattern<EventArgs>(t1, "TextChanged")
  20:          .Throttle(TimeSpan.FromMilliseconds(250))
  21:          .Select(x => ((TextBox) x.Sender).Text)
  22:          .Where(x => x.Length > 0)
  23:          .DistinctUntilChanged();
  24:   
  25:      IDisposable formRequest = textSource
  26:          .Finally(()=>Console.WriteLine("Finalized"))
  27:          .Subscribe(x => matchInWordNetByPrefix(x)
  28:                              .Finally(() => Console.WriteLine("Finalized: {0}", x))
  29:                              .Subscribe(words =>
  30:                                             {
  31:                                                 Console.WriteLine("{0} - {1}", x, words.Count());
  32:                                                 foreach (DictionaryWord w in words)
  33:                                                 {
  34:                                                     Console.Write("{0},", w.Word);
  35:                                                 }
  36:                                                 Console.WriteLine("n*******************************************");
  37:                                             },
  38:                                         ex =>
  39:                                             {
  40:                                                 Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  41:                                                 Console.WriteLine(ex.Message);
  42:                                                 Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  43:                                             }));
  44:   
  45:   
  46:      using (formRequest)
  47:      {
  48:          Application.Run(f1);
  49:      }
  50:  }

Zacznijmy od początku:
Linia 3 definiuje nowe proxy do serwisu WCF, nie istotne z punktu widzenia RX. Następnie linia 4 definiuje zmienną typu Func, będzie to delegat przyjmujący jako dane wejściowe trzy stringi, a zwracający tablicę wyrazów słownikowych (ach ten mój angielski). Dalej (ciągle w linii 4) przypisujemy do tego delegata metodę z RX wykorzystując wcześniej zdeklarowany serwis WCF, a dokładniej dwie jego metody BeginMatchInDict oraz EndMatchInDict. Są to dwie metody, które umożliwiają asynchroniczne wykorzystanie serwisu. Podczas definiowania dostępu do serwisu WCF zaznaczona została opcja generowania zapytań asynchronicznych. Dalej, linia 8 to ponowne zdefiniowanie delegata, tym razem będzie on przyjmować tylko jeden argument – string, który będzie zawierać właściwe dla nas zapytanie, zwracany typ nie ulegnie zmianie. Pozostałe dwa stringi zostają przypisane na stałe. Ich wartość nie jest istotna dla RX. Teraz możemy korzystać asynchronicznie z WCF podając tylko jednego stringa podczas zapytania. Potem “normalna” definicja formatki, później źródełko z danymi;  pierwsze to textbox, drugie to serwis WCF który na podstawie wysłanej części słowa, zwraca tablicę wyrazów, który mogą je dokończyć (np. “yellow a“: yellow adder’s tongue,yellow ageratum,yellow asphodel,yellow avens). Metoda Finally posłuży aby wykonać jakiś kod w momencie zwalniania zasobów zwróconych przez Subscribe. Kod z linijki 28 zostanie wykonany po tym, gdy wszystkie pasujące słowa zostaną wypisane na ekran. Linijka 26 zostanie wywołana po zamknięciu formatki, czyli  zakończeniu using z linii 46.

Jeśli podczas zabawy z tym przykładem zauważycie, że możliwe jest zdefiniowanie nowego pytania do serwisu WCF zanim, przyjdzie odpowiedź na wcześniejsze – to macie racje (za .net rocks – golfclap for you). Występuje tu taka sytuacja. Na rozwiązanie tego problemu zaproponowano dwa podejścia, dla mnie Switch jest (będzie poniżej) czytelniejszy i zrozumiały. W udostępnionym projekcie są oba rozwiązania.

   1:  static void Main(string[] args)
   2:  {
   3:      var t1 = new TextBox();
   4:      var l1 = new ListBox { Top = t1.Height + 10, Height = 250, Width = 150 };
   5:      var f1 = new Form
   6:      {
   7:          Controls = { t1, l1 }
   8:      };
   9:   
  10:      var textSource = Observable
  11:          .FromEventPattern<EventArgs>(t1, "TextChanged")
  12:          .Throttle(TimeSpan.FromMilliseconds(50))
  13:          .Select(x => (x.Sender as TextBox).Text)
  14:          .Where(x => x.Length >= 3)
  15:          .DistinctUntilChanged()
  16:          .Do(Console.WriteLine);
  17:   
  18:      var service = new DictServiceSoapClient("DictServiceSoap");
  19:      var dictSource = Observable
  20:          .FromAsyncPattern<string, string, string, DictionaryWord[]>(service.BeginMatchInDict, service.EndMatchInDict);
  21:   
  22:      Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => dictSource("wn", term, "prefix");
  23:   
  24:      var data = textSource
  25:          .Select(x => matchInWordNetByPrefix(x))
  26:          .Switch();
  27:   
  28:      using (data
  29:          .ObserveOn(WindowsFormsSynchronizationContext.Current)
  30:          .Subscribe(w =>
  31:          {
  32:              l1.Items.Clear();
  33:              l1.Items.AddRange(w.Select(word => word.Word).ToArray());
  34:          },
  35:          ex =>
  36:          {
  37:              MessageBox.Show(ex.Message);
  38:          }))
  39:   
  40:          Application.Run(f1);
  41:  }

Początek bez zmian, dopiero podczas wykorzystywania tego co wpisze użytkownik i przesyłania tego do serwisu WCF, pojawiają się pierwsze zmiany. Na początku (linia 24) czekamy aż, użytkownik coś wpisze, a następnie wysyłamy to do serwisu. Jeśli w między czasie użytkownik zmieni coś w kontrolce, ponownie wyślemy zapytanie, ale z nową wartością. Aby nie obsługiwać wyników dla poprzedniego zapytania wykorzystana zostaje metoda Switch, której działanie polega na zwróceniu wyniku tylko z ostatniego zapytania, wszystkie poprzednie zostają anulowane. Reszta zmian to tylko kosmetyka, w tym przykładnie wyniki pojawiać się będą w kontrolce na formatce.

Na moje oko RX wydaje się być interesującym rozszerzeniem, umożliwiającym uproszczenie i kodu, a już na pewno metody jak Throttle czy DistinctUntilChanged uczynią aplikacje mniej zasobożernymi. Jak z każdą nowo poznaną technologią trzeba trochę czasu, aby poznać wszystkie za i przeciw. Ja na razie jestem na etapie wow. Poprawcie mnie jeśli się mylę, ale RX jest wyczesany.

Projekt, źródła i książka dostępne pod następującym adresem:
Projekt: https://bitbucket.org/jstadnicki/rx-examples
Git: https://bitbucket.org/jstadnicki/rx-examples.git

Async i Await w Windows8 – małe szoł

Jestem w miarę świeżo po przeczytanie Programowania Windows 8  (w wersji preview) napisanej przez pana, który się nazywa Charles Petzold. Krótka recenzja:
Aktualnie książka zawiera siedem rozdziałów, w nich omówienie XAML, mechanizmu wiązań, kontrolek, layout i krótko o WinRT. Jeśli ktoś z was pisał już w WPF czy SL nie znajdzie w niej (przypominam że mówię ciągle o wersji preview) nic ciekawego. Prawie nic, otóż są dwie nowości warte uwagi wprowadzone w nowszej wersji .NET. CallerMemberName oraz async/await. Książkę kupiłem za 10USD i gdy CP dopisze kolejne rozdziały książki ja je dostanę za darmo. Taki bajer, także warto było wydać 30 parę złoty. Książkę nadal można kupić, choć wciąż jest to wersja preview, ale teraz kosztuje już 20USD (promocja z dosłaniem brakujących rozdziałów nadal obowiązuje). Docelowo finalna wersja ma kosztować 50USD (ciągle pisze usb zamiast usd) i być dostępna w okolicach listopada.

Dzisiaj krótko o tym jak mechanizm async/await jest w stanie uprościć kod pisany z myślą o Windows 8 i aplikacjach w stylu metro.
Zaczniemy od wersji nie zawierającej słów kluczowych async i await. Przykładowa implementacja kodu odpowiedzialnego za wczytanie tekstu ze wskazanego przez użytkownika pliku tekstowego, a zawartości tego pliku wyświetlona w kontrolce na oknie aplikacji. Kod zaczerpnięty z wcześniej przytoczonej książki.

Wybieraj mądrze użytkowniku:
Umożliwiamy wybranie pliku, którego zawartość ma zostać wczytana. Warto zauważyć że filtertype jest w postaci “.txt” bez gwiazdki na początku. Następnie ustawiamy wskaźnik do metody, która ma się wykonać gdy użytkownik potwierdzi swój wybór.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  private void OnOpenAppBarButtonClick(object s, RoutedEventArgs a)
   2:  {
   3:      FileOpenPicker fop = new FileOpenPicker();
   4:      fop.FileTypeFilter.Add(".txt");
   5:      IAsyncOperation<StorageFile> operation = fop.PickSingleFileAsync();
   6:      operation.Completed = OnPickSingleCompleted;
   7:  }

Otwieraj pliki cudny systemie:
Powiadomienie o zakończeniu działania wybieracza plików (FilePicker), warto sprawdzić czy użytkownik i system zachował się fair i dostarczył wszystkich potrzebnych danych wymaganych do załadowania pliku testowego. Jeśli tak to grzecznie prosimy system żeby otworzył plik i dał znać gdy skończy.

   1:  private void OnPickSingleCompleted(IAsyncOperation<StorageFile> asyncInfo, AsyncStatus asyncStatus)
   2:  {
   3:      if (asyncInfo.ErrorCode != null)
   4:      {
   5:          return;
   6:      }
   7:   
   8:      StorageFile storage = asyncInfo.GetResults();
   9:   
  10:      if (storage == null)
  11:      {
  12:          return;
  13:      }
  14:   
  15:      IAsyncOperation<IRandomAccessStreamWithContentType> operation = storage.OpenReadAsync();
  16:      operation.Completed = OnFileOpenReadCompleted;
  17:  }

Czytaj wszystko:
DataReader został w tym przypadku zdefiniowany w klasie. Pamiętamy, że to tylko przykład, nie czepiamy się czystości kodu. Ołkiej dalej, jeśli plik udało się otworzyć zaczynamy jego odczyt. Aby nie marnować czasu czekając na wynik takiej operacji, podobnie jak poprzednio ustawiamy obsługiwacza, który zostanie wywołany po tym, gdy datareader wczyta wszystko z pliku.

   1:  DataReader dataReader;
   2:   
   3:  private void OnFileOpenReadCompleted(IAsyncOperation<IRandomAccessStreamWithContentType> asyncInfo, AsyncStatus asyncStatus)
   4:  {
   5:      if (asyncInfo.ErrorCode != null)
   6:      {
   7:          return;
   8:      }
   9:   
  10:      using (var stream = asyncInfo.GetResults())
  11:      {
  12:          using (this.dataReader = new DataReader(stream))
  13:          {
  14:              uint length = (uint)stream.Size;
  15:              DataReaderLoadOperation operation = this.dataReader.LoadAsync(length);
  16:              operation.Completed = OnDataReaderLoadCompleted;
  17:          }
  18:      }
  19:  }

Wyświetlaj cudowna kontrolko.
Jeśli wszystko się udało, wyciągamy wczytane dane z datareader, a następnie ustawiamy je w kontrolce. Oczywiście, że pamiętamy o tym, że wolno modyfikować ją tylko z głównego wątku. Tutaj też mała zmiana, znika metoda Invoke, należy skorzystać z RunAsync.

   1:  private void OnDataReaderLoadCompleted(IAsyncOperation<uint> asyncInfo, AsyncStatus asyncStatus)
   2:  {
   3:      if (asyncInfo.ErrorCode != null)
   4:      {
   5:          return;
   6:      }
   7:   
   8:      uint length = asyncInfo.GetResults();
   9:      string text = dataReader.ReadString(length);
  10:   
  11:      this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  12:      {
  13:          this.textblock.Text = text;
  14:      });
  15:  }

No i to tyle. Kod można oczywiście skrócić wykorzystując wyrażenia lambda, ale wtedy nie uzyskamy efektu wow. Racja?

Uwaga, tylko u mnie, tylko teraz, tylko tu,  rewolucja w programowaniu. Jak się to robi w Windows8?

Klik w guzik:
Przypominam aby zwrócić uwagę na słówko kluczowe async podczas definiowania handlera. Async musi być w metodach, które chcą w swoim ciele skorzystać z opcji await. I co ważne brak wykorzystania Dispatcher, ponieważ to co następuje po await jest wywołane w głównym wątku aplikacji.

   1:  private async void OnOpenAppBarButtonClick(object s, RoutedEventArgs a)
   2:  {
   3:      this.textblock.Text = await this.ReadFileAsync();
   4:  }

Niech się dzieje magia.
Właściwa implementacja:

   1:  private async Task<string> ReadFileAsync()
   2:  {
   3:      FileOpenPicker picker = new FileOpenPicker();
   4:      picker.FileTypeFilter.Add(".txt");
   5:      var storage = await picker.PickSingleFileAsync();
   6:      if (storage == null)
   7:      {
   8:          return null;
   9:      }
  10:      return await FileIO.ReadTextAsync(storage);
  11:  }

No ja was proszę, kto teraz nie rzuca bielizną zrywaną przez głowę? Jest efekt wow? Nie ma? Przewińcie na górę i jeszcze raz przeczytajcie ten rozdmuchany kod, a potem z powrotem do nowego z async/await. Jeszcze raz na górę, jeszcze raz na dół, teraz spójrz na mnie, siedzę na krześle.

Ok, zen, wracamy do kodu. Tak napisany kod robi się czytelniejszy, jasne że za tą całą magią robi się syfek, jeśli ktoś oglądał sesje o tym co kompilator C# robi z kodem, to wie co może się znaleźć po sparsowaniu przejrzystego kodu, ale to już nie nasze zmartwienie. My – zwykli programiści, nie musimy się aż tak bardzo przejmować tym co się dzieje za kurtyną. Że MS robi totalną sieczkę z tych kilku linijek kodu, to sprawa MS jak to zaimplementował. Dla mnie (większości z was też to dotyczy) ważne jest, że działa i że mogę tego używać.

To tyle na dziś, zapraszam do dyskusji, uczestnictwa w grupach społecznościowych, np. takiej wrocławskiej i OWIONIE.
Uwagi w komentarzach chętnie przyjmę.
Kod z przykładami do Windows8 z książki CP trzymam na bitbucket. Jeśli ktoś chce obczaić to zapraszam:

git clone https://bitbucket.org/jstadnicki/programming-windows8.git
git clone git@bitbucket.org:jstadnicki/programming-windows8.git