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

Windows 8 und die Zukunft von Silverlight

by St. Lange 27. September 2011 17:30

Der Nachfolger von Windows 7

Zunächst einmal wird auf Windows 8 alles laufen, was auch auf der Vorgängerversion läuft. Es gab ja einige Befürchtungen, dies könnte nicht so sein.

Neben vielen wirklich coolen Neuerungen wie Hyper-V auf dem Client, RemoteFX oder Windows To Go, ist die neue Oberfläche im Metro Design und die ihr zugrundeliegende Windows Runtime das innovative Highlight von Windows 8. Neben dem gewohnten Windows Desktop, der auch weiterhin zur Verfügung stehen wird, gibt es die neue Metro Oberfläche, die speziell auch Touch-Geräte unterstützt und teilweise schon vom Windows Phone bekannt ist.

Grundlegend neu ist die Windows Runtime, die die Programmierschnittstelle für Metro-Apps bereitstellt und technologisch neue Wege einschlägt.

Die Windows Runtime

Die schlicht Windows Runtime (WinRT) genannte Schnittstelle zum Betriebssystem ist ein ABI (Application Binary Interface) nach dem Vorbild von COM. Die WinRT ist in nativem Code mit C++ geschrieben und darauf ausgelegt, das UI „fast and fluid“ zu machen. Um sie zu programmieren, wurde C++ mit vielen neuen Features angereichert, die man aus .NET kennt: Properties, Exceptions, Delegates, Events, Generics, Lambda-Expression, u.a. Die dazugehörige Syntax ist praktisch identisch mit C++/CLI, nur wird eben kein managed, sondern nativer Code generiert. Die WinRT stellt ihre Funktionalität in Form von Komponenten, die Interfaces implementieren, zur Verfügung. Um aus verschiedenen Programmiersprachen heraus genutzt werden zu können, legt sie bestimmte Datentypen und Konventionen fest. Das ist ganz ähnlich wie die CLR in .NET funktioniert, aber die WinRT basiert nicht auf .NET. Sie verwendet insbesondere kein Garbage-Collection, sondern das aus COM bekannte Reference-Counting. Um die Programmierung zu vereinfachen wurde dazu in C++ ein neuer new-Operator eingeführt, der den Compiler veranlasst, die Aufrufe von AddRef und Release selbst zu generieren, wenn der Pointer auf ein so erzeugtes Objekt verwendet wird.

Die in COM verwendete IDL (Interface Definition Language) zur Beschreibung von Schnittstellen wird nicht eingesetzt. Stattdessen kommt ein moderneres Metadatenformat zum Einsatz, welches eine Weiterentwicklung des aus .NET bekannten Metaformats darstellt. Aufgrund dieser Windows Metadata (WinMD) kann die WinRT von beliebigen Sprachen aus anprogrammiert werden (im Gegensatz zu .NET, was nur von beliebigen managed Sprachen aus angesprochen werden kann). Zurzeit sind C++ und JavaScript die einzigen unmanaged Sprachen und C# und VB die einzigen managed Code Sprachen, die zur Verfügung gestellt werden.

Das Binden an eine konkrete Programmiersprache wird als „Projection“ bezeichnet, wohl um auszudrücken, dass es mehr ist als simples „Binding“. Da die WinRT sich über ihre Metadaten selbst beschreibt, ist die Projection in eine konkrete Sprache automatisiert und sehr effizient möglich. Sprachen wie C# können diese Metadaten praktisch direkt nutzen und benötigen daher die in COM notwendigen Interop-Assemblies nicht mehr.

Die Projection erlaubt außerdem eine tiefe Integration in die jeweilige Programmiersprache. So sind beispielsweise ca. 10 bis 15 Prozent der WinRT Funktionen asynchron, d.h. sie starten einen länger andauernden Vorgang und liefern gleichzeitig ein Interface auf ein Operation-Objekt zurück, über das man diesen Vorgang kontrollieren und am Ende dessen Ergebnis abfragen kann. Ruft man eine solche Funktion aus C# heraus aus, kann man direkt das neuen Schlüsselwort await aus dem async/await Pattern von C# 5 verwenden. Aus dem IAsyncOperation der WinRT wird ein Task<T> in C#. In JavaScript dagegen wird die gleiche WinRT-Funktion in ein entsprechendes Promise-Konstrukt umgewandelt.

Programmiermodell

Das folgende Schaubild ist eine der Kernfolien von Microsoft.

Es zeigt deutlich, dass die Metro Apps neben dem bisherigen Windows Desktop Apps liegen, sie also um etwas Neues ergänzen und nichts ersetzen.

Die von der WinRT angebotenen Klassen und Funktionen sehen Silverlight zum Verwechseln ähnlich und jeder Silverlight- bzw. WPF-Programmierer findet sich praktisch sofort zurecht. Zwar sind die Namespaces und viele Details etwas anders, aber die aus Silverlight bekannten Controls und Konzepte wie XAML, Resources, Styles, Templates, etc. werden alle nahezu identisch unterstützt. Auch das Resource-Format .resw ist praktisch das Gleiche wie .resx.

Die Konzepte der WinRT eignen sich hervorragend für alle Arten von Schnittstellen, stehen aber zunächst mal nur unter Windows 8 und nur für Metro Apps zur Verfügung.

C++/CX

C++/CX ist der Name des für die WinRT erweiterten C++. Stark vereinfacht kann man es so ausdrücken: C++ plus die coolen Sprachfeatures von C# mit Reference Counting anstatt Garbage Collection. Da C++/CX nativen Code erzeugt, kann man nun die unschlagbare Performance von C++ mit den modernen Sprachmitteln von C# nutzen. Ein gewaltiger Schritt für C++ als Basissprache moderner Betriebssysteme. Microsoft möchte, dass C++/CX in den ANSI-Standard einfließt. Zurzeit kann man C++/CX allerdings nur unter Windows 8 und nur für WinRT Metro Apps verwenden.

JavaScript

Auch für JavaScript ist die WinRT verfügbar. Dadurch sind mit HTML5 Browser-basierte Metro-Anwendungen möglich, die alle Features des Betriebssystems nutzen können. Ich sehe zwei große Vorteile dieses Ansatzes. Zum einen findet das riesige Heer von JavaScript-Programmierern nun einen einfachen Zugang zur Programmierung von Windows Apps. Ein kluger Schachzug von Microsoft, der auch von Microsofthassern nur schwer zu kritisieren ist. Zum anderen lässt sich dynamischer Content aus dem Web leichter in eine HTML5- als in eine XAML-basierte Metro App integrieren.

Bevor nun jemand wegen HTML5 auf falsche Gedanken kommt: Alle Metro Apps basieren auf der WinRT. HTML5 Apps werden zwar vom IE 10 gehostet, laufen aber ausschließlich auf Windows 8 Geräten und sind damit genauso proprietär wie eine XAML-basierte Metro App.

C#/VB

Als .NET Sprachen stehen zurzeit C# und VB zur Verfügung. Auch wenn das obige Schaubild den Eindruck erweckt, als würde C# irgendwie ohne .NET Framework laufen, gibt es genau wie bisher eine CLR, Garbage Collection, IL Code und ein .NET Framework. Tatsächlich ist auf Windows 8 auch nur ein .NET 4.5 Framework installiert, mit dem man wahlweise WinForms, ASP.NET, WPF, etc. Anwendungen erstellen kann oder alternativ auf Basis der WinRT Metro Apps. Eine Vermischung der beiden Welten innerhalb einer Anwendung ist nicht vorgesehen.

Ansonsten hat man das Gefühl, Silverlight oder WPF zu programmieren. XAML, Controls, Events, Styles, Templates, ResourceDictionaries, VisualStateManager, usw. sind bis ins Detail sofort vertraut. Natürlich gibt es etliche Unterschiede, beispielsweise bei den Touch-Events oder bei neuen Namensräumen, aber jeder Silverlight-Entwickler findet sich schnell zurecht.

Es ist auch möglich, mit C#/VB neue WinRT-kompatible Komponenten zu erstellen, wenn man sich an bestimmte Konventionen hält. So kann man beispielsweise nützliche Hilfsroutinen, sein ViewModel oder neue Controls in C# schreiben und diese dann in einer HTML5 App über JavaScript verwenden. So muss das auch sein. Ich bin absolut überzeugt, dass .NET die dominierende Technologie für WinRT Anwendungen werden wird, weil .NET einfach die produktivste Art ist, Software zu entwickeln, die bisher erfunden wurde.

Performance zählt

Eine der wichtigsten Neuerungen der WinRT sind drastische Verbesserungen der UI-Performance gemäß dem Metro-Paradigma „fast and fluid“. Am einfachsten lässt sich das anhand eines Beispiels verdeutlichen. In Silverlight beruhen Animationen letztlich darauf, dass man über eine Timeline gesteuert eine oder mehrere Dependency-Properties über einen Zeitraum hinweg verändert. Basierend auf diesen Änderungen rendert der UI-Thread die Oberfläche mehrmals pro Sekunde neu. Auf einem Intel Desktop-Prozessor ist dies kein Problem, wenn auch ein Blick in den Taskmanager zeigt, dass damit ein Prozessorkern oft zu 100% ausgelastet ist. Auf einem mobilen Gerät mit einem ARM Prozessor führt dies jedoch zu einem unlösbaren Problem. Die Animation wirkt entweder ruckelig oder benötigt bestenfalls einfach zu viel Energie und belastet damit die Batterie unangemessen. Dies war einer der Hauptgründe, warum Microsoft vor rund 3 Jahren Silverlight nicht auf Windows Mobile sinnvoll zum Laufen bekommen hat. Andererseits erwartet ein Anwender heutzutage, das beispielsweise ein zu löschendes Element aus einer Liste nicht einfach „wegpoppt“, sondern sich langsam ausblendet, während die dahinterliegenden Elemente nachrücken und die Lücke schließen. Grundsätzlich kein Problem für ein Silverlight FluidMoveBehavior, aber leider sind diese zu performancehungrig für mobile Geräte.

Die WinRT geht daher mit fest eingebauten Animationen für Standard-Controls einen neuen Weg. Für das obige Beispiel aktiviert man einfach die sog. Theme-Transition AddDelete aus der eingebauten Animations-Library. Etwas vereinfacht ausgedrückt rendert diese Animation alle beteiligten List-Items genau einmal und bewegt sie dann GPU-beschleunigt über das Display, ohne dass der Inhalt dabei neu berechnet werden müsste. Visuell macht dies keinerlei Unterschied, sieht geschmeidig aus und braucht nur einen Bruchteil der Energie. Die vorgefertigten Animationen sind vielfältig und lassen sich auch untereinander kombinieren. Wem das dennoch nicht ausreicht, kann die jetzt als „Dependent Animation“ bezeichneten bisherigen Animationen immer noch verwenden. Allerdings muss man sie explizit einschalten, damit einem auch bewusst wird, dass man hier Leistung verbrät.

Dieses Beispiel zeigt sehr deutlich, wie sehr die WinRT auf Geschwindigkeit, flüssiges UI und Energieeffizienz getrimmt ist. Der Verzicht auf Garbage-Collection in der WinRT erfolgte übrigens nicht aufgrund von Performanceüberlegungen. Zum einen wollte man auf Betriebssystemebene die vollständige Kontrolle über die Speicherverwaltung haben und zum anderen sollte die WinRT wirklich von jeder Programmiersprache aus genutzt werden können.

Weitere Programmiersprachen

C++, JavaScript, C# und VB sind zurzeit die einzigen verfügbaren Programmiersprachen für die Windows Runtime und man kann damit auch nur Metro Apps bauen. Da jedoch alle Entwickler, mit denen ich auf der BUILD gesprochen habe, völlig begeistert sind von der WinRT, vermute ich mal, dass in naher Zukunft diverse Programmiersprachen auf die WinRT aufgesetzt werden. Einfach so, weil es eine interessante Herausforderung ist und Entwickler gerne spannende neue Dinge machen. Außerdem lassen sich viele Sprachen vermutlich sogar leichter auf die WinRT aufsetzen als auf das .NET Framework.

Silverlight

Zur strategischen Zukunft von Silverlight wurde auf der BUILD nichts gesagt. Natürlich läuft Silverlight im Browser und Out-of-Browser auf dem Windows 8 Desktop. Das war aber sowieso klar, da schon immer jede neue Windows-Version die Software der Vorgängerversion ausführen konnte. Auch das Silverlight, Flash und sonstige PlugIns nicht innerhalb einer HTML5 Metro App laufen werden ist logisch. Metro Apps laufen in einer Sandbox und werden vor dem Verkauf über den Windows Marketplace von Microsoft verifiziert. Und zu diesem Konzept passen nun mal keine Browser-PlugIns, auch nicht Silverlight.

Nun ist Metro zwar technisch gesehen eine Weiterentwicklung von Konzepten aus Silverlight, deckt aber einen größtenteils anderen bzw. neuen Markt ab. Metro steht für eine neue UX auf Windows 8 Geräten und läuft weder auf Windows XP, Vista oder 7, noch auf dem Desktop weder im Browser noch Out-of-Browser, noch auf dem Mac. Damit ist es auch mittelfristig keine Alternative für Projekte, die man heute mit Silverlight machen würde. Silverlight hat sich von der aus heutiger Sicht nicht mehr realistischen „WPF/Everywhere“ Idee hin zu einer Browser-basierten Windows Desktop Technologie entwickelt. Durch die Einführung von PInvoke kann eine Full-Trust Silverlight 5 Anwendung sowohl im Browser als auch Out-of-Browser alles machen, was mit Win32 möglich ist. Das ist eine andere Richtung als die von Metro eingeschlagene. Metro erweitert Windows für neue Gerätetypen, ersetzt aber keine der bisherigen Desktop-Technologien.

Ich vermute, dass Microsoft selbst noch nicht so genau weiß bzw. entschieden hat, was nach Silverlight 5 kommen wird. Viele Firmen haben auf Silverlight gesetzt und auch LightSwitch basiert darauf. Daher bin ich sicher, dass Silverlight auch in den nächsten Jahren eine zunehmend wichtigere Rolle spielen wird. Für reine Web-Anwendungen mit maximaler Reichweite muss man ausschließlich auf HTML und JavaScript setzen, das war aber auch schon bei Einführung von Silverlight so und Silverlight war nie als Alternative zu HTML angedacht. In Szenarien, wo man jedoch die Wahl zwischen HTML oder Silverlight hat, ist und bleibt Silverlight fast immer die bessere Alternative.

Die tägliche Arbeit

Metro und die WinRT sind sowohl von der UX Seite als auch technologisch ein großer Schritt nach vorne. Die Zukunft wird sicherlich sehr spannend werden. Bis Windows 8 auf dem Markt ist und eine nennenswerte Verbreitung hat, wird allerdings noch eine ganze Weile vergehen. Wer also nächsten Monat ein neues Projekt startet und sich heute Gedanken über die Technologie macht, für den hat sich durch die BUILD nicht viel geändert. Er sollte Silverlight oder WPF für den Desktop verwenden, genau wie bisher. Zwei sehr positive Dinge für die unmittelbare Zukunft hat die BUILD allerdings schon gebracht: Zum einen weiß jetzt jeder Silverlight-Entwickler, dass er sein Know-how zur UI-Programmierung, XAML oder RIA Services auch langfristig mit Metro weiter nutzen kann. Zum anderen sind die von Microsoft durch eine unglückliche Präsentation von Windows 8 im Juni geschürten Befürchtungen, die .NET Entwicklung würde durch JavaScript ersetzt, endlich vom Tisch.

Ich vermute, dass die WinRT langfristig das Win32 API verdrängen wird, jedoch frühestens in der übernächsten Windows-Version. Vorher werden aber bestehende Systeme wie Windows Phone 8 die WinRT als Basis erhalten. Microsoft hat jedenfalls eine sehr durchdachte und innovative Grundlage für die nächsten Jahre geschaffen.

Wenn ich mir was wünschen könnte: Silverlight 6 im Metro Look mit einer WinRT als PlugIn für weitere non-Microsoft Plattformen. Dies wird aber wohl ein Wunsch bleiben.

Tags: , ,

.net | Silverlight

C# vs. JavaScript im IE9

by St. Lange 11. February 2011 19:30

Mit dem gestern erschienenen Release Candidate von Internet Explorer 9 hat Microsoft einen äußerst leistungsfähigen Browser geschaffen. Neben vielem anderen ist die Ausführung von JavaScript enorm beschleunigt worden, u. a. weil der Code compiliert wird. In diesem Zusammenhang erinnerte ich mich an ein Programm aus den Anfängen von Silverlight.

Vor ca. drei Jahren gab es von Microsoft für Silverlight 1.1 eine Schach Demo-Anwendung zum Vergleich der Performance von .NET und JavaScript. Dieses SilverlightChess genannte Programm verwendet Silverlight zur Darstellung des Schachbretts und der Spielfiguren. Der eigentliche Spielalgorithmus Garbo Chess ist zweimal implementiert, einmal mit C# und einmal mit JavaScript. Wenn man das Programm gegen sich selbst spielen lässt, kann man je Farbe wählen, ob die .NET oder JavaScript Variante verwendet werden soll. Neben dem Spielfeld wird angezeigt, wie viele Spielknoten pro Sekunde für den jeweils letzten Zug durchprobiert wurden.

Auch wenn SilverlightChess kein richtiger Benchmark-Test ist, kann man damit ganz gut den Performanceunterschied zwischen .NET und JavaScript veranschaulichen. Mit dem IE8 schafft JavaScript so rund 3.000 Knoten pro Sekunde (getestet auf meinem schon etwas älteren Core 2 Quad mit 2.400 MHz). Der aktuelle Firefox 3.6 ist im Vergleich dazu fast 5-mal schneller. Der IE9 RC dagegen schafft um die 100.000 Knoten, was einer Steigerung von rund 3.000% entspricht. Dieser hohe Faktor kommt natürlich auch daher, dass der IE8 bzgl. JavaScript der langsamste Vertreter unter den aktuellen Browsern ist.

Die C#-Implementierung liegt hingegen mit ca. 1.400.000 Knoten pro Sekunde noch mal gut eine Größenordnung darüber. Dies zeigt sehr deutlich, dass C# bzw. Silverlight die bessere Wahl ist, wenn Client-seitig komplexe Berechnungen durchgeführt werden sollen und es auf hohe Performance ankommt. Hier SilverlightChess zum Ausprobieren. Es ist praktisch unverändert zur Originalversion; lediglich mit Visual Studio 2010 und für Silverlight 4 übersetzt:

Da der Quellcode auf www.silverlight.net offensichtlich nicht mehr existiert, hier die jüngste Version, die ich habe, zum Runterladen.

SilverlightChess2008.zip (67 kB)

Tags:

Silverlight

Silverlight Tipp der Woche: Dynamisches Nachladen und Caching von XAP-Dateien

by St. Lange 19. December 2010 11:20

In diesem Tipp wird das dynamische Laden und lokale Caching von Teilen einer Silverlight-Anwendung zur Verbesserung des Ladeverhaltens vorgestellt.

Zusammenfassung

Auch sehr große Silverlight-Anwendungen sollten beim ersten Aufruf möglichst schnell starten und beim nächsten Start am besten gar keine Assemblies mehr übers Web transportieren müssen. Durch das Zwischenspeichern von Teilen der Anwendung im Isolated Storage kann dies recht einfach erreicht werden.

Überblick

Bevor eine Silverlight-Anwendung ausgeführt werden kann, muss zunächst ihre XAP-Datei vom Webserver heruntergeladen werden. Bei größeren Anwendungen kann dies schon mal unangenehm lange dauern. Mit der Projektoption „Reduce XAP size by using application library caching“ kann die Größe der XAP-Datei dadurch vermindert werden, dass bestimmte Assemblies im Browser Cache zwischengespeichert werden. Diese Methode hat jedoch zwei Nachteile: Zum einen wird der erste Start dadurch nicht beschleunigt, denn beim allerersten Mal müssen ja trotzdem alle Assemblies zunächst heruntergeladen werden. Zum anderen funktioniert das Verfahren nicht bei Out-of-Browser Anwendungen.

Ich möchte hier ein Verfahren vorstellen, bei dem die Gesamtanwendung in mehrere XAP-Dateien zerlegt und im Isolated Storage zwischengespeichert wird. Tim Heuer hat die Grundidee in zwei Blog-Artikeln hier und hier schon vor längerem beschrieben. Bevor ich dieses Verfahren jedoch in einem unserer Produkte eingesetzt habe, wollte ich bestimmte Aspekte noch genauer untersuchen.

Zum „Proof of Concept“ dient eine minimalistische Testanwendung, die die Konstellation in einem realen Projekt modelliert. In unserem echten Projekt gibt es unter anderem eine Komponente, die PDF-Dateien im Corporate Design des Kunden generieren wird. Diese Komponente kann aufgrund von eingebetteten Bildern, Grafiken, Fonts etc. so groß werden, dass ein regelmäßiges Herunterladen aufgrund der Wartezeit nicht akzeptabel wäre.

Beispiel Solution

Das vorgestellte Beispielprojekt berücksichtigt auch Versions- und Lokalisierungsaspekte und kann zum Experimentieren für eigene Lösungen als Ausgangspunkt dienen. Es besteht aus 6 Silverlight Projekten. XapLoadingAndCaching ist die eigentliche Startanwendung. GeneralLibrary steht exemplarisch für eine Assembly, die von allen Projekten referenziert wird. Die drei Projekte LargePart1 bis LargePart3 stehen für drei große Teilkomponenten, die einzeln geladen werden können, jedoch auch voneinander abhängig sind.

Wenn ein Silverlight Application Projekt compiliert wird, erzeugt Visual Studio dabei eine XAP-Datei. Diese ist ein Zip-Archiv mit allen benötigten Assemblies und sonstigen Ressourcen, die Teil dieser Anwendung sind. Das Projekt XapLoadingAndCaching referenziert die Projkete GeneralLibrary, LargePart1 und LargePart2. Allerdings ist für LargePart1 und LargePart2 die Projekt-Eigenschaft Copy Local auf False gesetzt. Durch diesen Trick werden diese beiden Assemblies nicht in die XAP-Datei der Anwendung aufgenommen, obwohl es im Code Aufrufe in beide Assemblies gibt.

XapLoadingAndCaching.xap enthält daher nur folgende Dateien:

Tipp: Wer WinZip oder ein vergleichbares Tool verwendet, kann darin „.xap“ als Dateierweiterung hinzufügen und dann XAP-Dateien einfach anklicken, um ihren Inhalt zu kontrollieren.

Durch das Weglassen von referenzierten Assemblies lassen sich also sehr kleine und damit schnell zu ladende XAP-Dateien für die Startanwendung erzeugen. Die Anwendung läuft problemlos, sofern man keine Funktionen aus noch nicht geladenen Teilen aufruft. Eine echte Anwendung kann so sehr schnell ihre Hauptseite anzeigen. Und während der Anwender sich darin umschaut, werden asynchron weitere Teile nachgeladen.

Hier im Beispiel können die Applikationsteile zum Experimentieren jeweils einzeln über die drei Load Buttons nachgeladen werden.

Mit den beiden Call Buttons werden Testfunktionen in den jeweiligen Teilen aufgerufen. Drückt man einen der Buttons, bevor der benötigte Teil geladen wurde, kommt es erwartungsgemäß zu einer Exception.

Nachladen und Caching

Mit der Klasse WebClient kann man jede Art von Dateien und somit auch Assemblies vom Server downloaden und dann über die Klasse AssemblyPart in die laufende AppDomain hinzufügen. Das ist aber recht unpraktisch. Typische Komponenten bestehen meist aus mehreren zusammengehörigen Assemblies, die auf einen Schlag geladen werden sollten. Weiterhin sind Assembly-Dateien nicht komprimiert, was die Downloadzeit im Vergleich zu einer gezippten Datei mindestens verdoppelt.

Der nächste Trick besteht nun darin, die drei LargePart Projekte nicht als Silverlight Class Libraries, sondern als Silverlight Applications anzulegen. Dadurch werden automatisch XAP-Dateien generiert, die von Visual Studio auch gleich noch nach jedem Compilieren ins ClientBin-Verzeichnis der Web-Anwendung kopiert werden.

Im Beispiel ist festgelegt worden, dass LargePart1 vor LargePart2 geladen werden muss. Daher ist die gemeinsame Assembly PartLibrary nur in LargePart1.xap enthalten. LargePart2 referenziert zwar auch PartLibaray, aber Copy Local ist hier wieder auf False gesetzt, denn diese Assembly ist ja schon im vorher geladenen Part enthalten gewesen. Dadurch wird LargePart2.xap kleiner.

Die durch die Projektvorlage generierte Datei App.xaml wird übrigens nicht benötigt und kann aus dem Projekt herausgelöscht werden, denn diese XAP-Dateien benötigen ja kein Startup-Objekt. Es stört aber auch nicht, wenn sie wie in LargePart1 einfach drin bleiben. Man könnte App.xaml aber auch drin lassen und so diese XAP-Datei beispielsweise zu Testzwecken alleinstehend startbar machen.

Der nächste Trick ist das Zwischenspeichern der XAP-Dateien im Isolated Storage. Beim nächsten Start wird zunächst einmal nachgeschaut, ob die benötigte XAP-Datei schon auf dem Client Rechner vorliegt. Im Normalfall startet so auch eine extrem große Browser-Anwendung blitzschnell.

Hier zunächst der Code zum Downloaden einer XAP-Datei. Es ist jedoch nur eine Skizze zum Verständnis des Konzepts. Fehlerbehandlung oder die Erhöhung des Isolated Storage Kontingents wurden hier weggelassen.

public static void LoadXap(string name, string version)
{
  // Do not load XAP file more than once.
  if (_loadedXaps.ContainsKey(name))
    return;
 
  using (var store = IsolatedStorageFile.GetUserStoreForSite())
  {
    string cachedFileName = GetCachedFileName(name, version);
    if (store.FileExists(cachedFileName))
    {
      // Take file from cache.
      IsolatedStorageFileStream fileStream =
        store.OpenFile(cachedFileName, FileMode.Open, FileAccess.Read);
      LoadXap(fileStream);
      _loadedXaps.Add(name, null);
    }
    else
    {
      // Clear older version of file, if any exists.
      ClearCache(store, name);
 
      // Download file from site of origin.
      var webClient = new WebClient();
      webClient.OpenReadAsync(new Uri(name, UriKind.Relative));
      webClient.OpenReadCompleted += (sender, e) =>
      {
        using (var store2 = IsolatedStorageFile.GetUserStoreForSite())
        {
          // Save copy of new file in store.
          int length = (int)e.Result.Length;
          byte[] buffer = new byte[length];
          e.Result.Read(buffer, 0, length);
          using (var fileStream = store2.CreateFile(cachedFileName))
          {
            fileStream.Write(buffer, 0, length);
          }
          LoadXap(e.Result);
          _loadedXaps.Add(name, null);
        }
      };
    }
  }
}

Die Funktion erhält neben dem XAP-Dateinamen auch noch einen Version-String. Dieser wird an den Namen der zwischengespeicherten Datei angehängt. Dadurch wird sichergestellt, dass die Datei erneut heruntergeladen wird, wenn auf dem Server eine neuere Version vorliegt. In einem realen Projekt muss man einfach bei jedem Deployment die Version hochzählen. Hier im Beispiel kann man in der Textbox eine Versionsnummer eingeben oder mit dem Clear Cache Button die Dateien manuell löschen.

Um zu sehen, welche Dateien im Cache sind, kann man im Pfad

C:\Users\CurrentUser\AppData\LocalLow\Microsoft\Silverlight\is\

nach „LargePart“ suchen. Dann findet man den Isolated Storage der Anwendung.

Nachdem die XAP-Datei auf dem Client vorliegt, werden die in ihr enthaltenen Assemblies geladen:

static void LoadXap(Stream stream)
{
  StreamResourceInfo sri = new StreamResourceInfo(stream, null);
 
  // Get a list of all assembly part names from application manifest.
  string appManifest = new StreamReader(Application.GetResourceStream(sri,
      new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd();
  XElement deploy = XDocument.Parse(appManifest).Root;
  List<XElement> parts = (from assemblyParts in deploy.Elements().Elements()
      select assemblyParts).ToList();
 
  // Load all assembly parts.
  foreach (var source in parts.Select(element => element.Attribute("Source")))
  {
    var streamInfo = Application.GetResourceStream(sri, new Uri(source.Value, UriKind.Relative));
    new AssemblyPart().Load(streamInfo.Stream);
  }
}

Die in der XAP-Datei enthaltenen Assemblies werden über das Manifest herausgesucht und geladen.

Lokalisierung

LargePart2 enthält zwei RESX-Dateien mit einem deutschen bzw. englischen Text. Ein Blick in das für LargePart2.xap generierte Manifest zeigt, dass die deutsche Satellite Assembly LargePart2.resources.dll korrekt im Unterverzeichnis de enthalten ist:

<Deployment xmlns=http://schemas.microsoft.com/client/2007/deployment …>
  <Deployment.Parts>

    <AssemblyPart x:Name="LargePart2" Source="LargePart2.dll" />
    <AssemblyPart Source="de/LargePart2.resources.dll" />
  </Deployment.Parts>
</Deployment>

Der Testcode bestätigt, dass alles wie gewünscht funktioniert:

ResourceManager rm = new ResourceManager("LargePart2.StringResources",
    Assembly.GetExecutingAssembly());
var de = rm.GetString("String1");
var en = rm.GetString("String1", CultureInfo.InvariantCulture);

Sowohl der englische als auch der deutsche Text werden korrekt geladen.

Das Unterverzeichnis de existiert nur im Zip-Archiv der XAP-Datei. Im Isolated Storage liegt die Datei LargePart2.xap-1.00. Man braucht sich also um nichts zu kümmern, was im Vergleich mit einem Download einzelner Assemblies äußerst praktisch ist.

Es ist übrigens egal, in welcher Reihenfolge voneinander abhängige Assemblies geladen werden, da erst bei ihrer ersten Verwendung das Vorhandensein von referenzierten Assemblies überprüft wird.

Statische Ressouce-Dateien

Neben Code und lokalisierten Strings kann eine XAP-Datei natürlich auch Assemblies mit sonstigen statischen Ressourcen enthalten. LargePart3.xap enthält eine Assembly mit einem Beispiel-Font. Da XAP-Dateien Zip-Container sind, ist diese Datei signifikant kleiner als die Font-Datei selbst. Es macht also sehr viel Sinn, statische Ressourcen ebenfalls in XAP-Dateien zu verpacken.

Der folgende Code zeigt den Zugriff auf die Font Ressource:

void LoadFont(object sender, RoutedEventArgs e)
{
  var stream = Application.GetResourceStream(
    new Uri("/LargePart3;component/fonts/Early_Tickertape.ttf",
    UriKind.Relative));
  txtBlock.FontSource = new FontSource(stream.Stream);
  txtBlock.FontFamily = new FontFamily("Early Tickertape");
}

Wenn LargePart3.xap geladen wurde, ändert dieser Code den Font des angezeigten Beispieltextes.

Out-of-Browser Anwendungen

Das vorgestellte Verfahren funktioniert bei Browser und Out-of-Browser Anwendungen identisch. Bei Out-of-Browser Anwendungen muss man allerdings zwei Dinge beachten, wenn man offline arbeiten möchte: Alle XAP-Dateien müssen im Cache vorliegen und der Cache darf nicht gelöscht werden.

Managed Extensibility Framework

Der im Beispiel verwendete Trick mit dem Referenzieren von Assemblies, die nicht in der eigenen XAP-Datei vorhanden sind, sollte in Produktivcode besser nicht verwendet werden, da das Risiko eines Funktionsaufrufes in eine nicht geladene Assembly besteht. Während der Entwicklungszeit und zum Debuggen ist es allerdings praktisch.

Im echten Projekt verwenden wir das Managed Extensibility Framework. Bevor eine Komponente nicht geladen ist, steht sie im Composition-Container auch nicht zur Verfügung und kann somit auch nicht unbeabsichtigt aufgerufen werden. Unmittelbar nach Programmstart werden die XAP-Dateien in einer definierten Reihenfolge im Hintergrund heruntergeladen und in die laufende AppDomain hinzugefügt. Nach jedem Download findet ein Recomposition des Containers statt, damit die neu hinzugekommenen Teile auch verfügbar sind.

Nur durch MEF bzw. einem vergleichbaren Framework kann zuverlässig sichergestellt werden, dass man nicht doch einmal versehentlich einen Funktionsaufruf ins Leere macht.

Fazit

XAP-Dateien lassen sich auf sehr einfache Weise als universelle Zip-Container für Code und Daten verwenden. Der Verwaltungsaufwand bei der Entwicklung ist minimal. Das Nachladen und lokale Zwischenspeichern der Dateien führt zu einer spürbaren Verbesserung der Benutzererfahrung, vor allem wenn die Anwendung sehr groß geworden ist. Das Ganze ist auch keinerlei Hack, sondern einfach die konsequente Nutzung der Möglichkeiten von Silverlight.

Hier das Projekt:

XapLoadingAndCaching.zip (119 kB)

Tags:

Silverlight

Silverlight Tipp der Woche: DataBinding mit der Code-Behind Klasse

by St. Lange 23. October 2010 17:05

In diesem Tipp geht es um das Binden an Properties aus der Code-Behind Klasse eines UserControls.

Zusammenfassung

Wenn der DataContext eines UserControls auf das ViewModel zeigt, kann man mit Hilfe von ElementBinding dennoch einfach auf Properties aus der Code-Behind Klasse zugreifen, indem man das UserControl mit einem Namen versieht.

Beschreibung

Üblicherweise wird der DataContext eines UserConrols auf dessen ViewModel gesetzt. Das ViewModel enthält alle für die Anzeige benötigten Daten und das DataBinding der Unter-Controls bindet ausschließlich gegen das ViewModel.

Es kann aber auch vorkommen, dass ein UserControl selbst Properties implementiert, an die einzelne seiner Unter-Controls binden sollen. Wenn die Property DataContext jedoch auf ein ViewModel gesetzt ist, ist es nicht so ganz offensichtlich, wie denn ein Unter-Control innerhalb von XAML an eine Property aus der Code-Behind Klasse binden könnte. Ein einfacher Trick besteht darin, dem UserControl einen Namen zu geben und dann mit Element-Binding den Bezug zur Code-Bedhind Property herzustellen.

In folgendem Beispiel soll die Property SomeItems aus der Code-Behind Klasse als Datenquelle für eine Listbox dienen. Das UserControl erhält hier den Namen „codeBehind“ und bietet darüber der Listbox Property ItemsSource den Zugriff auf SomeItems.

<UserControl x:Name="codeBehind" x:Class="MyProject.MainPage" … />
  <Grid Background="White">
    <ListBox ItemsSource="{Binding SomeItems, ElementName=codeBehind}" … />
  </Grid>
</UserControl>

Obwohl dieser Weg auch bei WPF und Windows Phone funktioniert, klappt die Tool Unterstützung mit „Apply Data Binding…“ im Property Explorer von Visual Studio 2010 nicht. Es werden nur die Properties der Basisklassen aufgelistet. Bei Blend funktioniert der Property Explorer dagegen korrekt.

Anwendungsbeispiel

Es stellt sich natürlich die Frage, wann man überhaupt an Properties aus einem UserControl binden sollte. Dies ist dann sinnvoll und richtig, wenn das UserControl ein wiederverwendbares Control repräsentiert und unabhängig von der Datenmodellierung einer konkreten Anwendung ist. Ein Beispiel wäre ein UserControl „RichTextEditor“ zum Formatieren von Text. Neben einer RichTextBox enthielt so ein UserControl auch eine Toolbar mit Buttons und ComboBoxes zum Einstellen von Schriftart, Größe etc. Ein solcher RichTextEditor benötigt beispielsweise eine Property EditorFontFamily zum Setzen und Abfragen des Default-Fonts. Damit der RichTextEditor wie ein Control als Teil von anderen UserControls verwendet werden kann, sollten seine Properties DataBinding unterstützen. Dazu müssen sie als Dependency Properties implementiert werden, denn nur Dependency Properties können Target Objekte einer Datenbindung sein. Die Implementierung von EditorFontFamily als Dependency Property sieht wie folgt aus.

public partial class RichTextBox : UserControl
{
  public FontFamily EditorFontFamily
  {
    get { return (FontFamily)GetValue(EditorFontFamilyProperty); }
    set { SetValue(EditorFontFamilyProperty, value); }
  }
  public readonly DependencyProperty EditorFontFamilyProperty =
    DependencyProperty.Register("EditorFontFamily", typeof(FontFamily), typeof(RichTextEditor),
    new PropertyMetadata("Verdana"));
 }

Nun kann RichTextEditor in einem anderen UserControl wie jedes andere Control auch verwendet werden. In
diesem Kontext kann dann EditorFontFamily an eine Property MyFontFamily aus einem ViewModel
gebunden werden.

<UserControl x:Class="MyProject.MyPage" … />

  <Grid Background="White">
    <my:RichTextEditor EditorFontFamily="{Binding MyFontFamily}" … />
  </Grid>
</UserControl>

Damit dies funktioniert muss der DataContext auf das ViewModel des umschließenden Controls MyPage zeigen. Daher kann man nicht einfach im Constructor von RichTextEditor den DataContext auf this setzen.

Innerhalb von RichTextEditor holt sich dessen RichTextBox seine FontFamily aus der Code-Behind Klasse.

<UserControl x:Name="codeBehind" x:Class="MyProject.RichTextEditor"
  <Grid>

    <!-- … -->
    <RichTextBox FontFamily="{Binding EditorFontFamily, ElementName=codeBehind}"… />
    <!-- … -->
  </Grid>

</UserControl>

Dieses Binden über zwei Stufen ist der einfachste Weg, UserContols als eigenständige Controls wiederzuverwenden, und funktioniert in der beschriebenen Art und Weise ohne zusätzlichen Code in der Code-Behind Datei.

Hier geht's zum nächsten Tipp.

Tags:

Silverlight | WPF

Silverlight Tipp der Woche: Design-Time Data verwenden

by St. Lange 30. September 2010 22:04

In diesem Tipp geht es um die Verwendung von Design-Time Attributen wie d:DataContext, d:DesignInstance oder d:DesignData, die einem das Arbeiten im Visual Studio Designer ganz schön erleichtern können.

Zusammenfassung

Die Verwendung von Design-Time Attributen erlaubt die Nutzung des Property Explorers beim Erstellen von Datenbindungen im XAML-Code, sowie das Anzeigen von Beispieldaten im Designer zur Design-Zeit. Auf das Verhalten der Silverlight- Anwendung zur Laufzeit haben sie keinen Einfluss.

Beschreibung

Vor Visual Studio 2010 war das Erstellen von Datenbindungen in XAML mehr oder weniger ein Blindflug. Ob man alles richtig eingetippt hat, konnte man letztlich erst nach einem Start der Anwendung überprüfen. Und wenn man keine Daten angezeigt bekam, musste man recht mühsam nach den Ursachen suchen. Auch das Erstellen von Datatemplates war ohne Expression Blend praktisch nicht effizient möglich. Mit dem neuen Visual Studio ist das sehr viel einfacher geworden, wenn man ein paar Formalitäten beachtet.

Das Erstellen für den korrekten XAML-Code eines Databindings kann über Property Explorer durchgeführt werden. Dazu muss man ihm allerdings die Information über den zu bindenden Datentyp geben. Das folgende Beispiel zeigt, was zu tun ist:

<UserControl x:Class="DesignTimeData.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
    xmlns:data="clr-namespace:DesignTimeData"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
  <Grid x:Name="LayoutRoot" d:DataContext="{d:DesignInstance Type=data:Person}">
    <TextBox Text="{Binding Path=Name, Mode=TwoWay}"/>  
  </Grid>
</UserControl>

Der XML-Namespace d wird standardmäßig angelegt und verweist auf die Design-Time Attribute. Außerdem sorgt die Zeile mc:Ignorable="d" dafür, dass alle Elemente aus diesem Namespace zur Laufzeit ignoriert werden. Damit ist es nun möglich, einem äußeren Element wie hier dem Grid einen DataContext zuzuweisen, der nur zur Designzeit verwendet wird. Im Beispiel sorgt das Attribut DesignInstance dafür, dass dem DataContext eine Instanz des Typs Person zugewiesen wird. Mit diesen Informationen ausgestattet kann der Property Explorer den Datentyp analysieren und beispielsweise zum Binden der Property Text dem Entwickler einen hilfreichen Dialog anbieten:

Der Dialog zeigt die Properties der Klasse Person als mögliche Werte für den Binding Path. Den Dialog erhält man über die rechte Maustaste auf der Property Text im Property Explorer und dann über den Menüpunkt "Apply Data Binding…". Damit kann ein Databinding viel leichter erstellt werden als es bisher möglich war. Es empfiehlt sich also, in allen User-Controls generell im LayoutRoot-Element den Design-Time DataContext auf den zur Laufzeit verwendeten Datentyp zu setzen.

Design-Time Daten

Bei komplexeren Controls wie einem DataGrid kann es zusätzlich hilfreich sein, zur Designzeit ein paar Beispieldaten angezeigt zu bekommen. Dadurch kann beispielsweise das individuelle Layout der Spalten besser angepasst werden.

Im Beispiel gibt es zwei vereinfachte Datenklassen Person und Address, sowie eine Collection von Person Objekten:

public class Person
{
  public string Name { get; set; }
  public DateTime Birthday { get; set; }
  public Address Address { get; set; }
}
 
public class PersonCollection : ObservableCollection<Person>
{ }

public class Address
{
  public string City { get; set; }
}

Mit etwas XAML-Code werden damit die Beispieldaten angelegt:

<data:PersonCollection xmlns:data="clr-namespace:DesignTimeData">
  
  <data:Person Name="Miller" Birthday="1.2.1999">
    <data:Person.Address>
      <data:Address City="Cologne"/>

    </data:Person.Address>
  </data:Person>
 
  <data:Person Name="Baker" Birthday="2.3.1988">
    <data:Person.Address>
      <data:Address City="Berlin"/>
    </data:Person.Address>

  </data:Person>
 
  <data:Person Name="Smith" Birthday="3.4.1977">
    <data:Person.Address>
      <data:Address City="Munich"/>
    </data:Person.Address>
  </data:Person>

 
</data:PersonCollection>

Die Beispieldaten kommen in eine XAML-Datei, die Teil des Projektes ist. Die Build Action dieser Datei muss auf DesignData (oder DesignDataWithDesignTimeCreatableTypes) stehen.

Mit diesen Daten wird nun ein DataGrid wie im folgenden Beispiel versorgt:

<sdk:DataGrid Width="376" Height="138" HorizontalAlignment="Left" Margin="12,96,0,0" VerticalAlignment="Top"
          d:DataContext="{d:DesignData Source=./DesignTimeSampleData.xaml}"
          ItemsSource="{Binding}" AutoGenerateColumns="False">
  <sdk:DataGrid.Columns>
    <sdk:DataGridTextColumn Binding="{Binding Path=Name}" Header="Name" />
    <sdk:DataGridTextColumn Binding="{Binding Path=Birthday, StringFormat='dd.MM.yyy'}" Header="Birthday" />
    <sdk:DataGridTextColumn Binding="{Binding Path=Address.City, Mode=TwoWay}" Header="City" />
  </sdk:DataGrid.Columns>

</sdk:DataGrid>

Der DataContext des DataGrids wird über das Design-Time Attribut DesignData direkt mit den Beispieldaten aus der XAMLDatei verbunden. Als Effekt davon werden die Beispieldaten im Designer angezeigt:

Das ist äußerst praktisch, wenn man beispielsweise die Spalten über Data-Temples layouten möchte, denn ohne eine WYSIWYG-Datenvorschau geht das nur mit sehr sehr viel Übung.

Expression Blend verfügt schon etwas länger über die Möglichkeit, das Layout mit generierten Beispieldaten auszustatten. Das dabei verwendete Verfahren ist aber komplizierter und nutzt dafür u.a. Static Resources. Außerdem werden die Daten dem "echten" DataContext zugwiesen und sind daher auch zur Laufzeit vorhanden. Der Entwickler muss sie also entweder irgendwann wieder aus dem XAML entfernen oder zumindest per Code den DataContext überschreiben.

Das beschriebene Verfahren mit den Design-Time Attributen hat dagegen keine Auswirkungen zur Laufzeit und eignet sich auch sehr gut für Entwickler, die keinen Zugriff auf Blend haben.

Weitere Details finden sich in der MSDN Hilfe.

Das Projekt, mit dem die Screenshots gemacht wurden, ist hier:

DesignTimeData.zip (8 kB)

Hier geht's zum nächsten Tipp.

Tags:

Silverlight | WPF

Silverlight Tipp der Woche: .restext statt .resx Dateien für Text–Ressourcen

by St. Lange 4. September 2010 10:48

In diesem Tipp geht es um die Verwaltung von String Ressourcen in .restext Dateien, welche beispielsweise bei der Lokalisierung Vorteile gegenüber üblichen .resx Dateien haben.

Zusammenfassung

Beim Verwalten von Texten als Ressourcen beispielsweise für die Lokalisierung werden üblicherweise .resx Dateien verwendet. Für Texte sind Dateien im .restext Format jedoch oft besser geeignet.

Beschreibung

Der Standardweg zum Verwalten von Ressourcen in .NET sind die XML-basierten .resx Dateien. Mit ihnen können Daten aller Art als sog. Embedded Resources in Assemblies eingebettet werden. In .NET gibt es aber schon von Anfang an ein Format, mit dem man Text-Ressourcen sehr viel leichter erfassen und verwalten kann. Dabei handelt es sich um einfache Text-Dateien mit folgendem Aufbau:

; Kommentare beginnen mit ; oder #
; Texte werden als Name/Wertepaare aufgeschrieben

Bezeichner1 = Das ist ein Text.
Bezeichner2 = Das ist ein anderer Text.

Das ist schon alles, was man zum Aufbau dieser Dateien wissen muss. Bis einschließlich Visual Studio 2005 hatten diese Dateien einfach die Endung .txt und mussten mit dem SDK Tool resgen mehr oder weniger manuell in .resources Dateien konvertiert werden. Im Gegensatz dazu wurden .resx Dateien immer schon automatisch in .resources Dateien umgewandelt. Die .resources Dateien enthalten die Ressourcen in dem internen Format, wie sie in Assemblies eingebettet werden.

Seit Visual Studio 2008 kann man die Endung .restext anstatt .txt verwenden. Wenn man dann noch die Build Action auf Embedded Resource stellt, wird automatisch mit resgen eine entsprechende .resources Datei erzeugt und eingebettet.

Der Zugriff auf die Texte erfolgt wie bei .resx Dateien über die Klasse System.Resources.ResourceManager. Verschiedene Sprachen werden über den Dateinamen unterschieden. Beispielsweise enthält die Datei MyTextStrings .restext die neutrale bzw. default Sprache. Die Dateien MyTextStrings.de.restext und MyTextStrings.fr.restext enthalten die deutschen bzw. französischen Übersetzungen, die Datei MyTextStrings.de-CH.restext die schweizerischen Texte, die von denen aus MyTextStrings.de.restext abweichen usw. Der ResourceManager sucht abhängig von den UI-Einstellungen im laufenden Programm automatisch den richtigen Text heraus.

Der große Vorteil der .restext Dateien liegt in ihrem einfachen Format. Im Gegensatz zu dem für einfache Texte viel zu unübersichtlichen XML-Format der .resx Dateien kann man .restext Dateien ohne Weiteres einem Übersetzer geben. Dieser sieht sofort, was für ihn zu tun ist, wenn er die Datei in einem schlichten Texteditor öffnet. Als Einziges muss beachtet werden, dass die Datei im UTF-8 Format gespeichert werden muss, da resgen Unicode erwartet.

Hier geht's zum nächsten Tipp.

Tags:

.net | Silverlight | WPF

Silverlight Tipp der Woche: ConditionalConverter

by St. Lange 26. August 2010 18:15

In diesem Tipp geht es um das Umschalten zwischen einer Debug- und Release-Ansicht im XAML Designer.

Zusammenfassung

Während man im Code mit #if DEBUG zwischen Debug- und Release-Build unterscheiden kann, gibt es etwas Vergleichbares in XAML nicht. Mit Hilfe eines geeigneten Value Converters kann jedoch Abhilfe geschaffen werden.

Beschreibung

Mit dem #if pragma im Quellcode zwischen verschiedenen Builds zu unterscheiden hat wohl jeder Entwickler schon einmal gemacht. Diese Möglichkeit fehlt mir in XAML, da ich beispielsweise bei komplexeren Layouts während der Entwurfsphase gerne mal bestimmte Bereiche unterschiedlich einfärbe. So kann man viel besser erkennen, ob Margins, Paddings etc. stimmen. Ärgerlich dabei ist, dass man so ein buntes Layout zumindest nicht jedem Kunden zeigen kann und es schön wäre, dieses "Debug Layout" im Release-Build einfach abzuschalten zu können.

Natürlich kann man das mit etwas C#-Code einfach hinbekommen, aber eigentlich will ich die Unterschiede der Builds bereits im Designer sehen. Mit Hilfe des Value Converters ConditionalConverter kann man das erreichen. Das folgende XAML-Fragment zeigt exemplarisch ein Grid, welches im Debug-Build rot und im Release-Build blau ist. Natürlich auch zur Laufzeit, aber vor allem im XAML Designer . Durch den Wechsel des Builds in Visual Studio verändert sich die Anzeige des Designs entsprechend:

<Grid Background="{Binding Converter={StaticResource ConditionalConverter}, ConverterParameter='Red|Blue'}"/>

Der ConditionalConverter liefert je nach Build den ersten oder den zweiten Teil des Parameter-Strings zurück. Das funktioniert natürlich auch mit Zahlen, Texten, Margins etc. Zwar liefert der Converter immer nur Strings, aber diese werden beim Zuweisen an die Property von deren Type Converter entsprechend in den richtigen Typ umgewandelt. Wie das in XAML üblich ist.

Auffällig ist, dass das Binding keinen Path hat. Er wird jedoch auch nicht benötigt, denn das Ergebnis hängt nur von ConverterParameter ab. Die Implementierung ist relativ einfach:

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
  string result = null;
  if (parameter is string)
  {
    var param = ((string)parameter).Split('|');
 
#if DEBUG
    result = param[0];
#else
    if (param.Length >= 2)
      result = param[1];
#endif
  }
 
  // "x:Null" represents the markup extension "{x:Null}".
  if (result == "x:Null")
    result = null;
 
  return result;
}

Das Binding wird zur Designzeit allerdings nur dann ausgeführt, wenn die Property DataContext des Target Objekts und die Source-Property des Bindings nicht beide Null sind, denn in diesem Fall wird der Value Converter gar nicht erst aufgerufen. Sollte dies so sein, kann man die Source-Property im Binding einfach auf irgendeinen Wert setzen, denn da der Path nicht definiert ist, wird sie nicht verwendet. Sie darf nur nicht Null sein:

<Grid Background="{Binding Converter={StaticResource ConditionalConverter},
    Source=xyz,
    ConverterParameter='Red|Blue'}"/>

So ist Source nicht mehr Null und das Binding funktioniert.

Man muss noch beachten, mit dem ConditionalConverter keine Properties zu setzen, die eigentlich durch einen Style gesetzt werden sollten. Das Binding hat Vorrang und der Style kommt nicht zur Anwendung.

Eine weitere theoretische Möglichkeit wäre es, den ConditionalConverter in einem Style zu verwenden, z.B. so:

<Style TargetType="Border">
  <Setter Property="Background"
      Value="{Binding Source=_, ConverterParameter='#FF0000|#00FF00',
      Converter={StaticResource ConditionalConverter}}"/>
</Style>

Das funktioniert aber nicht. Während es in WPF durchaus üblich ist Bindings in Styles zu verwenden, ist diese Funktionalität in Silverlight leider noch nicht implementiert. (Genauer gesagt ist sie bisher nur im Designer implementiert. Das Binding im Style funktioniert bei Silverlight zur Designzeit; wenn man aber die Anwendung startet, gibt es eine Fehlermeldung.)

Expression Blend

In Blend funktioniert das Ganze im Prinzip genauso, allerdings muss hier beachtet werden, dass Blend aktuell ausschließlich Debug-Builds erzeugen kann. Wenn man also in Blend die Release-Ansicht betrachten möchte, muss man vorübergehend die Implementierung von ConditionalConverter ändern. Es reicht, einfach Underscore an #if DEBUG anzufügen und damit die Abfrage umzudrehen.

Code-Beispiel

Zum direkten Ausprobieren gibt es hier noch ein Beispiel: Die MainPage.xaml im Designer öffnen und dann zwischen Debug- und Release-Build hin- und herschalten. Vorher beide Builds einmal compilieren.

ConditionalConverter.zip (16 kB)

Hier geht's zum nächsten Tipp.

Tags:

Silverlight

Silverlight Tipp der Woche: XAP-Dateienrichtig cachen

by St. Lange 15. August 2010 18:30

In diesem Tipp geht es um das Caching von Silverlight XAP-Dateien durch den Browser oder einen Proxy-Server.

Zusammenfassung

Manchmal werden XAP-Dateien zu lange zwischengespeichert und der Anwender arbeitet mit einer älteren Version der Silverlight Anwendung, obwohl auf dem Server bereits eine neuere XAP-Datei vorliegt. Man kann dies verhindern, indem man an die URL der XAP-Datei einen kurzen Parameter anfügt, dessen Wert vom letzten Änderungsdatum der XAP-Datei abhängt.

Beschreibung

Normalerweise braucht man sich über das Caching der XAP-Dateien, in denen sich die gesamte Silverlight Anwendung befindet, keine Gedanken machen. Wenn jedoch beispielsweise ein Kunde in seinem Intranet einen ISA Server als Proxy verwendet, kann es passieren, dass der Kunde die vom Entwickler gerade frisch auf dem Server aktualisierte Silverlight Anwendung nicht verwenden kann, weil ihm sein Proxy-Server die zwischengespeicherte Vorgängerversion der XAP-Datei liefert.

Neu laden der Webseite im Browser bringt da leider gar nichts. Und das Unterdrücken des Cachings der Webseite, in der die Silverlight Anwendung eingebettet ist, hat natürlich auch keinen Erfolg, denn das Problem liegt im Caching der XAP-Datei auf dem Proxy-Server. Aus der Sicht eines Browsers oder Proxy- Servers ist eine XAP-Datei auch nur eine binäre Ressource wie beispielsweise eine PNG- oder JPEPDatei. Daher wird sie genauso zwischengespeichert.

Im HTML-Element <object>, welches die Silverlight Anwendung beschreibt, gibt es einen Parameter, der die URL der XAP-Datei festlegt. Dieser sieht beispielsweise so aus:

<param name="source" value="ClientBin/MySilverlightApplication.xap"/>

Eine mögliche Lösung wäre die aktuelle Versionsnummer in den Dateinamen aufzunehmen:

<param name="source" value="ClientBin/MySilverlightApplication-1.2.3.xap"/>

Dies führt zu einer Aktualisierung des Caches, wenn eine neue Version eingespielt wird, da sich dabei die URL ändert. Es kann jedoch zu neuen Problemen kommen, wenn beispielsweise das <param> Element auf der Webseite nicht zum Namen der XAP-Datei auf dem Server passt.

Es ist daher besser, die Versionsnummer als Dummy-Parameter an die URL anzufügen und den eigentlichen Namen der XAP-Datei unverändert zu lassen:

<param name="source" value="ClientBin/MySilverlightApplication.xap?v=1.2.3"/>

Die Silverlight Runtime interessiert der Text rechts vom Fragezeichen nicht. Und bei einer neue Versionsnummer ändert sich dennoch die URL, sodass der Proxy-Server die XAP-Datei neu vom Web-Server anfordert.

Der Nachteil liegt allerdings darin, dass die Versionsnummer serverseitig immer irgendwie bestimmt werden muss. Daher ist es einfacher, statt der Versionsnummer das letzte Schreibdatum der XAP-Datei als Parameter zu verwenden:

<param name="source" value="ClientBin/MySilverlightApplication.xap?x=2010-08-15_16:17:18"/>

Dieser Wert lässt sich serverseitig mit File.GetLastWriteTime praktisch ohne Overhead ermitteln. Wenn man nicht möchte, dass jeder dieses Datum sieht, kann man es beispielsweise auch einfach in eine Zahl umrechnen. Jetzt muss man nur noch das Caching der Webseite selbst verhindern:

<meta http-equiv="expires" content="0">

Damit haben wir erreicht, dass der Proxy-Server die XAP-Datei zwischenspeichert, solange sie sich nicht ändert, und neu anfordert, sobald sie auf dem Server ausgetauscht wurde.

Diese Methode funktioniert gut mit dem Internet Explorer und dem Firefox in Zusammenhang mit dem ISA Server. Andere Browser haben wir nicht ausreichend getestet. Bei anderen Browsern könnte es beispielsweise sein, dass das Fragezeichen in der URL das Caching im Browser verhindert und die XAPDatei jetzt jedes Mal neu angefordert wird.

Hier geht's zum nächsten Tipp.

Tags:

Silverlight

Silverlight Tipp der Woche: Lokalisierte Ressourcen verwenden

by St. Lange 6. August 2010 18:35

In diesem Tipp geht es um das richtige Einbinden von lokalisierten Ressource-Dateien beispielsweise aus dem Silverlight Toolkit.

Zusammenfassung

Das Control DataPager zeigt auch unter einem deutschsprachigen Windows den Text "Page ... of ..." anstatt "Seite ... von ..." an, wenn man nicht manuell in der Projektdatei das Element <SupportedCultures> manuell anpasst.

Beschreibung

Bei manchen Controls kann es vorkommen, dass der angezeigte Content sprachabhängig ist. Das Control DataPager beispielsweise zeigt "Page ... of ..." an, selbst wenn Windows und Browser Deutsch sind. Ein kurzer Blick mit dem Debugger auf den aktuellen Thread zeigt, dass sowohl CurrentCulture als auch CurrentUICulture wie erwartet auf "de" stehen. Auch die DataPager Property Language im XAML Code auf "de" zu setzen bringt keine Veränderung - der Text bleibt englisch.

Das Problem kennen viele und im Web finden sich diverse Lösungsversuche: Von DataPager eine eigene Klasse ableiten und dann mittels Control Template eine eigene TextBox anlegen oder auch von der Assembly System.Windows.Controls.Data.dll die Strong Name Signatur entfernen und dann mit einem externen Resource Editor den String "Page" auf "Seite" ändern. Das ist aber alles unnötig, denn Microsoft hat sämtlich Controls für die von Silverlight unterstützten Sprachen lokalisiert. Man muss die Verwendung der für die jeweiligen Sprachen vorhandenen Satellite Assemblies nur aktivieren.

Der richtige Weg ist eher unerwartet: Die Projektdatei ist mit einem XML-Editor zu öffnen und im Element SupportedCultures sind die benötigten Sprachen einzutragen, beispielsweise so:

<SupportedCultures>en;de;fr</SupportedCultures>

Dieser Eintrag führt dazu, das beim Build für alle im Projekt referenzierten Assemblies geprüft wird, ob es dazu zusätzliche deutsche bzw. französische Satellite Assemblies gibt. Alle diese zusätzlichen Assemblies werden mit in die XAP-Datei gepackt. Zur Laufzeit werden sie dann von der Klasse ResourceManager automatisch verwendet, sofern die aktuelle Culture Einstellung des Browsers dazu passt. Im Detail ist das hier beschrieben: How to: Create a Build that Targets a Specific Culture

Wenn man genauer darüber nachdenkt, ist es eigentlich logisch, dass man Visual Studio irgendwie mitteilen muss, für welche Sprachen es die Satellite Assemblies mit den lokalisierten Ressourcen in die XAP-Datei packen soll. Es wäre bei Silverlight ja ziemlich unklug, immer alle Satellite Assemblies für sämtliche vorhandenen Sprachen hinzuzufügen, selbst wenn die Anwendung beispielsweise nur für den deutschsprachigen Raum bestimmt ist. Die XAP-Datei würde unnötig vergrößert.

Dass man allerdings das XML der Projektdatei manuell bearbeiten muss, passt überhaupt nicht zu Visual Studio (und schon gar nicht zu Visual Studio 2010). Ein Eingabefeld unter den Silverlight Projektoptionen wäre sehr hilfreich, da vermutlich kaum jemand erwartet, dass eine so grundsätzliche Information direkt in die Projektdatei eingetragen werden muss.

Das Setzten der Property Language aus der Klasse FrameworkElement hat übrigens in Silverlight (im Gegensatz zu WPF) bei den meisten Controls keine weitere Auswirkung. Insbesondere führt der folgende XAML-Code nicht dazu, dass jetzt etwa französische Ressourcen verwendet werden.

<sdk:DataPager Language="fr" .../>

Möchte man die Sprache für eine Anwendung global umschalten, muss man der Property CurrentUICulture des Main-Threads bei Programmstart eine entsprechende neue CultureInfo zuweisen.

Hier geht's zum nächsten Tipp.

Tags:

Silverlight

Powered by BlogEngine.NET 1.6.1.0 - Impressum