Mittwoch, 20. August 2008

entwickler.com Magazine Konferenzen Entwickler Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag

 Aktuelle Ausgabe

 Leser Service

 Redaktion

 Marketing

aus MSDN Magazin Ausgabe: Volume 1.2007
PIMP MY LISTBOX
Styles, Templates und Custom Controls in XAML
Karin Huber
Bisher war es in Windows-Applikationen nicht gerade einfach, eine ansprechende Benutzeroberfläche zu gestalten. HTML und ASP.NET 2.0 haben dagegen schon seit langer Zeit eine Fülle an Möglichkeiten geboten, um Web-Applikationen ansprechend zu gestalten. Mit der Windows Presentation Foundation (WPF) ändert sich alles! Karin Huber zeigt Ihnen anhand einer ListBox, was man in WPF mit Controls alles anstellen kann.

Bisher war es in Windows-Applikationen nicht gerade einfach eine ansprechende Benutzeroberfläche zu gestalten. Entweder man musste mit den Standard Controls leben oder aber eine Menge Arbeit investieren, um eigene Controls mit hübscherem Aussehen zu entwickeln. Im besten Fall konnte man von bestehenden Controls ableiten oder sie zu neuen Controls zusammenfügen. Für ein außergewöhnlicheres Styling war es schon notwendig, die OnPaint-Methode eines Controls zu überschreiben, um dort mit Zeichenfunktionen ein neues Layout zu bauen.

Im Gegensatz zu Windows-Applikationen bieten HTML und ASP.NET 2.0 schon seit langer Zeit eine Fülle an Möglichkeiten, um die Gestaltung von Web-Applikationen zu vereinfachen wie z.B. Stylesheets, HTML-Components (.htc-Files), MasterPages oder Themes. Mit der Windows Presentation Foundation (WPF) haben sich die Zeiten geändert! Es gibt viele neue Konzepte um Windows-Applikationen ein neues Styling zu verpassen. Die folgenden Beispiele sollen anhand einer ListBox demonstrieren, was man in WPF mit Controls alles anstellen kann. Wir beginnen dazu mit einer einfachen ListBox wie man sie auch in herkömmlichen Windows-Applikationen sieht. Diese verwenden wir, um Ordner und Dateien des Dateisystems darzustellen. Durch folgende Schritte werden wir dann die Darstellung verändern:

  • Anwenden von Styles: Hier werden sich Web-Entwickler gleich wie zu Hause fühlen, denn Styles in WPF erinnern sehr stark an Cascading Stylesheets im Web. In WPF lässt sich mit Styles jedoch noch einiges mehr anfangen als mit Cascading Stylesheets.
  • Anwenden von Templates: Wenn Styles nicht mehr ausreichen, dann kann mit Hilfe eines Templates das Erscheinungsbild eines Controls noch grundlegender verändert werden. Controls lassen sich in WPF ganz einfach in Logik und Design trennen. Man kann die Logik beibehalten, das Design aber von Grund auf nach eigenen Vorstellungen aufbauen.
  • Implementieren eines Custom Controls: In WPF ist es nicht mehr nötig Custom Controls zu schreiben, nur um das Aussehen eines Controls zu verändern. Dazu reichen Styles und Templates aus. Doch wenn man auch eigene Logik kapseln möchte, sind sie unverzichtbar.

File Explorer Listbox
Unsere Testapplikation besteht aus einem TextBlock, in dem ein Pfad aus dem Dateisystem angezeigt wird, und aus einer ListBox, in der alle dazugehörigen Unterordner und Dateien angezeigt werden:

<TextBlock FontWeight="Bold" Name="txtPath" />
<ListBox Name="lstFileBrowser" ItemsSource="{Binding}" />

Der Pfad für den TextBlock und die Datenquelle für die ListBox werden in unserem Beispiel im Code gesetzt. Die Listenelemente sind vom Typ System.IO.DirectoryInfo bzw. System.IO.FileInfo. Abbildung 1 zeigt die unformatierte Listbox.


Abb. 1: Unformatierte ListBox in XAML

Bis jetzt ist noch kein Unterschied zu herkömmlichen Windows-Applikationen zu erkennen. Objekte werden wie bisher durch ihre ToString()-Funktion in Listbox-taugliche Strings umgewandelt. Objekte mit eigener graphischer Repräsentation wie z.B. Buttons oder Textboxen können in WPF jedoch auch graphisch in Listboxen dargestellt werden (siehe Abbildung 2).


Abb. 2: ListBox mit enthaltenen Controls

Styles
Der einfachste Weg, um Controls optisch zu verändern, ist das Definieren von Styles. Wie in CSS ist es in WPF möglich, Styles für alle Objekte eines Typs zu definieren (Attribut TargetType), um damit z.B. alle Listboxen in einer Applikation einheitlich umzugestalten, oder aber einen Key für den Style zu vergeben (Attribute x:Key) um ausgewählte Objekte zu formatieren. Diese Objekte müssen explizit im Attribut Style diesen Key angeben, um entsprechend formatiert zu werden. In unserem Beispiel werden wir der ListBox einen anderen Hintergrund und eine andere Rahmenfarbe zuweisen (siehe Listing 1).

<ListBox Name="lstFileBrowser" ItemsSource="{Binding}">
<ListBox.Resources>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="#ffffdd66" />
<Setter Property="Border.BorderBrush" Value="#ffff9900" />
</Style>

<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#ffff9900" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>

Unter dem ListBox-Tag werden die ListBox.Resources eingefügt. Dort können alle gewünschten Styles definiert werden. Diese gelten nur für jene ListBox. Wenn man alle Listboxen in einem Window umstylen möchte, wäre der geeignete Platz dafür Window.Resources. Applikationsweite Styles finden ihren Platz im File App.xaml unter Application.Resources. Wenn in der Applikation keine anderen Anweisungen gefunden werden, wird von der Applikation auf die Systemressourcen zurückgegriffen.

Im unserem Beispiel werden zwei Styles angelegt. Der erste gilt für Objekte vom Typ ListBox. In ihm werden die Hintergrund- und Rahmenfarbe gesetzt. Der zweite Style gilt für Objekte vom Typ ListBoxItem. Hier wird die Formatierung davon abhängig gemacht, ob sich der Mauszeiger gerade über dem ListBoxItem be- findet. Es wird dazu im Style ein Trigger mit der Bedingung IsMouseOver = True eingefügt. Nur wenn die Bedingung erfüllt ist, wird der Hintergrund auf eine andere Farbe gesetzt. In Abbildung 3 wird das Ergebnis dieser Änderungen dargestellt.


Abb. 3: ListBox mit Styles in XAML

Möchte man die Hintergrundfarbe nicht nur bei IsMouseOver sondern auch bei IsSelected ändern, wird es etwas komplizierter, denn das Standardtemplate für Listboxen – dazu später mehr – erlaubt es nicht, die Hintergrundfarbe für selektierte Elemente zu überschreiben. Es wird immer die Farbe SystemColors.HighlightBrushKey verwendet, d.h. selektierte Elemente werden in Windows XP mit blauem Hintergrund und weißer Schrift dargestellt. Das lässt sich einfach umgehen, indem der Wert für SystemColors.HighlightBrushKey in einer Ressource überschrieben wird. Hierzu fügt man in ListBox .Resources, oder auch auf Window- oder Applikationsebene, eine Ressource vom Typ SolidColor-Brush mit dem entsprechenden Key hinzu und ändert die Farbe nach seinen Wünschen. Für die Hintergrundfarbe ist z.B. folgende Anpassung notwendig:

<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="#ffff9900" />

Die Textfarbe kann mit dem Key HighlightTextBrushKey angepasst werden. Wer mit dieser Lösung nicht glücklich, ist kann über Templates den Aufbau der ListBox so ändern, dass die Formatierung für selektierte Elemente auch über Styles gesteuert werden kann. Wie man an diesem Beispiel sieht, kann man mit Styles das Design eines Controls oberflächlich verändern, schlussendlich besteht unsere ListBox aber immer noch aus einem rechteckigen Container mit einem Scrollbalken auf der rechten Seite und untereinander angeordneten Elementen. Um diese Anordnung aufzubrechen ohne das Verhalten der Listbox zu verlieren, muss mit Templates gearbeitet werden, da hier die Grenzen von Styles erreicht sind.

Templates
In WPF sind Design und Logik von Controls wirklich voneinander getrennt. Beispielsweise enthält ein Button die Logik, auf Klicks zu reagieren. Das Design wird in einem Template festgelegt und ist nicht mehr untrennbar mit dem Code für die Logik verbunden. Jedes Control stellt ein Standardtemplate zur Verfügung. Dieses bietet eine gute Ausgangsbasis für die Entwicklung eigener Templates. Man kann sich die Standardtemplates von Controls im Microsoft Interactive Designer ansehen. Dazu wählt man in der Designansicht ein Control aus und wählt dann im Menü Format | Edit Template | Edit a Copy of the Template. Im Dialog wählt man aus, wo die Kopie des Standardtemplates eingefügt werden soll, und schon kann man sich das Template für das ausgewählte Control ansehen. In Listing 2 wird das Standardtemplate einer ListBox dargestellt. Dieses besteht aus einem Border und einem Scrollviewer. Das Element ItemsPresenter ist ein Platzhalter für die Listenelemente.

<ControlTemplate x:Key="ListBoxControlTemplate1" TargetType="{x:Type ListBox}">
<Border Background="{TemplateBinding Background}"
x:Name="Bd" SnapsToDevicePixels="True"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="1,1,1,1">
<ScrollViewer Focusable="False"
x:Name="ScrollViewer" Padding="{TemplateBinding Padding}">
<ItemsPresenter
x:Name="ItemsPresenter"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
...
</ControlTemplate.Triggers>
</ControlTemplate>

In unserem Beispiel werden wir nun dieses Template überschreiben, und damit die ListBox an unsere Vorstellungen anpassen. Der XAML-Code für dieses Template findet sich in Listing 3.

<ListBox Grid.Column="0" Grid.Row="1" Name="lstFileBrowser" ItemsSource="{Binding}">
<ListBox.Template>
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
Background="{TemplateBinding Background}"
Padding="0,10,0,10"
CornerRadius="15">
<Border.BitmapEffect>
<DropShadowBitmapEffect Direction="320"
ShadowDepth="10" Softness="0.5" Opacity="0.5" />
</Border.BitmapEffect>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<StackPanel IsItemsHost="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</ListBox.Template>

<ListBox.Resources>
<Style TargetType="{x:Type ListBox}">
...
</Style>

<Style TargetType="{x:Type ListBoxItem}">
...
</Style>

<DataTemplate DataType="{x:Type local:DirectoryUpInfo}">
...
</DataTemplate>

<DataTemplate DataType="{x:Type io:DirectoryInfo}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Image Width="19" Height="15">
<Image.Source>
<BitmapImage UriSource="/Images/Directory.png" />
</Image.Source>
</Image>
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type io:FileInfo}">
<DataTemplate.Resources>
<local:BitmapImageConverter x:Key="bitmapImageConverter"/>
</DataTemplate.Resources>
<StackPanel>
<StackPanel Name="pnlOverview" Orientation="Horizontal">
<Image Width="19" Height="15">
<Image.Source>
<BitmapImage UriSource="/Images/File.png" />
</Image.Source>
</Image>
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>

<StackPanel Name="pnlDetails" Orientation="Horizontal"
Visibility="Collapsed">
<Image Width="100" VerticalAlignment="Top"
Source="{Binding Converter=
{StaticResource bitmapImageConverter}}"
Margin="0,0,10,0" />
...
</StackPanel>
</StackPanel>

<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=IsSelected
}"
Value="True">
<Setter TargetName="pnlOverview"
Property="StackPanel.Visibility" Value="Collapsed" />
<Setter TargetName="pnlDetails"
Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.Resources>
</ListBox>

In Abbildung 4 wird das Ergebnis der Neugestaltung dargestellt. Im Template für die ListBox wird ein Border mit abgerundeten Ecken und einem Schatteneffekt eingefügt. Dieser enthält einen ScrollViewer, damit gescrollt werden kann, wenn der zur Verfügung stehende Platz nicht ausreicht. Um die Elemente der ListBox in den ScrollViewer einzufügen, stehen zwei Möglichkeiten zur Verfügung. Entweder man fügt ein Objekt ItemsPresenter ein (wie im Standardtemplate) oder aber man fügt ein Panelobjekt wie z.B. das StackPanel ein und setzt dessen Property IsItemsHost auf True. Die Listbox-Elemente werden dann in dieses Objekt eingefügt. Man könnte somit z.B. horizontale Listboxen ermöglichen, indem man das Orientation-Property des StackPanels auf Horizontal setzt. Oder aber man verwendet ein WrapPanel als ItemsHost, welches die enthaltenen Elemente in Zeilen oder Spalten anordnet und automatisch umbricht. Das Standardtemplate wird nun nicht mehr berücksichtigt aber die Funktionalität der ListBox mit der Möglichkeit zur Selektion und den diversen Klick-Events steht nach wie vor zur Verfügung.


Abb. 4: ListBox mit Template in XAML

DataTemplate
Genauso wie die ListBox könnte man ListBoxItems über ein Template formatieren, doch in unserem Fall bietet sich dafür eine andere Methode an. Die ListBox enthält Elemente vom Typ DirectoryUpInfo (um ein Verzeichnis nach oben zu navigieren), DirectoryInfo (um ein Verzeichnis nach unten zu navigieren) und FileInfo. Es wäre nun schön, wenn für jeden Objekttyp ein passendes Template hinterlegt werden könnte. Bei Dateien könnte man beispielsweise gleich eine Vorschau angezeigen, während bei Verzeichnissen ein Ordnersymbol ausreichen würde. Um das Aussehen von Objekten zu verändern, die keine graphische Repräsentation besitzen, gibt es DataTemplates. DataTemplates können wie Styles behandelt werden. Über den TargetType wird gesteuert, für welchen Objekttyp sie gelten. Je nachdem ob sie nur für ein Element, für ein Window oder aber für eine ganze Applikation gelten sollen, werden sie in die entsprechenden Ressourcen eingefügt. In unserem Beispiel sind sie in den ListBox-Ressourcen enthalten (siehe Listing 3). Das DataTemplate für den Typ DirectoryInfo besteht aus einem Bild (ein Ordner-Symbol) und dem Namen des Ordners. Beim DataTemplate für FileInfo wird es schon etwas komplizierter. Nicht selektierte Elemente sollen, analog zu DirectoryInfo, aus einem Bild und dem Dateinamen bestehen. Für selektierte Elemente sollen aber eine Vorschau der Datei und einige Zusatzinformationen angezeigt werden. Dafür werden in dieses Template zwei StackPanels mit den Namen pnlOverview und pnlDetails eingefügt. Die Visibility von pnlDetails ist auf Collapsed gesetzt. Das Element wird also nicht angezeigt. Um die Sichbarkeit beim Selektieren des Elements zu ändern, ist ein etwas trickreicher DataTrigger nötig. Normale Trigger können nur auf Properties des Elements zugreifen in dem sie enthalten sind (z.B. ListBoxItem.IsSelected), während in einem DataTrigger über Binding-Ausdrücke auch Eigenschaften von übergeordneten Elementen abgefragt werden können. Im DataTrigger für FileInfo können wir im Binding mit FindAncestor das erste übergeordnete Element vom Typ ListBoxItem finden und überprüfen, ob IsSelected erfüllt ist. Wenn ja, wird die Sichtbarkeit der beiden StackPanels vertauscht, sodass nur das Panel pnlDetails sichtbar ist (siehe Listing 3). Für die Vorschau einer Datei gibt es in der Klasse FileInfo leider kein Property, das man direkt im Binding setzen könnte. Wir nehmen daher den Umweg über einen Converter. Dieser kann im Binding zusätzlich angegeben werden und wandelt den im Binding gesetzten Wert in einen anderen Wert um. In unserem Beispiel wird das Objekt FileInfo an den Converter übergeben. Zurückgegeben wird ein Objekt vom Typ BitmapImage. Dieses kann als Source für unser Image-Objekt verwendet werden. In Listing 4 ist der Aufbau unseres Converters dargestellt. Es müssen die Methoden Convert und ConvertBack implementiert werden.

[ValueConversion(typeof(FileInfo), typeof(BitmapImage))]
public class BitmapImageConverter : IValueConverter
{
public object Convert
(object value, Type targetType, object parameter, CultureInfo culture)
{
FileInfo fileInfo = (FileInfo)value;
BitmapImage previewImage = null;
...
return previewImage;
}

public object ConvertBack
(object value, Type targetType, object parameter, CultureInfo culture)
{
// not implemented.
return null;
}
}

Sofern man nur das Design von bestehenden Controls anpassen will, gibt es in WPF keinen Grund mehr, sich mit Custom Controls abzumühen. Will man jedoch eigene Logik in einem Control kapseln, dann sind Custom Controls genau der richtige Weg.

Custom Controls
Grundsätzlich sollte man bei der Implementierung von Custom Controls darauf achten, die Logik vollständig vom Design zu trennen. Custom Controls werden in WPF daher oft als "lookless" bezeichnet. In unserem Beispiel werden wir das Lesen der Dateisysteminformationen und das Navigieren zwischen den Verzeichnissen in ein Custom Control auslagern, um die Wiederverwendung an verschiedenen Stellen zu ermöglichen. Zusätzlich werden wir einen FileSystemWatcher einbauen, um Änderungen an Verzeichnissen und Dateien sofort in der ListBox anzuzeigen, ohne dass die ListBox aktualisiert werden muss. Dazu legen wir ein neues Custom-Control-Library-Projekt an. In dieses werden für unsere Zwecke zwei neue Custom-Control-Klassen eingefügt. Die erste namens FileExplorer ersetzt unsere bisherige ListBox und leitet daher auch von ListBox ab, um möglichst viel von deren Funktionalität zu nutzen (z.B. die Eigenschaften IsMouseOver und IsSelected sowie diverse Klick-Events). Wenn man keine bereits vorhandene Funktionalität eines bestehenden Controls benötigt, bietet sich die Klasse System.Windows.Controls.Control als Basisklasse an. Unser zweites Custom Control FileExplorerItem stellt ein Element im FileExplorer dar. Es kümmert sich selbst darum, dass ein Icon und – wenn möglich – auch eine Vorschau für die Datei als Property zur Verfügung gestellt werden. Wenn man das Template überschreiben möchte, muss man nicht mehr auf Converter zurückgreifen,um eine Vorschau zu bekommen. Stattdessen kann man im Binding direkt auf das Property zugreifen. Man muss jedoch einiges berücksichtigen, wenn man Properties in Custom Controls zur Verfügung stellen möchte, die dann auch in Bindings verwendet oder sogar verändert werden können. Dazu werden Dependency Properties anstelle normaler Properties benötigt. Diese werden vom Framework außerhalb des Objekts verwaltet und werden im Code für das Custom Control folgendermaßen angelegt:

public static DependencyProperty PathProperty = DependencyProperty.Register(
"Path",
typeof(string),
typeof(FileExplorer),
new PropertyMetadata("", new PropertyChangedCallback(OnPathInvalidated)));

Der PropertyChangedCallback wird nur benötigt, wenn man im Programm auf Änderungen des Properties reagieren möchte. In unserem Beispiel werden wir bei einer Änderung die Liste der Dateien aus dem angegebenen Verzeichnis neu laden:

private static void OnPathInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FileExplorer fileExplorer = (FileExplorer)d;
fileExplorer.UpdateList();
}

Der Zugriff auf das Property erfolgt über die Methoden GetValue und SetValue und kann durch entsprechende Getter und Setter gekapselt werden:

public string Path
{
get
{ return (string)GetValue(PathProperty); }
set
{ SetValue(PathProperty, value); }

Die Klasse FileExplorer besteht zusätzlich noch aus dem Code zum Laden der Verzeichnisse und Dateien, der Navigation durch die Verzeichnisse und einem FileSystemWatcher, um Änderungen im aktuell ausgewählten Verzeichnis zu überwachen. Die Klasse FileExplorerItemist dagegen sehr viel einfacher. Sie besteht nur aus einigen Dependency Properties wie z.B. Name, Icon oder Preview. Alle diese Properties können in Templates im Binding verwendet werden. Die Logik der beiden Klassen wäre somit komplett, allerdings kann das Control so noch nicht verwendet werden, da jeder, der es verwenden möchte, das Design dafür von Grund auf selbst implementieren müsste. Das wäre zwar möglich, aber nicht sehr komfortabel. Man findet daher in Custom-Control-Projekten im Verzeichnis themes die Datei generic.xaml. In dieser kann man für alle Controls im Projekt ein Standardtemplate festlegen. In Listing 5 wird unser Standardtemplate für die Klasse FileExplorer dargestellt.

<Style TargetType="{x:Type local:FileExplorer}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FileExplorer}">
<Border BorderBrush="Gray" BorderThickness="1"
Background="{TemplateBinding Background}">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<StackPanel IsItemsHost="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

In der Klasse FileExplorer selbst muss das Standardtemplate dann in einem static-Konstruktor zugewiesen werden:

static FileExplorer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FileExplorer),
new FrameworkPropertyMetadata(typeof(FileExplorer)));
}

Das Standardtemplate für FileExplorerItems stellt den Namen des Verzeichnisses oder der Datei mit einem passenden Icon dar. Zusätzlich ist ein Trigger enthalten, der einen TextBlock mit dem Text CHANGED auf sichtbar setzt, wenn das Dependency Property IsChanged der Klasse FileExplorerItem auf True

<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsChanged,
RelativeSource={RelativeSource TemplatedParent}}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>

In unserem Beispiel ist die Logik vollständig vom Design getrennt. Die Klasse muss nicht wissen, welche Controls das Template zur Darstellung verwenden wird. Nicht immer kann die Trennung so streng eingehalten werden. Das ComboBox-Control besteht beispielsweise immer aus einer TextBox und einem Popup. Die Klasse ComboBox ist daher mit folgenden Attributen versehen:

[TemplatePartAttribute(Name="PART_EditableTextBox", Type=typeof(TextBox))] 
[TemplatePartAttribute(Name="PART_Popup", Type=typeof(Popup))]

Im Code von ComboBox kann davon ausgegangen werden, dass im Template eine TextBox mit Name PART_Edit-ableTextBox und ein Popup mit Namen PART_Popup vorhanden sind. Wenn man das Template überschreiben möchte, muss man darauf achten, diese beiden Controls mit genau diesen Namen auch einzubauen. Die Anwendung unseres FileExplorer Controls ist jetzt denkbar einfach. Damit auf die neuen Klassen zugegriffen werden kann, muss der Namespace mit dem Namen des Assembly im Page- oder Window-Tag angegeben werden:

<Page ...
xmlns:fileExplorerControls="clr-namespace:FileExplorerControls;assembly=
FileExplorerControls"
>

Die Einbindung in die Seite sieht dann folgendermaßen aus:

<TextBlock Grid.Column=“0“ Grid.Row="0" FontWeight="Bold" 
Text="{Binding ElementName=lstFileExplorer, Path=Path}" />
<fileExplorerControls:FileExplorer Grid.Column="0" Grid.Row="1"
Name="lstFileExplorer" Path="c:\" />

Im TextBlock wird, wie in den vorhergehenden Beispielen, der gerade ausgewählte Pfad dargestellt. Diesmal wird der Pfad aber nicht im Code gesetzt, sondern über ein Binding direkt aus unserem Custom Control über das Dependency Property Path ausgelesen. Würden wir den TextBlock in eine TextBox umwandeln, dann könnte man in diese einen Pfad eingeben. Durch das Verlassen der TextBox würde über das Binding das Property Path im FileExplorer sofort geändert, und es würden die neuen Dateien angezeigt werden. In unserem Beispiel wird der Startpfad aber im Control FileExplorer auf C:\ gesetzt. Das Ergebnis wird in Abbildung 5 dargestellt.


Abb. 5: File Explorer mit DefaultTemplate aus Generic.xaml

Der Unterschied zu unserer ersten ListBox ist nicht gleich ersichtlich. Diesmal brauchen wir jedoch keinen Code mehr zu schreiben, um die Navigation durch die Ordner zu ermöglichen, da die gesamte Logik im Custom Control bereits enthalten ist.

Custom Controls mit Templates Auch bei Custom Controls ist es einfach, das Standardtemplate für die Darstellung zu überschreiben. In Listing 6 wird für das FileExplorer-Control ein neues Template gesetzt.

<fileExplorerControls:FileExplorer Grid.Column="0" Grid.Row="1" 					         Name="lstFileExplorer" Path="c:\">
<fileExplorerControls:FileExplorer.Template>
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1"
Background="{TemplateBinding Background}"
Padding="0,10,0,10" CornerRadius="15">
<Border.BitmapEffect>
<DropShadowBitmapEffect Direction="320"
ShadowDepth="10" Softness="0.5" Opacity="0.5v />
</Border.BitmapEffect>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<StackPanel IsItemsHost="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</fileExplorerControls:FileExplorer.Template>

<fileExplorerControls:FileExplorer.Resources>
...
</fileExplorerControls:FileExplorer.Resources>
</fileExplorerControls:FileExplorer>

In Abbildung 6 wird das neue Template mit zwei selektierten Items dargestellt.


Abb. 6: FileExplorer mit neuem Template

Custom Controls mit eigenem Rendering
Wem die bisher vorgestellten Möglichkeiten nicht ausreichen, der kann selbstverständlich immer noch direkt im Code, durch absolutes Platzieren von Objekten, jedes gewünschte Design realisieren. Dazu müssen die beiden Methoden MeasureOverride und ArrangeOverride der Klasse System.Windows.FrameworkElement (Basisklasse von System.Windows.Controls.Control) überschrieben werden. In MeasureOverride werden alle Child-Elemente vermessen, und dem Container des Controls wird die gewünschte Gesamtgröße für das Control zurückgegeben. Ein Panel z.B. muss die Höhen der einzelnen Elemente addieren, da die Elemente untereinander angeordnet werden. Zur Ermittelung der Breite wird das breiteste Element gesucht. In ArrangeOverride bekommt das Control dann die Information, wie viel Platz der Container ihm tatsächlich zur Verfügung stellt. Die Child-Elemente müssen dann entsprechend des verfügbaren Platzes positioniert werden.

Zusammenfassung
Mit WPF bekommen Entwickler eine Vielzahl an Möglichkeiten, mit denen sich das Aussehen ihrer Applikationen tunen lässt. Bei all den neuen Features wird es jedoch nicht unbedingt einfacher, wirklich gute Applika- tionen zu schreiben. Man tendiert leicht dazu, viele graphische Spielereien einzubauen ohne konkreten Nutzen für die Anwender der Applikation zu erzielen. Auch Sie können sich wahrscheinlich noch gut an die ersten HTML-Sites mit hunderten verschiedenen Farben, blinkenden Texten und herumfliegenden Elementen erinnern. Das ist für Anwender nervig und auch die Performance leidet schnell unter zu vielen grafischen Effekten. Auf der anderen Seite haben wir jetzt endlich die Mittel an der Hand, um optisch das Beste aus unseren Applikationen zu machen. Sinnvoll eingesetzt können wir Anwendern das Arbeiten wesentlich erleichtern und über Styles und Templates eine konsistente Benutzeroberfläche zur Verfügung stellen. Schließlich zählt immer der erste Eindruck!

Karin Huber (k.huber@cubido.at) ist Mitgründerin von cubido business solutions GmbH, einem Microsoft Gold Certified Partner. Sie arbeitet als Software Architect mit besonderem Schwerpunkt auf ASP.NET und WPF.



Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie direkt über unser


 zurück
zurück
zum Seitenanfang
top
drucken
drucken
Artikel weiterempfehlen
empfehlen




 
Software & Support Verlag GmbH