Silverlight Tipp der Woche: async-basierte Silverlight Web-Services

by St. Lange 25. May 2012 21:10

Silverlight Tipp der Woche: async basierte Silverlight Web-Services

In diesem Tipp geht es um die Verwendung vom neuen C# 5 Schlüsselwort async beim Aufruf von Web-Services in Silverlight.

Zusammenfassung

async und await machen asynchronen Code wesentlich übersichtlicher, weil sie dessen logische Struktur erhalten. Das hilft vor allem beim Aufruf von Web-Services. In Silverlight sind Web-Service Referenzen von Hause aus aber leider nicht „awaitable“. Hier wird erklärt, wie es trotzdem geht.

(Dieser Artikel setzt die Arbeitsweise von async und await als bekannt voraus.)

Aufruf von asynchronem Code

Wer schon mal mit den neuen C# 5 Schlüsselworten async und await programmiert hat, fragt sich sehr schnell, wie er denn bisher ohne diese leben konnte.

In Visual Studio 2010 werden sie durch die Async CTP Version 3 für alle Arten von .NET Projekten zur Verfügung gestellt.

In Visual Studio 11 sind sie als Compiler-Feature bereits enthalten und in .NET 4.5 Projekten auch unmittelbar verfügbar. Für .NET 4 und Silverlight 5 Projekte muss zunächst noch das Microsoft.CompilerServices.AsyncTargetingPack via NuGet dazu installiert werden.

Vor allem beim Aufruf von asynchronen Web-Service-Funktionen in Silverlight kommt die neue Schreibweise wie gerufen. Im Prinzip könnte man jetzt folgendes schreiben:

var result1 = await client.DoThisAsync("Hallo");
var result2 = await client.DoThatAsync(result1, 43);

Keine Callbacks, keine Lambdas, kein Spaghetti-Code mit Error und UserState Properties. Nur zwei intuitive Zeilen Code, die exakt das machen, was man unmittelbar beim Lesen des Codes erwartet. Das ist schon phantastisch. Leider steht diese Schreibweise für Web-Services nicht unmittelbar zur Verfügung.

Das Problem

Die bisher generierten asynchronen Web-Service-Funktionen kann man natürlich nicht mit async verwenden, denn Funktionen, die „awaitable“ sein wollen, müssen ein Task- oder Task<T>-Objekt zurückgeben. Für bestehende Klassen wie WebClient gibt es daher Extension-Methods wie DownloadStringTaskAsync als Variante von DownloadStringAsync, die ein Task<string> zurückliefert.

Beim Import von Service-Referenzen müssten also eigentlich nur anders aufgebaute Funktionen generiert werden. Diese Funktionen müssten Task-Objekte zurückgeben und dafür würden die ganzen Completed-Handler und deren EventArgs wegfallen. Für .NET 4.5 Projekte wurde dazu in Visual Studio 11 eine neue Option beim Importieren eingeführt: „Generate task-based operations“. Diese Option generiert alternative Funktionen, die man mit await verwenden kann. Für Silverlight oder .NET Projekte kleiner als 4.5 ist diese Option allerdings nicht verfügbar. Hier der Sceenshot für ein Silverlight 5 Projekt:

Die Option ist grau. Und in Visual Studio 2010 geht das natürlich sowieso nicht.

Die Lösung

Beim Importieren eines Web-Services generiert einem der Compiler ja schon asynchrone Funktionen mit jeweils einem Completed-Handler. Vielleicht kann man den Code ja irgendwie wiederverwenden. Bei meinem Versuch die generierten Funktionen mit Hilfe eines Expression-Trees zu zerlegen, um dann mit Reflection die Funktionen des darunterliegenden Channels aufzurufen, habe ich eine sehr viel einfachere Lösung gefunden.

Nehmen wir zur Erläuterung einen WCF-Webservice mit drei typischen Funktionen:

public void DoAction(string s, double d, DateTime t) …
public string DoThis(string s) …
public int DoThat(string s, int n) …

Dieser Service wird wie üblich in einer Web-Anwendung implementiert und über „Add Service Reference“ zum Silverlight Projekt unter dem Namen Service1 hinzugefügt. Der Compiler generiert daraus diverse Dateien, die man auch sehen kann, wenn für das Projekt „Show All Files“ aktiv ist. Eine der generierten Dateien ist Reference.cs, die wiederum u.a. die Klasse Service1Client enthält. Service1Client enthält die private Unterklasse Service1ClientChannel, eine typsichere Implementierung des Servicecontracts unseres Webservices. Und hier können wir ansetzen, denn der Servicecontract ist auf Basis des BeginInvoke/EndInvoke Patterns implementiert. Dieses Pattern wurde bereits in .NET 1.0 eingeführt und benötigt für jede asynchrone Operation zwei Funktionen, die als Paar immer den Namenskonventionen BeginXxx und EndXxx folgen. BeginXxx liefert ein IAsyncResult zur Überwachung der asynchronen Operation und EndXxx nimmt nach deren Beendigung ein IAsyncResult entgegen und extrahiert daraus den Rückgabewert. Die wesentlich neuere Klasse Task verfügt über eine Kompatibilitätsmethode FromAsync, die asynchrone Operationen, die über ein solches async-Pattern bereitgestellt werden, in ein Task-Objekt umwandelt.

Da Service1Client freundlicherweise als partial deklariert ist, brauchen wir die Klasse nur um drei neue Funktionen in der gewünschten Form ergänzen:

public partial class Service1Client
{
  public Task DoActionTaskAsync(string s, double d, DateTime t)
  {
    return Task.Factory.FromAsync(Channel.BeginDoAction(s, d, t, null, null), Channel.EndDoAction);
  }
 
  public Task<string> DoThisTaskAsync(string s)
  {
    return Task<string>.Factory.FromAsync(Channel.BeginDoThis(s, null, null), Channel.EndDoThis);
  }
 
  public Task<int> DoThatTaskAsync(string s, int n)
  {
    return Task<int>.Factory.FromAsync(Channel.BeginDoThat(s, n, null, null), Channel.EndDoThat);
  }
}

Das Muster ist leicht zu erkennen. In die Funktion FromAsync werden das Ergebnis des Aufrufs der BeginXxx-Funktion sowie ein Delegate auf EndXxx reingereicht. Die beiden letzten Parameter der BeginXxx-Funktionen sind übrigens immer ein AsyncCallback und ein User-State Objekt. Beide sind null, da wir sie hier nicht benötigen.

Da alle Servicefunktionen bereits unter den Namen XxxAsync existieren, habe ich sie analog zu oben erwähnten Extension-Methods von WebClient in XxxTaskAsync umbenannt. Alternativ könnte man auch eine neue Klasse von Service1Client ableiten und die bisherigen Funktionsnamen mit new überschreiben. Das Ganze mit Extention Methods zu machen funktioniert hingegen nicht, weil die Property Channel protected ist.

Mit verhältnismäßig wenig Tipparbeit können wir jetzt doch unseren Code von oben schreiben:

var result1 = await client.DoThisTaskAsync("Hallo");
var result2 = await client.DoThatTaskAsync(result1, 43);

Die zweite Funktion wird mit dem Ergebnis der ersten aufgerufen, ganz natürlich und ohne Klimmzüge.

Das hintereinander Aufrufen von asynchronen Funktionen ist aber nur die Spitze des Eisbergs. Auch der folgende (hier inhaltlich unsinnige) Code ist möglich:

string result = null;
try
{
  var client = new Service1Client();
  if (await client.DoThatTaskAsync("xxx", 43) > 123)
    result = await client.DoThisTaskAsync("Hallo");
  else
  {
    await client.DoActionTaskAsync("yyy", 4.5, DateTime.Now);
    int x = 321;
    while ((x = await client.DoThatTaskAsync("zzz", x)) < 42)
      result += await client.DoThisTaskAsync(x.ToString("0"));
  }
}
catch (Exception ex)
{
  Debug.WriteLine(ex.Message);
}

Ein try-Block um asynchrone Funktionen, die sich innerhalb beliebiger Kontrollstrukturen befinden! Und eine Exception auf dem Server landet stets sauber beim Client im catch-Block.

Wer immer noch nicht von async/await restlos begeistert ist, sollte mal versuchen, den Code oben mit den bisherigen Mitteln zu schreiben. Insbesondere wegen des try-catch-Blocks ist dies eine sehr schwierige Übung.

Noch ein technischer Hinweis: Das Erzeugen des Task-Objektes führt zur Erzeugung eines zusätzlichen Threads, der auf das WaitHandle von IAsyncResult wartet. Dies hat aber praktisch keinerlei Laufzeitrelevanz. Der Thread wird einmal im Threadpool erzeugt und dann wiederverwendet. Die überwiegende Zeit wartet er auf die Fertigstellung der asynchronen Operation. Danach benachrichtigt er den Main-Thread, damit dieser hinter dem await weitermacht.

Fazit

„Fast und fluid“ war Silverlight ja immer schon. Mit async/await und den selbst gekapselten Task-basierten Service-Operationen haben wir nun auch in Silverlight, was mit der Windows Runtime der neue Standard werden wird. Und es funktioniert so gut, dass ich schon fast vergessen habe, wie ich es bisher ohne await gemacht habe.

Hier der Beispielcode zum Ausprobieren:

TaskBasedWebOperations.zip (61 kB)

Tags: , ,

Silverlight

Powered by BlogEngine.NET 1.6.1.0 - Impressum