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)

 
kick it on dotnet-kicks.de

Tags: ,

Silverlight

Comments

10/13/2009 12:47:09 PM #

BFreakout

Hallo Stefan,

was auch möglich wäre ist das verwenden von Lambda.
Somit wird eine Asynchrone-Kommunikation in einer Methode gehandhabt werden, als wäre diese Synchron:

public void GetCustomers()
{
     Proxy.GetCustomersCompleted += (sender, eventArgs) =>
        {
            // Der Bereich wird aufgerufen wenn der Asynchrone Vorgang fertig ist...  
        };
    
     Proxy.GetIntegratedUserAsync();
}

Das besondere daran ist, das bestimmte Werte die nach dem Asynchronen Vorgang noch einmal benötigt werden, zur Verfügung stehen.

Beste Grüße,
Gregor

BFreakout Germany |

11/3/2009 7:46:03 AM #

trackback

Web Services in Silverlight synchron aufrufen

Sie wurden gekickt (eine gute Sache) - Trackback von  dotnet-kicks.de

dotnet-kicks.de |

Comments are closed

Powered by BlogEngine.NET 1.6.1.0 - Impressum