Silverlight Tipp der Woche: XML Namespace Definition

by St. Lange 31. July 2010 18:06

In diesem Tipp geht es um die Verwendung des XmlnsDefinition Attributs.

Zusammenfassung

Um eigene Controls in XAML Dateien verwenden zu können, kann man eine Namespace-Definition der Form xmlns:xyz="clr-namespace:MyNamespace;assembly=MyAssembly" verwenden. Es ist jedoch einfacher eigene Controls mit Hilfe des XmlnsDefinition Attributs zu verwalten.

Beschreibung

Controls, die nicht aus dem Default Namespace http://schemas.microsoft.com/winfx/2006/xaml/presentation stammen, benötigen eine XML Namespace-Definition, um in XAML Code verwendet werden zu können. Eine solche Definition bildet einen .NET CLR Namespace auf einen XML Namespace Präfix ab. In Silverlight 2 musste man dafür im Root-Element der XAML Datei ein Attribut mit folgendem Aufbau angeben:

<UserControl xmlns:xyz="clr-namespace:MyNamespace" ...>

- oder -

<UserControl xmlns:xyz="clr-namespace:MyNamespace;assembly=MyAssembly" ...>

Über den Präfix xyz kann man dann Controls aus dem angegebenen CLR Namespace und der entsprechenden Assembly verwenden. Wächst nun ein Projekt im Laufe der Entwicklung immer weiter an, verteilen sich die selbst geschriebenen Custom- und insbesondere User-Controls meist auf mehrere Namespaces und Assemblies. Dies führt zum einen zu vielen verschiedenen Namespace-Definitionen pro XAML Datei in der sie verwendet werden und zum anderen wird auch das Refactoring immer mühsamer.

Seit Silverlight 3 gibt es nun das aus WPF bekannte Attribut XmlnsDefinition, welches auf Assembly-Ebene das Mapping von XML Namespaces auf CLR Namespaces regelt.

[assembly: XmlnsDefinition("http://mycompany.com/xaml", "MyNamespace.Controls")]
[assembly: XmlnsDefinition("http://mycompany.com/xaml", "MyNamespace.Controls.Xyz")]

Dieses Attribut kann mehrfach vorkommen und steht typischerweise in der Datei AssemblyInfo.cs. Es stellt sicher, dass alle Controls aus allen aufgeführten Namespaces in einer XAML Datei über ein und dieselbe Url angesprochen werden können. Auch wenn die Controls aus verschiedenen Assemblies stammen, die jeweils ihre eigenen XmlnsDefinition Attribute haben, reicht jetzt eine einzige Namespace-Definition:

<UserControl xmlns:abc="http://mycompany.com/xaml" ...>
<abc:MyControl ...>

Die Url selbst kann beliebig gewählt werden und hat keine weitere Bedeutung, außer dass sie eindeutig sein muss.

Über das Attribut XmlnsPrefix kann man optional noch festlegen, welches der Default Namespace-Prefix sein soll, den ein XAML Designer-Tool automatisch vergibt, wenn das erste Control per Drag & Drop zu einem User Control hinzugefügt wird.

[assembly: XmlnsPrefix("http://mycompany.com/xaml", "abc")]

Microsoft verwendet beispielsweise in Silverlight 4 den Präfix toolkit für Controls aus dem Silverilght Toolkit.

Ausnahmen

Theoretisch könnte man so vollständig auf die Schreibweise mit clr-namespace verzichten, gäbe es nicht eine Ausnahme. Wenn man in einer XAML Datei ein Control verwenden möchte, welches aus der selben Assembly stammt wie die XAML Datei, muss für dieses Control eine clr-namespace basierte Definition verwendet werden. Oder anders ausgedrückt: Die Url basierte Namespace Definition funktioniert nur für Controls, die aus referenzierten Assemblies stammen. Controls aus der eigenen Assembly werden leider nicht gefunden und ein Fehler angezeigt. Meiner Ansicht nach ist das ein Bug im XAML Parser, da ich keinen Grund sehe, warum dies "by Design" so sein sollte.

Hier geht's zum nächsten Tipp.

Tags:

Silverlight

Silverlight Tipp der Woche

by St. Lange 31. July 2010 18:05

In mehr als zwei Jahren Einsatz von Silverlight in ganz unterschiedlichen Projekten habe ich jede Menge Erfahrungen gesammelt und hatte viele große und kleine Probleme zu lösen. Wenn ich davon erzähle, höre ich oft, dass auch andere einen nicht unerheblichen Teil ihrer Zeit mit dem "Herausfinden von Dingen" verbringen. Daraus ist nun die Idee entstanden, die interessantesten Erfahrungen aufzuschreiben und hier regelmäßig als Silverlight Tipp der Woche zu veröffentlichen.

Der Aufbau der Tipps wird so gestaltet, dass es für den Leser möglichst einfach ist zu entscheiden, ob die jeweilige Information für ihn von Interesse ist oder nicht. Daher gibt es jeweils am Anfang eine nur wenige Sätze lange Zusammenfassung, in der die wesentliche Aussage des Tipps beschrieben wird. Danach kommen dann weitere Erläuterungen, sowie ggf. Hintergrundinformationen und Links.

Hier geht's zum ersten Tipp.

Tags:

Silverlight

bonn-to-code.net – Blend für Nichtdesigner

by St. Lange 24. June 2010 15:12

Zu meinem Vortrag „Blend für Nichtdesigner“ am 22.06.2010 bei der .NET User Group für Bonn und Umgebung finden sich hier die Materialien.

In diesem Vortrag ging es um den Nutzen von Blend 4 speziell für den Software-Entwickler. Anhand vieler Beispiele wurde gezeigt, dass Blend keineswegs nur ein Werkzeug für Grafiker ist, sondern auch für Entwickler praktisch gleichberechtigt neben Visual Studio 2010 stehen sollte.

Da der überwiegende Teil des Vortrags mit Blend durchgeführt wurde, sind die Slides eigentlich wenig hilfreich, aber auf mehrfachen Wunsch stehen sie hier dennoch zum Download bereit.

Die am Ende gezeigte Anwendung von der Mix 2010 kann mir hier downloaden: Dynamic Layout and Transitions in Blend 4

Materialien zu diesem Vortrag:

Tags:

Silverlight | WPF

Transparenz-Simulation in Silverlight

by St. Lange 25. May 2010 17:55

Anhand von zwei Projekt-Beispielen wird die Anwendung von nicht ganz alltäglichen Transparenz-Effekten vorgestellt.

1. Transparentes Video

Die Vorgabe der Grafikerin klang eigentlich ganz einfach: Das zu bewerbende Produkt sollte aus sich bewegendem Nebel herausragen und dabei aber noch teilweise mehr oder weniger vom Nebel verdeckt werden. Da Nebel an sich ja durchsichtig ist, sollte dies eigentlich kein Problem sein.

Der Nebel lag in Form eines HD Videos von real gefilmtem Nebel vor, das Bild vom Produkt war ein freigestelltes PNG. Nun ist aber ein Video von Nebel etwas anderes als realer Nebel. Natürlich kann man der Property Opacity des Media-Elements, welches den Nebel abspielt, einen Wert kleiner eins geben. Damit erhält man zwar ein durchsichtiges Video, aber nicht den gewünschten Effekt. So einfach geht es leider nicht.

Das folgende Bild zeigt einen Screenshot der fertigen Seite. Durch Anklicken kommt man auf die echte Seite.

Die Lösung besteht darin, das Produktbild vor das Video zu legen und es mit Hilfe einer OpacityMask nach unten hin immer durchsichtiger werden zu lassen. Hier ein Auszug des dazugehörigen XAML-Codes:

<MediaElement x:Name="fog1" Source="http://localhost/newblonde/assets/NewBlondeFog.wmv" AutoPlay="True" Stretch="UniformToFill"/>
<SSME:SmoothStreamingMediaElement x:Name="fog2" SmoothStreamingSource="http://localhost/newblonde/assets/NewBlondeFog.ism/Manifest" AutoPlay="True" Stretch="UniformToFill"/>
...
<Image x:Name="imgPackShot" Height="501" Width="356" Canvas.Left="321" Canvas.Top="94" Source="Assets/packshot.png" Stretch="Fill" RenderTransformOrigin="0.5,0.5">
  <Image.OpacityMask>
    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
      <GradientStop Color="White" Offset="0"/>
      <GradientStop Color="#7FFFFFFF" Offset="0.33"/>
      <GradientStop Color="Transparent" Offset="1"/>
    </LinearGradientBrush>
  </Image.OpacityMask>
</Image>

Die ersten beiden Elemente sind zwei verschiedene Videoplayer. Der erste ist der Standardplayer für WMV-Dateien, während der zweite aus dem Smooth Streaming SDK stammt. Durch Silverlight Smooth Streaming kann wahlweise eine höhere Qualität des Nebel-Videos erreicht werden. Welcher der beiden Player verwendet wird, kann per Konfiguration festgelegt werden.

Das dritte Element ist das Foto des Produktes. Die OpacityMask sorgt dafür, das im unteren Bereich des Bildes mehr und mehr vom dahinterliegenden Nebel zu sehen ist. Beim Betrachter entsteht so die Illusion, das Produkt schwebe mitten im Nebel. Durch eine leichte, mittels PowerEase gedämpfte Bewegung des Bildes wird dieser Effekt noch unterstrichen. Es ist schon erstaunlich: Obwohl ich es gemacht habe, kann ich nicht "sehen", dass sich der Nebel in Wirklichkeit hinter dem Bild befindet.

Der Nebel

Das Nebel-Video ist übrigens wie folgt gemacht: Das von der Agentur gelieferte ca. 25 Sekunden lange HD Video habe ich mit Sony Vegas so bearbeitet, dass die letzten 5 Sekunden mit dem Anfang per Kreuzblende überlagert wurden. Damit sehen der letzte Frame und der Frame aus Sekunde 5 des Originalvideos identisch aus. Das so entstandene neue und um 5 Sekunden kürzere Video kann nun in einer Endlosschleife abgespielt werden und der Sprung an den Anfang ist dabei fast nicht zu bemerken.

 

2. Transparente Silverlight Anwendung

Hier bestand die Aufgabe darin, ein mit Silverlight animiertes Firmenlogo auf einer normalen HTML-Seite anzuzeigen. Eine Silverlight-Anwendung von beispielsweise 150 x 150 Pixeln Größe zu erstellen, in der ein Logo animiert wird, ist trivial. Das Problem bestand darin, durch die Anwendung hindurch das Hintergrundbild der darunterliegenden HTML-Seite sehen zu können.

Die einfachste Lösung ist, die Silveright-Anwendung auf windowless zu schalten. Dies sollte jedoch gerade nicht gemacht werden, da dieser Modus möglicherweise nicht bei allen Browsern einwandfrei funktioniert.

Eine alternative Lösung besteht darin, dass die Silverlight-Anwendung das gleiche Hintergrundbild wie die HTML-Seite lädt und dann exakt den Ausschnitt davon anzeigt, den sie selbst gerade verdeckt. Die folgende Test-Anwendung zeigt, wie man das im Prinzip machen kann:

Das Bild mit dem Baum soll den Hintergrund einer HTML-Seite darstellen. Die eigentliche Silverlight-Anwendung ist zur Verdeutlichung mit gelben Punkten umrandet. Durch Anklicken kann die Umrandung ab und auch wieder angeschaltet werden.

Als Logo Beispiel dient hier ein Dodekaeder, dessen Seiten mit einem teiltransparenten ImageBrush gefüllt sind. 5 Sekunden nach dem Laden der Seite beginnt der Dodekaeder sich zu drehen. Nach weiteren 15 Sekunden fängt die Silverlight-Anwendung an, sich zusätzlich noch auf dem Hintergrund herumzubewegen.

Es entsteht dabei die Illusion, die Silverlight-Anwendung sei transparent, obwohl sie in Wirklichkeit nur geschickt ihren Hintergrund zeichnet.

Das div Element, welches das Silverlight object Element enthält, liegt wiederum in einem div mit dem background Bild:

<div style="height:375px; width:600px; background:url(http://.../background.jpg)">
  <div id="silverlightControlHost" style="height:150px; width:150px; left:90px; top:50px;
      position:relative">
    <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
       width="150" height="150">

Dieses div mit dem Namen "silverlightControlHost" sorgt dafür, dass sich die Anwendung 90 Pixel vom linken und 50 Pixel vom oberen Rand entfernt befindet. Hier nun der XAML-Code der eigentlichen Anwendung:

<UserControl x:Class="PseudoTransparentBackground.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:PseudoTransparentBackground" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" 
    Height="150" Width="150">
  <Grid x:Name="LayoutRoot" Cursor="Hand">
    <Grid.Background>
      <ImageBrush ImageSource="http://.../background.jpg"
          Stretch="None" AlignmentX="Left" AlignmentY="Top">
        <ImageBrush.Transform>
          <CompositeTransform x:Name="Offset" TranslateX="-90" TranslateY="-50"  />
        </ImageBrush.Transform>
      </ImageBrush>
    </Grid.Background>
    <local:DodecahedronLogo RenderTransformOrigin="0.5 0.5" >
      <local:DodecahedronLogo.RenderTransform>
        <ScaleTransform ScaleX="0.25" ScaleY="0.25"/>
      </local:DodecahedronLogo.RenderTransform>
    </local:DodecahedronLogo>
    <Rectangle x:Name="ControlBorder" Stroke="Yellow" StrokeThickness="4" StrokeDashArray="0 2"
        StrokeDashCap="Round" StrokeDashOffset="0.5" />
  </Grid>
</UserControl>

Das selbe Hintergrundbild wie aus der HTML-Seite wird über eine ImageBrush als Background verwendet. Dabei wird das Bild über eine CompositeTransform entsprechend so verschoben, dass es den gleichen Ausschnitt zeigt wie die Stelle, die die Anwendung momentan verdeckt.

Damit habe ich erreicht, eine nicht-windowless Silverlight-Anwendung mit einem scheinbar transparenten Hintergrund zu erstellen.

Noch mehr Bewegung

Nur so zum Ausprobieren habe ich dann auch noch die Position der Silverlight-Anwendung animiert. Dazu wurde eine neue Klasse AppPosition mit zwei Dependency Properties Left und Top angelegt. Mittels zweier DoubleAnimation Objekte werden Left und Top animiert und so die Anwendung immer wieder auf neue Zufallspositionen innerhalb der Grenzen des Hintergrundbildes bewegt.

Damit nun die Silverlight-Anwendung ihre eigene Position im Browserfenster verändern kann, verwendet sie die Funktion SetStyleAttribute der Klasse HtmlElement. Hier der Code, der aufgerufen wird, wenn sich beispielsweise der Wert der Property Left ändert:

private static void LeftChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  int x = Convert.ToInt32((double)e.NewValue);
  MainPage.Offset.TranslateX = -x;
  SilverlightControlHost.SetStyleAttribute("left", String.Format("{0}px", x));
}

Zum einen wird die Property TranslateX aus dem ImageBrush angepasst, zum anderen das Attribut left des Styles des SilverlightControlHost div Elements. Analoges gilt für die Property Top. Diesen Effekt hätte man vielleicht auch mit JavaScript erreichen können, aber mit Silverlight fiel mir das Programmieren deutlich leichter.

Im Internet Explorer 8 funktioniert diese Bewegung übrigens ganz gut, während der Hintergrund im Firefox 3.6 und in Chrome 4.1 relativ stark zittert. Im aktuellen Internet Explorer 9 Platform Preview sieht es am geschmeidigsten aus.

Für alle, die es sich genauer anschauen wollen, hier der Quellcode zum Downloaden:

PseudoTransparentBackground.zip (235 kB)

(Hinweis: Vor dem Build muss möglicherweise die Datei Petzold.Media3D.dll über Eigenschaften/Sicherheit zur Verwendung zugelassen werden.)

Tags:

Silverlight

BASTA! 2010 Spring – Anwendungsarchitektur für WPF und Silverlight

by St. Lange 25. February 2010 13:30

Zu meinem Vortrag „WPF und Silverlight Architektur“ auf der BASTA! Spring finden sich hier die Materialien.

In diesem Vortrag ging es um die Architektur von WPF und Silverlight Anwendungen. Es wurden Möglichkeiten zur Strukturierung von Anwendungen vorgestellt und auch gezeigt, wie man Code zwischen WPF und Silverlight gemeinsam nutzen kann.

Der Beispielcode liegt in C# und VB.NET vor. An dieser Stelle ein großes Dankeschön an Thomas, der mir bei der Erstellung des VB Codes kräftig geholfen hat.

Materialien zu diesem Vortrag:

Tags:

Silverlight | WPF | Architecture

BASTA! 2010 Spring – Blend für Nichtdesigner

by St. Lange 24. February 2010 20:00

Zu meinem Vortrag „Blend für Nichtdesigner“ auf der BASTA! Spring finden sich hier die Materialien.

In diesem Vortrag ging es um den Nutzen von Blend für den Software Entwickler beispielsweise beim Erstellen von Data Templates oder beim Designen von Custom Controls. Da der überwiegende Teil des Vortrags mit Blend durchgeführt wurde sind die Slides eigentlich wenig hilfreich, der Vollständigkeit halber aber stehen sie hier zum Download bereit.

Materialien zu diesem Vortrag:

Tags:

Silverlight | WPF

SmartBackgroundWorker

by St. Lange 9. January 2010 13:01

Eine alternative Implementierung der Klasse BackgroundWorker erlaubt es, in ein und derselben Funktion beliebig zwischen UI- und Worker-Thread hin und her zu wechseln. Die verblüffende Implementierung führt zu sehr übersichtlichem Code ganz ohne Callbacks und Delegates.

Schon seit .NET 1.0 leistet die Klasse System.ComponentModel.BackgroundWorker gute Dienste, wenn es darum geht, zeitintensive Funktionen von einem Hintergrund-Thread ausführen zu lassen, damit das Userinterface nicht vorübergehend eingefroren wird. Über die drei Events DoWork, ProgressChanged und RunWorkerCompleted kann man sich über den Fortschritt der Ausführung informieren lassen. So weit, so gut. Wird die Aufgabenstellung komplizierter, beispielsweise weil man während der Hintergrundausführung die Oberfläche mit den bisher berechnenden Zwischenergebnissen aktualisieren oder mehrere Aufgaben hintereinander ausführen möchte und dabei ggf. Fehlermeldungen anzeigen möchte, wird der benötigte Code schnell recht unübersichtlich. Das liegt daran, dass man bei den meisten nicht trivialen Problemen immer eine einfache State Machine (einen Endlichen Automaten) bauen muss, welche(r) die Aktionen des UI-Threads und des Worker-Threads synchronisiert. Die benötigten Delegates, Lambda-Expressions und Synchronization-Contexte führen schnell zu schwer lesbarem und somit auch schwer wartbarem Code.

Wäre es nicht schön, wenn man ganz einfach sequenziellen Code und einfache Schleifen in einer übersichtlichen Funktion „einfach so“ hinschreiben könnte und dabei für einzelne Codeabschnitte jeweils angeben könnte, ob sie im UI-Thread oder im Background-Thread ausgeführt werden sollen?  Wäre nett, geht aber nicht, hätte ich gesagt, bevor ich letztes Jahr den Kurzvortrag von Ralf Hoffmann bei einem Treffen der Bonner .NET Usergroup gesehen habe. Ralf verwendet den yield-Befehl für eine erstaunliche Lösung des beschriebenen Problems. Da Ralf keinen Blog betreibt, der Ansatz aber sehr nützlich ist, möchte ich seine Idee hier vorstellen.

Beispielanwendung sdddd

Zur Demonstration des Verfahrens nehmen wi xxxr an, wir wollen irgendwelche Items berechnen und eine Listbox damit füllen. Das Berechnen eines jeden Items dauert so lange, dass es in einem Background-Thread ausgeführt werden muss. Jedes Item soll jedoch sofort angezeigt werden, wenn es berechnet wurde und nicht erst am Ende des Vorgangs, wenn alle Ergebnisse vorliegen. Außerdem soll die Berechnung über das UI abgebrochen werden können, wenn es dem Anwender zu lange dauert.

Hier das Beispiel als Silverlight-Anwendung. Die Berechnung jedes Items dauert eine knappe Sekunde und lastet dabei den Worker-Thread voll aus, wie ein Blick in den Task-Manager zeigen kann. Der Browser und die Silverlight-Anwendung werden davon aber nicht blockiert. Der Vorgang kann jederzeit über einen Button abgebrochen und wieder neu gestartet werden.

Die zugehörige Worker-Funktion aus der Codebehind-Datei sieht wie folgt aus:

/// <summary>
/// A simple function that gets alternating executed by
/// the UI-thread and a background-thread.
/// </summary>
IEnumerator<SwitchTo> MyWorkerFunktion(string someParameter, int someOtherParameter)
{
  // At start we run in the background thread, so switch to UI-thread
  // to update the StartStopButton and clear the ListBox
  yield return SwitchTo.UIThread;
  StartStopButton.Content = "Stop";
  Listbox.Items.Clear();

  // Now switch to background thread and start the loop
  yield return SwitchTo.BackgroundThread;
  for (int idx = 1; idx <= 25; idx++)
  {
    // Get the next item
    string item = DoSomeHardWorkToGetAnItem(idx);

    // Switch to UI-thread to update the list-box
    yield return SwitchTo.UIThread;
    Listbox.Items.Add(item);

    // Check if user pressed the Stop button
    if (_stop)
    {
      Listbox.Items.Add("<Operation was canceled by user>");
      _stop = false;
      break;
    }

    // Switch back to background thread for next loop iteration
    yield return SwitchTo.BackgroundThread;
  }

  // We have done and reset Start/Stop button in UI-thread
  yield return SwitchTo.UIThread;
  StartStopButton.Content = "Start";
}

Unglaublich, oder? Über den Enum-Typ SwitchTo wird im yield return angegeben, ob der nächste Codeabschnitt im UI- oder im Background-Thread ausgeführt werden soll.  Der Code ist praktisch selbstdokumentierend und sehr gut lesbar.

Aufgerufen wird diese Funktion beim Drücken des Start-Buttons wie folgt:

_smartBackgroundWorker.RunWorkerAsync(MyWorkerFunktion("Hello", 42));

Die Parameter der Woker-Funktion werden im Beispiel nicht gebraucht und sollen nur andeuten, dass man ohne weiteres beliebige Parameter übergeben kann.

Warum funktioniert das?

Um den SmartBackgroundWorker zu verwenden, muss man nicht in allen Einzelheiten verstehen, warum bzw. wie er funktioniert. Für Interessierte hier eine knappe Zusammenfassung der internen Struktur.

Der in C# 2 eingeführte yield-Befehl kann in Funktionen verwendet werden, die IEnumerator als Rückgabewert haben. Zu einer solchen Funktion generiert der Compiler eine passende private Klasse, die u.a. von IEnumerator  abgeleitet ist und daher auch die Funktion MoveNext implementieren muss. Der ursprüngliche Code mit den yield return-Anweisungen wird nun vom Compiler in eine State Machine konvertiert, die beim Aufruf von MoveNext jeweils in den nächsten Zustand übergeht. Als Effekt davon führt ein Durchlaufen des Iterators mit MoveNext dazu, dass jeweils genau die Statements bis zum nächsten (ursprünglichen) yield return ausgeführt werden (bzw. der Durchlauf beendet wird). Der generierte Code von MoveNext kann dabei sehr umfangreich und verworren werden, wie ein Blick darauf mit dem .NET Reflector zeigt. Diese Komplexität stört aber nicht weiter, da der Code ja vom Compiler unsichtbar und zuverlässig generiert wird.

Durch den Einsatz von yield return haben wir also erreicht, dass der Code unserer Worker-Funktion abschnittsweise ausgeführt wird. Wir müssen nun nur noch dafür sorgen, dass zwischen den einzelnen Aufrufen ggf. der ausführende Thread gewechselt wird. Dies wird innerhalb von SmartBackgroundWorker in der Funktion OnRun durchgeführt.

/// <summary>
/// Steps through the worker function.
/// </summary>
void OnRun(object argument)
{
  try
  {
    SwitchTo context = SwitchTo.BackgroundThread;
    var enumerator = (IEnumerator<SwitchTo>)argument;
    bool moveNext = true;
    SendOrPostCallback nextStep = obj =>
                                    {
                                      moveNext = enumerator.MoveNext();
                                      if (moveNext)
                                        context = enumerator.Current;
                                    };
    while (moveNext)
    {
      if (context == SwitchTo.UIThread)
      {
        // Run next step synchronously on UI thread
        _uiSynchronizationContext.Send(nextStep, null);
      }
      else
      {
        // Run next step on background thread
        nextStep(null);
      }
    }
  }
  finally
  {
    _isRunning = false;
  }
}

Ein SmartBackgroundWorker merkt sich im Constructor den SynchronizationContext des UI-Threads und muss daher immer im UI-Thread angelegt werden. Beim Start der Worker-Funktion über RunWorkerAsync wird diese über ein Delegate an einen Thread aus dem ThreadPool gebunden. Bis hierher ist die Implementierung identisch mit dem .NET Typ BackgroundWorker. Der Thread aus dem Pool startet dann in obigem OnRun, die den Unterschied zu BackgroundWorker ausmacht.

In der Variablen nextStep wird der Code zum Durchlaufen des nächsten Iterator-Schritts gespeichert. Die Variable context speichert den Rückgabewert von yield return, also entweder UIThread oder BackgroundThread.

In der while-Schleife wird über den Wert von context unterschieden, in welchem Thread der nächste Codeabschnitt aufgerufen werden soll. Ist es der Background-Thread, wird nextStep direkt aufgerufen, denn OnRun läuft ja bereits im Worker-Thread. Ist es der UI-Thread, wird nextStep mit der Funktion Send von SynchronizationContext an den UI-Thread „gesendet“. Technisch passiert dabei folgendes: Durch Send wird eine Art „spezielles Event“ in die Event-Queue des UI-Threads eingereiht. Ist dieses Event an der Reihe, wird der Code in nextStep durch den UI-Thread ausgeführt und somit auch der nächste Abschnitt in unserer Worker-Funktion. Bis diese Ausführung abgeschlossen ist, blockiert der Background-Thread, d.h. der Funktionsaufruf von Send kommt erst dann zurück, wenn nextStep vom UI-Thread vollständig ausgeführt wurde.

Mit etwas Abstand betrachtet liegt also die Innovation von SmartBackgroundWorker in Folgendem: Anstatt einen normalen BackgroundWorker zu verwenden und selber eine State Machine zu bauen, die zwischen dem UI-Thread und einem Background-Thread jongliert, schreibt man relativ linearen Code zusammen mit yield return und lässt den Compiler die passende State Machine generieren.

Coole Sache. Wie schon gesagt, ich habe es nicht erfunden, sondern meine Implementierung nur aus Code von Ralf Hoffmann abgeleitet, der wiederum von einem Screencast von Jeffrey Richter inspiriert wurde (vermutlich diesem hier).

Bewertung

Der Einsatz von SmartBackgroundWorker ist nicht auf Silverlight beschränkt, sondern funktioniert mit WPF oder WinForms genauso. Ich habe nur deshalb Silverlight verwendet, damit ich die Demo direkt in diesen Artikel im Blog einbauen kann.

Im letzten Jahr haben wir den SmartBackgroundWorker in verschiedenen Silverlight-Anwendungen verwendet und er hat sich als sehr nützlich erwiesen. Hier meine persönliche pro/contra-Liste.

Vorteile

  • Leicht zu verwenden
  • Keine zusätzlichen Eventhandler, Delegates oder Lambdas notwendig
  • Führt zu übersichtlichem und intuitiv nachvollziehbarem Code
  • Worker-Funktionen lassen sich sehr leicht verketten bzw. ineinander verschachteln

Nachteile

  • Exception-Handling in der Worker-Funktion kann unübersichtlich werden, da yield nicht innerhalb von try/catch verwendet werden kann
  • In VB.NET nicht verwendbar, da diese Sprache nicht über ein yield-Statement verfügt (was aber eher eine Einschränkung der Sprache ist)

Insgesamt kann ich den SmartBackgroundWorker sehr empfehlen.

Fazit

Man kann sich natürlich fragen, ob das Ganze wirklich eine gute Idee oder nur ein Hack ist, da ja der yield-Befehl für etwas missbraucht wird, für das er nicht erfunden wurde. Meiner Meinung nach ist es eine sehr gute Idee, denn es ist eine saubere Lösung für ein konkretes Problem. Und dies ist das Einzige, worauf es letztlich ankommt, wenn man den Nutzen einer Idee bewerten will. Und im Vergleich zu anderen Innovationen finde ich die Schreibweise der Worker-Funktion sogar sehr elegant. Die Parallel-Extensions von .NET 4.0 sind beispielsweise auch sehr nützlich und innovativ, aber der Code, den man teilweise schreiben muss, ist doch arg gewöhnungsbedürftig. Aber dies ist ein anderes Thema.

Hier der Quellcode zum Downloaden:

SmartBackgroundWorker.zip (7,77 kB)

Tags:

.net | Silverlight | WPF

Benutzerdefinierte Animationen in Silverlight

by St. Lange 9. November 2009 22:18

Bei einer Silverlight Applikation ergab sich kürzlich die Aufgabe, einen bestimmten Bereich über eine Art „Maximize Button“ so zu vergrößern, dass er sich über den ganzen im Browser zur Verfügung stehenden Platz erstreckt. Hier zunächst ein Beispiel, wie es funktionieren soll:

Beispiel ohne Animation

Die äußeren Bereiche sind hier im Beispiel nicht ganz auf 0 verkleinert, um den Effekt deutlicher zu machen. Die Implementierung ist einfach: Die Row- und ColumnDefinition Objekte werden per Code einfach mit neuen GridLength Objekten umkonfiguriert, so dass der Content Bereich den gesamten verfügbaren Platz einnimmt.

void Maximize()
{
  const int zero = 15;  // set to non-zero for demonstration purposes only

  LayoutRoot.ColumnDefinitions[0].Width = new GridLength(zero, GridUnitType.Pixel);
  LayoutRoot.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
  LayoutRoot.ColumnDefinitions[2].Width = new GridLength(zero, GridUnitType.Pixel);

  InnerLayout.RowDefinitions[0].Height = new GridLength(zero, GridUnitType.Pixel);
  InnerLayout.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
  InnerLayout.RowDefinitions[2].Height = new GridLength(zero, GridUnitType.Pixel);
  InnerLayout.ColumnDefinitions[0].Width = new GridLength(zero, GridUnitType.Pixel);
  InnerLayout.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
}

void Restore()
{
  LayoutRoot.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
  LayoutRoot.ColumnDefinitions[1].Width = new GridLength(700, GridUnitType.Pixel);
  LayoutRoot.ColumnDefinitions[2].Width = new GridLength(1, GridUnitType.Star);

  InnerLayout.RowDefinitions[0].Height = new GridLength(100, GridUnitType.Pixel);
  InnerLayout.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
  InnerLayout.RowDefinitions[2].Height = new GridLength(30, GridUnitType.Pixel);
  InnerLayout.ColumnDefinitions[0].Width = new GridLength(180, GridUnitType.Pixel);
  InnerLayout.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
}

Für den Anwender sieht das Umschalten aber nicht besonders attraktiv aus. Der zu vergrößernde Bereich sollte sich beim Maximieren nicht schlagartig ausdehnen, sondern animiert anwachsen. Entsprechendes gilt natürlich auch für ein späteres Restore in die Ursprungsgröße.

Die Aufgabe ist nun nicht ganz so einfach, da eine Animation von Row/ColumDefinition weder in Silverlight noch in WPF vorgesehen ist. Das zu animierende Objekt ist der Value Type GridLengh, für den es keine im Framework vorhandene Animation gibt. In WPF könnte man nun eigene Animation GridLengthAnimation implementieren, die man von AnimationTimeline ableitet. Wie das geht wurde schon sehr oft erklärt.

In Silverlight ist die Ableitung eigener Animationen weder vorgesehen noch möglich. Von der abstrakten Klasse Timeline sind 7 fest definierte Animationen sowie die Klasse Storyboard abgeleitet; alle Ableitungen sind sealed. Diese vordefinieren Animationen sind in Silverlight lediglich Wrapperklassen. Die eigentliche Arbeit wird von der darunterliegenden Silverlight Runtime ausgeführt, was mit ziemlicher Sicherheit aus Performance Gründen so gemacht wurde. Leitet man probeweise eine eigene Klasse von Animation ab und fügt sie in einen Storyboard ein, erhält man sofort eine COM Runtime Exception. So einfach geht es also nicht.

Alternativen

Zunächst einmal sollte man sich natürlich fragen, ob es für ein gegebenes Problem wirklich notwendig ist eine eigene Animation zu implementieren. Bei meinem konkreten Maximize/Restore Problem könnte ich ja auch anders vorgehen: Anstatt die Breiten und Höhen von Zeilen und Spalten verschiedener Grids zu animieren, wäre es auch möglich, überall den GridUnitType auf Auto zu setzen und in die entsprechenden Grid Zellen Border Objekte packen. Border Objekte besitzen die von FrameworkElement geerbten Properties Width und Height, die ganz normal mittels DoubleAnimation animiert werden können. Da die Zeilen und Spalten der Grid Objekte auf Auto stehen und sich der Größe der darin liegenden Border Objekte anpassen, führt eine Animation der Border Objekte letztlich zu einer Animation des Gesamtlayouts. Der große Nachteil dabei ist aber, dass man dann das gesamte Layout selber berechnen muss, denn so etwas Praktisches wie GridUnitType.Star stünde einem dann nicht zur Verfügung. Also scheint es doch sinnvoll zu sein, ein wenig über die Animation von GridLength Objekten nachzudenken.

Lösungsansatz in Silverlight

Glücklicherweise konnte ich mich an einen Artikel von Charles Petzold erinnern, in dem er eines seiner Beispiele aus seinem WPF Buch auf Silverlight ans Laufen gebracht hat. Er demonstriert darin, wie man die WPF Animation MatrixAnimationUsingPath unter Silverlight nachbaut (siehe hier).

Meine Klasse MaximizeRestoreAnimation funktioniert nach dieser Vorlage. Die Grundidee besteht darin, sich in das Rendering Event der Klasse CompositionTarget einzuklinken. Dieser Event wird vor dem Rendern jedes einzelnen Frames aufgerufen und dient zur Berechnung des nächsten Animationsschrittes. Hier ein Quellcodeauszug:

void OnCompositionTargetRendering(object sender, EventArgs e)
{
  // Comes here for every rendered frame, so don't waste performance if nothing is to animate
  if (!(_startAnimation || _running))
    return;

  // Prevent division by zero
  if (!Duration.HasTimeSpan)
    return;

  // Calculate progress using the storyboard Ticks
  double progress = (double)Storyboard.GetCurrentTime().Ticks / Duration.TimeSpan.Ticks;

  if (_startAnimation)
  {
    // switch to running
    _startAnimation = false;
    _running = true;
  }

  if (_maximized)
    AnimateMaximize(progress);
  else
    AnimateRestore(progress);
}

Mit Hilfe eines Storyboards wird der Fortschritt der Animation als eine Zahl zwischen 0 und 1 berechnet. Diese Zahl dient dann zum Skalieren der Breiten und Höhen der Grid Rows/Columns. Petzolds Trick besteht darin, so viel wie möglich von der vorhandenen Silverlight Infrastruktur zu nutzen. So wird beispielsweise mit Storyboard.GetCurrentTime().Ticks der aktuelle Fortschritt der Animation berechnet.

Da die Startwerte der Animationen von der aktuellen Größe des Browser Fensters abhängen, muss man diese vor dem ersten Animationsschritt zunächst ermitteln. Außerdem muss man für bestimmte Elemente den GridUnitType ändern.

void StartMaximize()
{
  _lrc0Width = MainPage.LayoutRoot.ColumnDefinitions[0].ActualWidth;
  _lrc2Width = MainPage.LayoutRoot.ColumnDefinitions[2].ActualWidth;

  MainPage.LayoutRoot.ColumnDefinitions[0].Width = new GridLength(_lrc0Width);
  MainPage.LayoutRoot.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
  MainPage.LayoutRoot.ColumnDefinitions[2].Width = new GridLength(_lrc2Width);

  MainPage.InnerLayout.RowDefinitions[0].Height = new GridLength(100);
  MainPage.InnerLayout.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
  MainPage.InnerLayout.RowDefinitions[2].Height = new GridLength(30);
  MainPage.InnerLayout.ColumnDefinitions[0].Width = new GridLength(180);
  MainPage.InnerLayout.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
}

Nun kann bei jedem Redering Event die Größe der einzelnen Grids entsprechend skaliert werden. Die Animation sieht dabei insgesamt noch etwas geschmeidiger aus, wenn man den linearen Zeitverlauf durch eine Ease Funktion etwas modifiziert. Mit einem PowerEase Objekt wird die Bewegung kurz vor Erreichen der Endposition leicht abgebremst. Hier exemplarisch der Code für AnimateMaximize:

void AnimateMaximize(double progress)
{
  var factor = 1 - new PowerEase { EasingMode = EasingMode.EaseOut, Power = 3 }.Ease(progress);

  MainPage.LayoutRoot.ColumnDefinitions[0].Width = new GridLength(factor * _lrc0Width);
  MainPage.LayoutRoot.ColumnDefinitions[2].Width = new GridLength(factor * _lrc2Width);

  MainPage.InnerLayout.RowDefinitions[0].Height = new GridLength(factor * 100);
  MainPage.InnerLayout.RowDefinitions[2].Height = new GridLength(factor * 30);
  MainPage.InnerLayout.ColumnDefinitions[0].Width = new GridLength(factor * 180);
}

Für Restore ist die Implementierung analog.

Hier das Endergebnis zum Ausprobieren

Fazit

Zwar ist die Lösung letzlich ein Hack, funktioniert aber wesentlich besser als beispielsweise ein Rumtricksen mit einem DispatcherTimer. Der Animationscode ist komplett in einer eigenen Klasse versteckt und zieht sich nicht durch die Code Behind Datei. Außerdem werden durch die Verwendung des Rendering Events exakt so viele Animationsschritte berechnet, wie Silverlight in der entsprechenden Zeit Frames rendert. Das sorgt für einen ruckelfreien Verlauf.

Ein weiterer Vorteil in der direkten Animation der GridLenth Objekte liegt darin, dass keinerlei besondere Vorkehrungen im XAML Code gemacht werden müssen. So kann die Animation auch nachträglich in eine bestehende Anwendung eingebaut werden und es müssen keine Hilfsobjekte (wie Border oder etwas vergleichbares) nur zum Zweck der Animation eingeführt werden.

Das gezeigte Verfahren lässt sich leicht auf andere Situationen anpassen, bei denen Datentypen animiert werden müssen, für die es keine vordefinierten Animation gibt.

Hier der gesamte Quellcode zum Downloaden:

SilverlightGridAnimation.zip (16,10 kb)

Tags:

Silverlight

DataTemplate Selector für Silverlight

by St. Lange 9. October 2009 21:09

Data Templates sind eine feine Sache. Sie erlauben es, die Präsentation von Daten sehr einfach und präzise zu definieren. Hierzu vorab ein einfaches Beispiel in Form einer Silverlight Listbox, die die 16 deutschen Bundesländer auflistet. Standardmäßig ist das Data Template für die Items einer Listbox einfach ein ContentPresenter. Dieser ruft an den Items die Funktion ToString auf und stellt den resultierenden Text dar. Durch das Zuweisen eines Data Templates wird aus dem simplen String eine viel informativere Darstellung.

Hier kann man das Beispiel ausprobieren

In diesem Beispiel wird beim Drücken von „Apply Template“ ein Data Template geladen und an die Property ItemTemplate der Listbox zugewiesen. Dies hat zur Folge, dass die Listbox zur Darstellung ihrer Items nicht mehr einfach nur den Default Content Presenter verwendet, sondern ein individuell auf die Daten zugeschnittenes Template. Diese Funktionalität ist in WPF und Silverlight nahezu identisch.

WPF bietet nun zusätzlich noch die Möglichkeit, das Template abhängig von Daten Items individuell auszuwählen. Dazu muss man eine eigene Klasse MyTemplateSelector von DataTemplateSelector ableiten und darin die Funktion SelectTemplate überschreiben. Eine Listbox hat wie jedes Items Control in WPF die Property ItemTemplateSelector. Dieser Property muss man MyTemplateSelector zuweisen. Die Listbox ruft nun für jedes Daten Item was sie in Ihrer Items Source findet die Funktion SelectTemplate in MyTemplateSelector auf. So hat man die Möglichkeit, im Code abhängig von den Daten jedes einzelnen Items unterschiedliche Templates auszuwählen.

Lösung in Silverlight

Silverlight 3 kennt diese WPF Funktionalität nicht, aber man kann sich eine vergleichbare Funktionalität einfach selber bauen. Da Silverlight für alle Daten Items immer das gleiche Template verwendet, muss man einen Trick anwenden: Man baut sich ein spezielles Data Template, welches genau nur ein Content Control beinhaltet. In diesem Content Control redefiniert man die Funktion OnContentChanged und wechselt hier einfach das Data Template des Content Controls abhängig vom aktuellen Content. Diese zusätzliche Indirektion liefert im Ergebnis das gleiche wie in WPF.

Ein Beispiel soll zeigen, wie es im Einzelnen funktioniert. Um es einfach zu halten, bauen wir das obige Beispiel mit den Bundesländern so um, dass alle Bundesländer, die Stadtstaaten sind, mit einem anderen Template dargestellt werden sollen.

Zunächst die Klasse DataTemplateSelector für Silverlight:

/// <summary>
/// Base class for Silverlight DataTemplateSelector.
/// </summary>
public class DataTemplateSelector : ContentControl
{
  /// <summary>
  /// Called when the value of the <see cref="System.Windows.Controls.ContentControl.Content"/> property changes.
  /// </summary>
  protected override void OnContentChanged(object oldContent, object newContent)
  {
    base.OnContentChanged(oldContent, newContent);

    var template = SelectTemplate(newContent, null);
    if (template != null)
      ContentTemplate = template;
  }

  /// <summary>
  /// When overridden in a derived class, returns a DataTemplate based on custom logic.
  /// </summary>
  /// <param name="item">The data object for which to select the template. </param>
  /// <param name="notUsedInSilverlight">The not used in silverlight. Exists for WPF compatibility only.</param>
  /// <returns>Returns a DataTemplate or a null reference. The default value is a null reference.</returns>
  public virtual DataTemplate SelectTemplate(object item, DependencyObject notUsedInSilverlight)
  {
    return null;
  }
}

Wie man sieht, ist diese Klasse von ContentControl abgeleitet. Ändert sich der Content, wird über die Funktion OnContentChanged die Funktion SelectTemplate aufgerufen. Diese Funktion hat die gleiche Signatur wie ihr WPF Gegenstück.

Um unser Beispiel umzusetzen wird eine neue Klasse MyTemplateSelector von DataTemplateSelector abgeleitet:

public class MyTemplateSelector : DataTemplateSelector
{
  public override DataTemplate SelectTemplate(object item, DependencyObject container)
  {
    var stateInfo = item as States.StateInfo;
    if (stateInfo != null)
    {
      if (stateInfo.Capital != "–")  // the state is not one of the three German city-states
      {
        // This variation demonstrates how to read the template from a resource file
        var info = Application.GetResourceStream(new Uri("/DataTemplateSelectorSample;component/Assets/RegularStateTemplate.xaml", UriKind.Relative));
        using (var reader = new StreamReader(info.Stream))
        {
          return XamlReader.Load(reader.ReadToEnd()) as DataTemplate;
        }
      }

      // This variation demonstrates how to read the template from the Application resources
      return Application.Current.Resources["CityStateTemplate"] as DataTemplate;
    }
    return null;
  }
}

Anhand der Property Capital wird unterschieden, ob es sich bei einem Daten Item um ein Stadtstaat oder ein normales Bundesland handelt. Je nachdem was es ist, werden unterschiedliche Templates geladen.

Zuletzt muss noch die Listvox mit der Klasse MyTemplateSelector verknüpft werden:

<ListBox x:Name="lbxStatesOfGermany" Margin="10 10 10 10">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <tmpl:MyTemplateSelector Content="{Binding}"/>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

Wie bereits gesagt, wird MyTemplateSelector zum Item Template der Listbox. Die Listbox erzeugt nun für jedes ihrer Daten Items eine Instanz von MyTemplateSelector und bindet das jeweilige Item daran. Abhängig vom Item setzt nun jede Instanz von MyTemplateSelector wiederum ihr eigenes Content Template.

Hier das Ergebnis zum ausprobieren

Fazit

Der Silverlight Data Template Selector lässt sich genau so einfach verwenden wie in WPF. Er funktioniert natürlich nicht nur mit Listboxen, sondern mit alle Items Controls. Dieses Beispiel zeigt wieder einmal, wie einfach und elegant man Silverlight erweitern kann.

Hier der gesamte Quellcode der beiden Beispiele zum Downloaden:

SilverlightDataTemplateSelector.zip (202,00 kb)

Tags: ,

Silverlight | WPF

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