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