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

by St. Lange 25. Mai 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

Web Services in Silverlight synchron aufrufen

by St. Lange 28. September 2009 21:18

Einen Web Service aus Silverlight heraus aufzurufen ist sehr einfach. Man importiert den Service über einen Wizard und Visual Studio generiert alle notwendigen Dateien und Funktionen. Im Gegensatz zum normalen .net Framework können die Service Funktionen aber nur asynchron aufgerufen werden. So wird verhindert, dass ein wirklich sehr unerfahrener Programmierer eine Service Funktion direkt aus dem UI Thread synchron aufruft und damit nicht nur seine Silverlight Anwendung, sondern den gesamten Browser für die Zeit des Funktionsaufrufes einfriert. An sich ja gut gemeint, aber bei ernsthaften Anwendungen ist das Fehlen von synchronen Service Aufrufen schon lästig.

Wenn man beispielsweise in einem Background Thread mehrere Web Service Aufrufe nacheinander oder in einer Schleife ausführen möchte, ist dies ebenfalls nur asynchron möglich. Man muss also in so einem Fall überflüssigerweise mit Completed Handlern arbeiten, obwohl ein synchroner Aufruf hier nicht nur ungefährlich,  sondern genau richtig wäre. Hier würde ein synchroner Aufruf zu sequenziellem und übersichtlicherem Code führen.

Wenn man sich mit Web Services gut auskennt, kann man natürlich über eine ChannelFactory seine synchronen Aufrufe selber bauen, wie es beispielsweise in dem hervorragenden Artikel von Daniel Vaughan beschrieben ist. Dies setzt aber ein tieferes Verständnis von der Architektur von Web Services voraus, das man sich genau jetzt nicht aneignen will, weil man ja gerade mit seiner ersten großen Silverlight Anwendung genug zu tun hat.

Der vom mir entwickelte einfache Lösungsweg zum synchronen Aufruf von Web Services besteht in der Klasse SyncCallHelper, die unmittelbar nachdem asynchronen Aufruf der Service Funktion den laufenden Thread so lange anhält, bis der asynchrone Aufruf beendet ist. Die Verwendung sieht beispielsweise so aus:

var syncHelper = new SyncCallHelper<SampleServiceReference.DoSomeWorkCompletedEventArgs>();
client.DoSomeWorkCompleted += syncHelper.OnCompleted;
client.DoSomeWorkAsync(syncHelper);

if (syncHelper.Wait() && syncHelper.Succeeded)
{
  // success
  var result = syncHelper.EventArgs.Result;
}

SyncCallHelper blockiert in der Funktion Wait über ein ManualResetEvent den laufenden Thread so lange, bis die Service Funktion fertig ist und OnCompleted aufgerufen hat. Natürlich darf man diesen Code nur in einem Background Thread verwenden. Falls man ihn versehentlich doch mal aus dem UI Thread heraus aufruft, wird eine Exception ausgelöst. Im UI Thread würde der Code auch gar nicht funktionieren. Weil in diesem Fall Wait den UI Thread blockiert, wird der OnCompleted Handler niemals aufgerufen, da dieser in einem Synchronization Context auf den UI Threads wartet. Es läge also ein klassischer Deadlock vor.

Hier der Quellcode von SyncCallHelper:

namespace System.ComponentModel
{
  /// <summary>
  /// Helper class for synchronized asynchron WCF service calls.
  /// </summary>
  public class SyncCallHelper<T> where T : AsyncCompletedEventArgs
  {
    /// <summary>
    /// Initializes a new instance of the <see cref="SyncCallHelper<T>"/> class
    /// and sets the event semaphore to non-signaled.
    /// </summary>
    public SyncCallHelper()
    {
      Event = new ManualResetEvent(false);
    }

    /// <summary>
    /// Blocks the current thread until the current System.Threading.WaitHandle receives a signal.
    /// </summary>
    public bool Wait()
    {
      if (Deployment.Current.CheckAccess())
        throw new InvalidOperationException("SyncCallHelper must not be used in UI thread.");

      return Event.WaitOne();
    }

    /// <summary>
    /// Blocks the current thread until the current System.Threading.WaitHandle receives a signal,
    /// using 32-bit signed integer to measure the time interval.
    /// </summary>
    public bool Wait(int millisecondsTimeout)
    {
      if (Deployment.Current.CheckAccess())
        throw new InvalidOperationException("SyncCallHelper must not be used in UI thread.");

      return Event.WaitOne(millisecondsTimeout);
    }

    /// <summary>
    /// Blocks the current thread until the current instance receives a signal,
    /// using a System.TimeSpan to measure the time interval.
    /// </summary>
    public bool Wait(TimeSpan timeout)
    {
      if (Deployment.Current.CheckAccess())
        throw new InvalidOperationException("SyncCallHelper must not be used in UI thread.");
      return Event.WaitOne(timeout);
    }

    /// <summary>
    /// Gets a value indicating whether the service call is succeeded.
    /// </summary>
    public bool Succeeded
    {
      get { return EventArgs != null && EventArgs.Error == null && !EventArgs.Cancelled; }
    }

    /// <summary>
    /// Gets the event semaphore used to wait for completion of asynchronous WCF service calls.
    /// </summary>
    public ManualResetEvent Event { get; private set; }

    /// <summary>
    /// Gets the event args that contains the result of the WCF service call.
    /// Is null until the WCF service call has completed.
    /// </summary>
    public T EventArgs { get; private set; }

    /// <summary>
    /// Called when the WCF service call has completed.
    /// </summary>
    public void OnCompleted(object sender, T e)
    {
      var res = (SyncCallHelper<T>)e.UserState;
      res.EventArgs = e;
      res.Event.Set();
    }
  }
}

Übrigens kann man mit der Funktion Deployment.Current.CheckAccess auf einfache Weise überall im eigenen Code testen, ob man sich gerade im UI Thread oder in einem Background Thread befindet.

Hier nun noch ein Beispielprogramm, dass in einem Background Thread zwei Service Funktionen hintereinander aufruft:

void worker_DoWork(object sender, DoWorkEventArgs e)
{
  // This code is executed in background thread
  string result;

  var client = new SampleServiceClient();

  // Do 1st service call with no parameters
  var syncHelper1 = new SyncCallHelper<DoSomeWork1CompletedEventArgs>();
  client.DoSomeWork1Completed += syncHelper1.OnCompleted;
  client.DoSomeWork1Async(syncHelper1);

  if (syncHelper1.Wait() && syncHelper1.Succeeded)
  {
    result = syncHelper1.EventArgs.Result;
  }
  else
  {
    e.Result = "Error";
    return;
  }

  // Do 2nd service call with one parameter
  var syncHelper2 = new SyncCallHelper<DoSomeWork2CompletedEventArgs>();
  client.DoSomeWork2Completed += syncHelper2.OnCompleted;
  client.DoSomeWork2Async(42, syncHelper2);

  if (syncHelper2.Wait() && syncHelper2.Succeeded)
  {
    result += syncHelper2.EventArgs.Result;
  }
  else
  {
    e.Result = "Error";
    return;
  }

  e.Result = result;
}

Der Code läuft wie man sieht sequentiell ab und wird nicht durch OnCompleted Handler zerstückelt.

Noch ein wichtiger Hinweis: Im Beispielcode wird ein Background Worker direkt im Event Handler eines Button Clicks in der Code Behind Datei angelegt. So darf man ausschließlich in Beispielcode programmieren, der einen bestimmten Aspekt herausstellen soll!

Fazit

Microsoft möchte verhindern, dass man mit synchronen Web Service Aufrufen den Browser vorübergehend einfriert. Das ist absolut richtig und einen gewissen Schutz vor unsauberen Programmierpraktiken vorzusehen ist auch nicht verkehrt. Eine wesentliche bessere Lösung wäre es allerdings gewesen, die synchronen Web Service Aufrufe zusätzlich anzubieten und einfach eine InvalidOperationException auszulösen, wenn man sie aus dem UI Thread heraus versucht aufzurufen.

Hier der gesamte Quellcode des Beispiels zum Downloaden:

SynchronousWcfCalls.zip (40,99 kb)

Tags: ,

Silverlight

Powered by BlogEngine.NET 1.6.1.0 - Impressum