Silverlight Snippet: Kompakte Event Handler mit Anonymen Methoden

Oftmals möchte man als Reaktion auf Events, z.B. das Klicken auf einen Button, nur sehr wenige oder sogar nur eine Anweisung ausführen.

In diesem Fall wirkt das übliche Verfahren, auf Events zu reagieren, ziemlich überfrachtet:

button.Click += new RoutedEventHandler(button_Click);

void button_Click(object sender, RoutedEventArgs e)
{
       button.Content = "Clicked!";
}

Wir brauchen hier mehrere Zeilen Code und eine neue Methode in der Klasse, nur um den Content des Buttons beim Klicken umzusetzen. Das sollte auch mit weniger Code möglich sein.

Und tatsächlich gibt es in C# ziemlich praktische Konstruktionen, um gerade solche Fälle enorm abzukürzen: Lambda Ausdrücke und Anonyme Methoden.

Damit kann man den obigen Code auf nur eine Zeile reduzieren:

button.Click += (sender, e) => button.Content = "Clicked!";

Ein Lambda Ausdruck hat in der Regel folgende Form:

(Eingabe Parameter) => Ausdruck

Das bedeutet im Grunde, der Ausdruck (Eingabe Parameter) „wechselt zu“ dem Anweisungsblock rechts. Wir verwenden den Lambda Ausdruck hier, um eine anonyme (namenlose) Methode als Event-Handler zu definieren.

In unserem Fall sind die Eingabe Parameter die Parameter, die in der Event-Handler Methode für das Click-Event erwartet werden: Der erste (sender) vom Typ Object, der zweite (e) vom Typ RoutedEventArgs. Die Typen muss man in diesem Fall nicht explizit angeben – sie werden vom Compiler korrekt aufgelöst.

Nach dem => Operator folgt schließlich die eigentliche Implementierung der Methode. Also das, was sonst im Methodenrumpf stehen würde.

Falls die anonyme Methode aus mehr als einer Anweisung bestehen soll, muss man die Anweisungen in geschweiften Klammern schreiben:

button.Click += (sender, e) =>
{
          button.Width = 32;
          button.Content = "Klein!";
};

Je nach größe des Projektes kann man so eine Menge eigentlich überflüssigen Code einsparen.

Silverlight Tutorial: Transformationen und DataBinding

Willkommen zum vierten Silverlight Tutorial!

Diesmal werden wir uns gleich zwei kleineren und einem größeren Thema widmen:
Zum einen werden wir eine externe Grafik in unsere Silverlight-Webseite einfügen und anhand dessen lernen, wie man GUI-Elemente transformiert.
Zum anderen werden wir uns ein weiteres sehr fundamentales Thema der Silverlight-Entwicklung anschauen: DataBinding.

Ein externes Bild einbinden…

Externe Dateien, die ihr in eure Silverlight-Webseite einbinden wollt, seien es Bilder, Videos oder MP3s, lassen sich auf zwei Arten zu einem Projekt hinzufügen.

Ihr könnt die entsprechende Datei entweder direkt per Drag and Drop in den Solution-Explorer ziehen und dort fallen lassen, oder ihr rechtsklickt auf das Projekt (der oberste Eintrag im Solution-Explorer mit dem grünen Icon) und wählt Hinzufügen -> Vorhandenes Element aus.
Im selben Kontextmenü könnt ihr auch Unterordner für eure Solution erstellen, was praktisch ist, um bei vielen Dateien den Überblick zu behalten.

Erstellt also ein neues Silverlight-Projekt und fügt wie oben beschrieben eine beliebige Grafik ein. Ihr solltet jedoch beachten, dass Silverlight nur die Bildformate JPG und PNG anzeigen kann.

Wenn man externe Dateien ins Projekt einbindet, hat man mehrere Auswahlmöglichkeiten, wie genau sie eingebunden werden, wovon für uns aber momentan nur eine von Bedeutung ist. Sobald ihr die Datei im Solution-Explorer markiert, sehr ihr wie im folgenden Bild deren Eigenschaften:

tut4.3

Dort steht der Buildvorgang auf Resource und Ins Ausgabeverzeichnis kopieren auf Nicht kopieren. Falls die Einstellungen bei euch abweichen, müsst ihr sie wie auf dem Bild gezeigt anpassen.

Ohne ins Detail zu gehen, haben wir damit festgelegt, dass unser Bild zusammen mit der Anwendung direkt beim Start in einem Rutsch geladen wird, und nicht erst wenn es in der GUI angezeigt werden soll.

…und anzeigen

Um das Bild anzuzeigen, werden wir im XAML-Code ein Image-Objekt verwenden. Öffnet dazu die MainPage.xaml und schreibt folgendes:

<Grid x:Name="LayoutRoot">
	<Image Source="EuerBildName.png" />
</Grid>

Ziemlich einfach, oder? Sollte euer Bild in einem Solution-Ordner liegen (also nicht auf der obersten Projektebene wie die anderen Dateien), müsst ihr den Pfad, den ihr der Source-Eigenschaft zuweist, anpassen (z.B. zu ”Images/Bild.png” wenn euer Bild im Ordner Images liegt).

Die Image-Klasse hat neben den üblichen Eigenschaften wie Width und Height die interessante Eigenschaft Stretch.

Sie gibt an, wie das Bild gestreckt wird, falls das umgebende Element (der Container) kleiner als das Bild sein sollte. Ihr könnt beispielsweise dem Grid, in dem unser Image liegt, explizit verschiedene Werte für Width und Height zuweisen und mit der Stretch-Eigenschaft experimentieren, um zu sehen, wie sich das Bild verhält.

None bewirkt, dass das Bild seine Originalgröße behält, auch wenn der Container kleiner ist. Das Bild wird dabei beschnitten, um in den Container zu passen.

Fill bewirkt, dass das Bild den Container immer komplett ausfüllt und auch seine Seitenverhältnisse annimmt, also eventuell verzerrt dargestellt wird.

Uniform bewirkt, dass das Bild sich so weit wie möglich (also nicht unbedingt über die gesamte Breite und Höhe!) im Container ausdehnt, dabei aber seine Seitenverhältnisse beibehält. Dies ist der Standardwert von Stretch.

UniformToFill bewirkt, dass das Bild den gesamten Container ausfüllt und dabei seine Seitenverhältnisse beibehält. Sollte der Container ein anderes Seitenverhältnis haben, wird das Bild dabei eventuell an den Seiten beschnitten.

Elemente transformieren

Anhand unseres Images wollen wir jetzt einige Transformationen ausprobieren, wodurch man interessante visuelle Effekte erreichen kann.

Silverlight stellt fünf verschiedene Transformationen für GUI-Elemente zur Verfügung, von denen wir uns drei ansehen werden:

TranslateTransform verschiebt das Element unabhängig vom Layout beliebig weit in X- und Y-Richtung.

RotateTransform rotiert das Element um den angegebenen Winkel.

ScaleTransform skaliert die Höhe und Breite des Elements. Man könnte also beispielsweise das Element unabhängig von seiner realen Größe damit nur halb so hoch und doppelt so breit machen.

In der Regel beeinflussen Transformationen das Layout System in Silverlight nicht, d.h. verschiebt man zum Beispiel ein Element in einem StackPanel mit einem TranslateTransform, schiebt es dabei nicht seine Nachbarelemente weiter.

Um Transformationen auf ein Element anzuwenden, muss man seine RenderTransform-Eigenschaft mit einem der Transformations-Objekte belegen. Hier ein Beispiel, in dem wir ein RotateTransform auf das Image anwenden und es um 45 Grad rotieren:

<Grid x:Name="LayoutRoot">
<Image Source="EuerBildName.png">
	<Image.RenderTransform>
		<RotateTransform Angle="45" />
	</Image.RenderTransform>
</Image>
</Grid>

Wenn ihr das Programm jetzt ausführt, ist das Bild gedreht – jedoch um seinen Koordinatenursprung (0,0) links oben.

Oftmals möchte man aber, dass Transformationen vom Mittelpunkt des Objektes ausgehen. Hierfür kann man der Eigenschaft RenderTransformOrigin des Elements den relativen Koordinatenursprung zuweisen (zwischen 0,0 oben links und 1,1 unten rechts) , der das Zentrum für die Transformationen sein soll.

Wenn wir den obigen Code folgendermaßen erweitern, sollte sich das Bild um seinen Mittelpunkt drehen:

<Grid x:Name="LayoutRoot">
<Image Source="EuerBildName.png" RenderTransformOrigin="0.5, 0.5">
	<Image.RenderTransform>
		<RotateTransform Angle="45" />
	</Image.RenderTransform>
</Image>
</Grid>

Es ist auch möglich, einem Element mehrere Transformationen zuzuweisen. Dazu muss das Element, das wir als RenderTransform angeben, eine TransformGroup sein. Das ist quasi ein StackPanel für Transformations-Objekte, das wir mit beliebig vielen Transformationen füttern können.

Im folgenden Beispiel rotieren wir das Bild um 45 Grad (RotateTransform), verschieben es um 50 Pixel nach unten und nach rechts (TranslateTransform) und machen es halb so hoch und doppelt so breit (ScaleTransform):

<Grid x:Name="LayoutRoot">
<Image Source="EuerBildName.png" RenderTransformOrigin="0.5, 0.5">
	<Image.RenderTransform>
		<TransformGroup>
			<RotateTransform Angle="45" />
			<ScaleTransform ScaleX="2" ScaleY="0.5" />
			<TranslateTransform X="50" Y="50" />
		</TransformGroup>
	</Image.RenderTransform>
</Image>
</Grid>

Übrigens ist die Reihenfolge der Transformationen wichtig, wenn man sie bei einer TransformGroup angibt. Zuerst eine Rotation und dann eine Translation hat also einen anderen Effekt, als erst eine Translation und dann eine Rotation.

Elemente perspektivisch verzerren

Dieses Thema werde ich hier nur kurz ansprechen, aber ihr findet im herunterladbaren Projekt weiter unten ein ausführliches Beispiel der Anwendung von Transformationen und der perspektivischen Projektion.

Neben den oben genannten Transformationen gibt es in Silverlight noch eine weitere Art, Elemente visuell zu verzerren.

Dies funktioniert ähnlich wie bei der RenderTransform-Eigenschaft: Weist man der Projection-Eigenschaft eines Elements ein PlaneProjection-Objekt zu, kann man visuell einen 3D-Effekt erzeugen, wo das Bild beispielsweise im 3D-Raum gedreht wird. Dazu hat die PlaneProjection-Klasse einige Eigenschaften, die verschiedene Arten von Effekten bewirken.

Eigenschaften verbinden: Das DataBinding

DataBinding ist zusammen mit ein, zwei anderen Themen eines der wichtigsten Konzepte in Silverlight.

Es ermöglicht eine Entkopplung der eigentlichen Applikationslogik von der Benutzeroberfläche und sorgt damit in der Praxis für einen optimalen „Developer-Designer-Workflow“. Das bedeutet, während ein Programmierer C#-Code mit der Applikationslogik schreibt, kann unabhängig davon parallel ein GUI-Designer mit XAML eine hübsche Benutzeroberfläche kreieren. Er muss nur ein Minimum an Wissen haben, welche Daten später an die GUI geliefert werden.

Das Prinzip von DataBinding ist ziemlich schnell erklärt. Man „bindet“ zwei Eigenschaften zweier Objekte zusammen, sodass bei Änderungen der einen Eigenschaft sich auch die zweite Eigenschaft ändert. Die beiden Eigenschaften haben also immer (je nach Binding-Modus jedoch nicht zwingend) den selben Wert, sie sind sozusagen permanent synchron.

Man kann die Eigenschaften zweier GUI-Elemente, die man im XAML definiert hat verbinden, aber auch ein GUI-Element mit einem Objekt aus dem Code-Behind, das Applikationslogik beinhaltet.

In diesem Tutorial werden wir erstmal nur das Binding zwischen GUI-Elementen behandeln.

Ein typisches Beispiel:

Man hat einen Videoplayer in der Oberfläche, dessen aktuelle Abspielposition und Lautstärke man mit zwei Reglern ändern möchte.

In der „konventionellen“ Programmierung würde man sich beispielsweise Event-Handler für den Fall, dass sich die Positionen der Regler ändern, erstellen und in diesen Handlern die Reglerwerte abrufen und zu den entsprechenden Videoplayer-Eigenschaften weiterreichen.

Mit DataBinding verbindet man einfach direkt die „Aktueller-Wert“-Eigenschaft des einen Reglers mit der Lautstärke-Eigenschaft des Videoplayers, und die des anderen mit der Positions-Eigenschaft. Man braucht dazu nur zwei kurze Zeilen XAML-Code.

Hier ein erstes Beispiel, in dem wir den aktuellen Wert eines Reglers in einem TextBlock anzeigen wollen:

<StackPanel Orientation="Horizontal">
	<Slider x:Name="mySlider" Width="128" Minimum="0" Maximum="100" />
	<TextBlock Text="{Binding ElementName=mySlider, Path=Value}" />
<StackPanel>

Wenn ihr diesen Code in die MainPage.xaml schreibt und das Programm ausführt, werdet ihr sehen, wie der aktuelle Wert des Reglers bei Veränderungen automatisch im TextBlock angezeigt wird.

Grafisch sieht das obige Binding in etwa so aus:

tut4.2

Das Binding-Ziel ist die Text-Eigenschaft des TextBlocks.

Die Binding-Quelle ist die Value-Eigenschaft des Sliders, die seinen aktuellen Wert enthält.

Bei Änderungen der Binding-Quelle wird automatisch auch das Binding-Ziel aktuallisiert.

Wie man im Code sieht, erzeugt man ein Binding, in dem man in die Ziel-Eigenschaft die sogenannte Markup-Extension Binding einträgt. Diese hat folgende Form:

ZielEigenschaft=“{Binding ElementName=QuellObjekt, Path=QuellEigenschaft}

Zu beachten ist, dass das Binding via ElementName nur eine Art von DataBinding ist. In späteren Tutorials werden wir eine weitere Art kennenlernen, um Elemente im XAML auch mit Code-Behind Objekten zu verbinden.

Im Normalfall wird also die Ziel-Eigenschaft aktuallisiert, wenn sich die Quell-Eigenschaft ändert. Was ist aber, wenn man auch Änderungen der Ziel-Eigenschaft zurück an die Quell-Eigenschaft weiterreichen möchte?

Hierzu gibt es beim Binding den optionalen Parameter Mode, den man wiefolgt festlegt:

ZielEigenschaft=“{Binding ElementName=QuellObjekt, Path=QuellEigenschaft, Mode=TwoWay}“

Er kann folgende Werte annehmen:

OneWay: Das ist die Standardbelegung und reicht Änderungen nur von der Quelle zum Ziel weiter.

TwoWay: So werden auch Änderungen am Ziel zurück zur Quelle weitergereicht. Die Aktuallisierung der Eigenschaften funktioniert dann in beide Richtungen.

OneTime: Hierbei wird die Ziel-Eigenschaft nur ein einziges mal beim Erzeugen des Bindings aktuallisiert.

Ein kleiner Fallstrick bei der Aktuallisierung des Bindings ist die TextBox. Hier wird es nämlich nicht aktuallisiert, wenn man in ihr herumtippt, sondern erst, wenn sie den (Eingabe-)Fokus verliert.

Bevor wir mit einem größeren DataBinding-Beispiel weitermachen, noch zwei Sachen, die man sich merken sollte:
Erstens: Die Ziel-Eigenschaft muss ein
DependencyProperty sein.

Das sind spezielle Eigenschaften, die nach außen hin wie normale Eigenschaften auftreten und uns erstmal nicht weiter interessieren sollen. Die meisten für das Binding relevante Eigenschaften (Value, Text, Background, …) sind DependencyProperties. Solltet ihr jedoch mal einen Binding-Fehler haben, informiert euch (z.B. auf www.msdn.com), ob die Ziel-Eigenschaft tatsächlich ein DP ist.

Für die Quell-Eigenschaft gibt es keine solche Beschränkung, wie folgende verallgemeinerte Illustration zeigt:

tut4.1

Zweitens: In der Regel müssen die Datentypen der beiden verbundenen Eigenschaften übereinstimmen.

Wenn wir wie oben eine Double-Eigenschaft (Value) an eine String-Eigenschaft (Text) verbinden, konvertiert Silverlight automatisch von double nach string.

Sollte man jedoch auf die Idee kommen, den Value eines Sliders an die Background-Eigenschaft (Typ: Brush) einer Border zu binden, wird das scheitern. Wie sollte auch aus einer Zahl eine Farbe erzeugt werden?

Es gibt in diesem Fall zwar die Möglichkeit, einen sogenannten Converter zu verwenden, um bei verschiedenen Eigenschafts-Datentypen die Werte in den jeweils anderen Typ zu konvertieren, aber das schauen wir uns ebenfalls erst in späteren Tutorials an.

Bis dahin gilt: Binding-Ziel und -Quelle müssen (abgesehen von Zahlen und Strings) den selben Datentyp haben!

Von Slidern, Transformationen und der Magie des DataBindings

Weiter geht’s mit einem DataBinding-Beispiel, in dem wir die Werte einiger Slider an verschiedene Transformationen binden, die dann ein Bild skalieren und rotieren. Hier der komplette Code:

<Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    <Image Grid.Column="0" Source="bild.png"
           Width="640" Height="480"
           RenderTransformOrigin="0.5, 0.5"
           HorizontalAlignment="Center" VerticalAlignment="Center">
      <Image.RenderTransform>
        <TransformGroup>
          <ScaleTransform x:Name="scale" />
          <RotateTransform x:Name="rotation" />
        </TransformGroup>
      </Image.RenderTransform>
    </Image>

    <StackPanel Grid.Column="1"
                VerticalAlignment="Center" Margin="8">
      <Slider Minimum="0.5" Maximum="2" Width="128" Margin="8"
              Value="{Binding ElementName=scale, Path=ScaleX, Mode=TwoWay}" />
      <Slider Minimum="0.5" Maximum="2" Width="128"
              Value="{Binding ElementName=scale, Path=ScaleY, Mode=TwoWay}" />
      <Slider Minimum="0" Maximum="360" Width="128"
              Value="{Binding ElementName=rotation, Path=Angle, Mode=TwoWay}" />
    </StackPanel>

  </Grid>

Wir definieren ein einfaches Grid-Layout mit zwei Spalten:
In der linken Spalte befindet sich ein Image-Objekt, das wir transformieren wollen (passt hier eventuell den Dateinamen und den Pfad an!).In der rechten Spalte ist ein StackPanel, das drei Slider enthält, die die Intensität der Transformationen steuern.

Die Slider-Klasse hat neben der Value-Eigenschaft, die den aktuellen Reglerwert speichert, noch eine Minimum und Maximum Eigenschaft. Die beiden geben (überraschenderweise) den Minimal- und Maximalwert an, die Value annehmen kann.

Die Slider haben wir wiefolgt an die beiden Transformationen des Images gebunden:

Der erste Slider steuert die ScaleX-Eigenschaft des ScaleTransforms, die das Bild horizontal streckt.

Der zweite Slider steuert die ScaleY-Eigenschaft des ScaleTransforms, die das Bild vertikal streckt.

Der dritte Slider steuert die Angle-Eigenschaft des RotateTransforms, die das Bild um den angegebenen Winkel rotiert.

Damit alle Transformationen vom Bild-Mittelpunkt ausgehen, haben wir die RenderTransformOrigin-Eigenschaft des Images auf seinen relativen Mittelpunkt gesetzt. Ihr könnt hier gerne mit verschiedenen Werten zwischen 0 und 1 experimentieren, um den Effekt der Eigenschaft zu sehen.

Bei den Bindings gilt es noch zu beachten, dass wir Mode auf TwoWay gesetzt haben.

Da bei den Bindings die Slider die Binding-Ziele und die Transformationen die Binding-Quellen sind, würden normalerweise die Änderungen bei den Slidern nicht zurück in die Transformationen geschrieben werden. Mit dem Binding-Modus TwoWay ändern wir dieses Verhalten, sodass auch Änderungen beim Binding-Ziel zur Binding-Quelle weitergereicht werden.

Das war’s soweit zum Thema DataBinding und Transformationen.
Unten findet ihr ein ausführliches Beispielprojekt.
Solltet ihr noch Fragen haben, postet ins eality Forum.

Im nächsten Tutorial wird es wieder etwas anschaulicher und bunter zu gehen, wenn wir uns Resourcen, Brushes und Styles ansehen.

Beispielprojekt herunterladen

Silverlight Snippet: Setzen von Attached Properties im Code-Behind

Obwohl es für die meisten Silverlight-Entwickler trivial sein wird, dürften gerade Anfänger sich fragen, wie sie Eigenschaften wie Canvas.Top oder Grid.Row nicht nur im XAML, sondern auch im C# Code-Behind festlegen. Alle Klassen, die solche Attached Properties definieren, haben dazu statische Set- und Get-Methoden, mit dem dies möglich ist.

Zum Beispiel kann man das hier…

<Button x:Name="myButton" Grid.Row="0" />
<Button x:Name="myButton" Canvas.Top="0" />

…folgendermaßen in C# festlegen:

Grid.SetRow(myButton, 0);
Canvas.SetTop(myButton, 0);

Das Abfragen der Werte funktioniert ähnlich:

int row = Grid.GetRow(myButton);
double top = Canvas.GetTop(myButton);

Natürlich könnt ihr so nicht nur die Eigenschaften eines Elements setzen, das ihr in XAML mit einem Namen versehen habt, sondern auch mit einer Referenz auf ein Objekt, das ihr direkt im Code-Behind erstellt habt.

Kaffeepause

Vor kurzem fand ich einen Seite, die in interessanter Form die Geschichte des Lieblingsgetränks eines jeden Programmierers beschreibt: Kaffee. (Obwohl erstaunlich viele unserer Art inzwischen der dunklen Seite des koffeinfreien Genusses verfallen sind: Dem Tee.)

15 Things Worth Knowing About Coffee

Sehr schön gemachte Illustration, wie ich finde.

Und ein kurzes Statusupdate: Ich bin leider gerade ziemlich mit diversen Projekten ausgelastet, sodass ich nicht genau sagen kann, wann ich das nächste Tutorial schreiben kann. Eventuell lade ich als Lückenfüller ein neues kleines Silverlight-Projekt hoch.

Na dann bis zum nächsten Update.

Das eality.de Forum ist online!

Ab heute habt ihr die Möglichkeit, bei Fragen zur Silverlight- und WPF-Programmierung oder für generelles Feedback bezüglich der Webseite das eality.de Forum zu verwenden.

Ihr könnt dort auch als Gast, also ohne Registrierungszwang, Beiträge erstellen.

Ich wünsche allen Besuchern fröhliches posten!

« geh zurückweiter umsehen »