Freitag, 29. 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: 2.2007
Data Editing Challenge mit XAML
WPF – A Life without DataGrids
von Karin Huber und Rainer Stropek
Grenzenlose Begeisterung, Ernüchterung, Frust, Erkenntnis und schlussendlich die realistische Einschätzung der Möglichkeiten. Das sind unserer Erfahrung nach die Stufen, die ein Entwickler durchläuft, wenn er die ersten Schritte in der Windows Presentation Foundation (WPF) wagt.

Uns ist noch kaum jemand untergekommen, der angesichts der in Beispielanwendungen plakativ vorgeführten Möglichkeiten von WPF nicht begeistert gewesen wäre und voller Tatendrang seine erste Anwendung mit XAML & Co zu schreiben begonnen hat. Es folgt meist die Ernüchterung darüber, dass der Einstieg WPF alles andere als banal ist. Wie auch in anderen Entwicklungsumgebungen decken die „Hello-World“-Beispiele eben doch nicht alle Themen ab, die man für Anwendungen im wirklichen Leben braucht.


Abb. 1: EditGrid, eine editierbare ListView in WPF

Die Ernüchterung verwandelt sich schnell in Frust wenn man mit Programmen loslegen möchte, bei denen es um Darstellung und Verwaltung von Daten geht, die in Tabellenform vorliegen. Je nachdem ob man von seiner Erfahrung her aus der Welt der Web-Anwendungen kommt oder zur Gattung der Windows-Full-Client-Entwickler zählt, man sucht verzweifelt nach den in der Vergangenheit so nützlichen DataGrid’s oder DataGridView’s – ohne Erfolg! Falls Sie diesen Prozess mit WPF schon hinter sich gebracht haben und jetzt kurz davor stehen diese Plattform nach dem Motto „hübsch aber (noch) nicht brauchbar“ für sich abzuhaken, raten wir dazu, der schönen neuen WPF-Welt noch eine Chance zu geben – zumindest bis zum Ende dieses Artikels. Wir möchten Ihnen erklären wie Sie mit wenig Code die zentralen Bausteine für datenorientierte WPF-Anwendungen erfolgreich zusammenstellen können.

Fesselspiele
Mit Data Binding bezeichnet man generell den Mechanismus wie Elemente aus der Benutzerschnittstelle mit der Geschäft slogik oder Datenzugriffsschicht verbunden werden. Richtig konfiguriert ist keine Programmierung mehr notwendig, um Daten aus einer Datenquelle ins Front-End zu übertragen. Selbst der umgekehrte Weg, also das Zurückschreiben von Änderungen durch einen Benutzer, wird automatisiert. Der Data Binding Mechanismus von WPF geht jedoch weit über diese, aus anderen Plattformen bekannte Funktionalität hinaus: Es lassen sich alle Properties, die als Dependency Properties umgesetzt sind, an beliebige CLR-Objekte oder auch andere Dependency Properties binden. Es können also Steuerelemente an Datenbankspalten genauso gebunden werden wie Steuerelemente an andere Steuerelemente.

Die beiden wichtigsten Typen von Bindungen sind One Way und Two Way Bindings. Ein One Way Binding sorgt dafür, dass das Binding Target automatisch angepasst wird wenn sich die Binding Source verändert. Ein typisches Anwendungsbeispiel dafür wäre eine Listbox, die an das Ergebnis einer Datenbankabfrage gebunden ist. Listing 1 zeigt den Code für diesen Anwendungsfall. Die Listbox ContentListBox wird an das Ergebnis eines Select Statements gebunden.

Listing 1: One Way Binding für ListBox
-------------------------------------------------------------------------
<Window x:Class="SimpleDataBind.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleDataBind" Height="300" Width="300"
>
<Grid>
<ListBox x:Name="ContentListBox" ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

[...]
protected override void OnInitialized(EventArgs e)
{
// open a connection to the database
SqlConnection conn = new SqlConnection("server=.;database=AdventureWorks;Integrated Security=SSPI");
conn.Open();

// Get query result and store it in a DataTable object
DataTable dt = new DataTable();
SqlDataAdapter adapt = new SqlDataAdapter("select FirstName+' '+LastName as Name from Person.Contact", conn);
adapt.Fill(dt);

// Close the database connection
conn.Close();

// set the data context for the listbox
ContentListBox.DataContext = dt;

base.OnInitialized(e);
}

Two Way Bindings werden beispielsweise für editierbare Eingabedialoge verwendet. In diesen Fällen werden die Elemente der Benutzerschnittstelle mit den Inhalten aus der Geschäftslogikschicht (z. B. Datenbankabfrage) befüllt, und Änderungen können wieder zurückgeschrieben werden. Das unten näher erläuterte Beispiel der editierbaren ListView verwendet Two Way Binding.

Ohne Zweifel wurden Web- und Full-Client-Entwickler bisher von Visual Studio verwöhnt, wenn es darum ging, den Inhalt einer Datenbank oder von Geschäftsobjekten auf den Bildschirm zu bringen. Das Entwicklungswerkzeug bot eine große Zahl an Assistenten und Eigenschaftsdialogen mit denen man die Verbindung zwischen Benutzerschnittstelle und Hintergrundlogik zusammenklicken konnte. Im Endeffekt ließ sich eine einfache Datenbank-Anwendung zusammenstellen, ohne dass man sich näher mit Klassen wie SqlConnection, SqlDataAdapter oder SqlCommand beschäftigen musste. Die Assistenten genierten den notwendigen Code zum überwiegenden Teil automatisch. Bei WPF-basierenden Anwendungen gibt es diese Funktion – zumindest in der aktuellen Version von Visual Studio – nicht. Praktisch ist diese Einschränkung aber nur selten ein Nachteil, denn unserer Erfahrung nach stieß man auch früher bei größeren, komplexeren Anwendungen rasch an die Grenzen der Datenzugriffs-Assistenten. Man musste sich selbst um die Erstellung einer Datenzugriffsschicht kümmern. Data Binding ist so allgemein und flexibel gestaltet, dass die Chancen gut stehen, dass ihre bestehende Datenzugriffs- oder Geschäftslogikschicht damit mit einem WPF-Front-End zu verbinden ist.

Eingabehilfen
Es ist schon unglaublich, welche grafischen Effekte mit WPFControls zu machen sind. Für die Umsetzung einer Eingabemaske steht aber leider nur eine recht überschaubare Anzahl an Steuerelementen zur Verfügung. Im Lieferumfang von WPF fehlen noch recht grundlegende Controls wie beispielsweise ein Date-Picker-Control. Abhilfe schafft hier zum Teil die rasch wachsende Gruppe von Steuerelement-Entwicklern. Besonders empfehlen möchten wir den Blog von Kevin Moore, Program Manager bei Microsoft in Redmond. Er hat eine Sammlung der wichtigsten WPF-Controls zusammengestellt, die im Standard noch fehlen.

Trotzdem ist damit zu rechnen, dass sich in der Praxis immer wieder die Notwendigkeit ergibt, eigene Steuerelemente zu entwickeln, die dem Anwender das Leben erleichtern. Diesem Artikel liegt ein Beispiel bei, in dem wir exemplarisch eine Textbox zur Eingabe von Zahlen implementiert haben. Die Grundlage dafür ist die Klasse System.Windows.Controls.TextBox, die wir als Basisklasse für unser Custom Control NumberEditControl verwenden. Um Data Binding zu ermöglichen, erweitern wir NumberEditControl um Dependency Properties wie Value, IsNull und NumberFormat:

public decimal Value
{
get { return (decimal)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(decimal), typeof(NumberEditControl),
new UIPropertyMetadata(new PropertyChangedCallback(OnValueChanged)));
public static void OnValueChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
NumberEditControl control = (NumberEditControl)d;
control.IsNull = false;
control.prevValue = (decimal)e.NewValue;
control.UpdateTextBoxText();
}

Die Besonderheit von NumberEditControl im Vergleich zu TextBox ist die Unterstützung des Benutzers während der Eingabe. Die Logik ist in den Methoden OnKeyDown und OnTextChanged umgesetzt. OnTextChanged ist deshalb zusätzlich zu OnKeyDown notwendig, da OnKeyDown von .NET bei speziellen Tasten wie Backspace und Delete nicht ausgelöst wird.

NumberEditControl zeigt schön, wie spezielle Controls, die auf bestehenden basieren können, mit wenig Aufwand zu erstellen sind. Das Ergebnis ist eine Sammlung an Steuerelementen, die im verwendenden Programm in XAML wie ganz normale Standard- Controls verwendet werden können:

<Window x:Class="CustomEditControlsTester.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CustomEditControlsTester" Height="300" Width="400"
xmlns:custEdit="clr-namespace:Cubido.Windows.Controls; assembly=CustomEditControlsLibrary"
>
[...]
<custEdit:NumberEditControl Margin="5,5,5,5" Value="0" IsNull="false"
x:Name="numEdit" NumberFormat="#,##0.00" CultureName="de-AT"/>
[...]

Grid ≠ ListBox ≠ ListView
Nachdem wir mittlerweile wissen, wie wir unsere Benutzeroberfläche mit der Datenbank verbinden (Data Binding) und sogar individuelle Steuerelemente zur Steigerung der Benutzerfreundlichkeit schreiben können, stellt sich spätestens jetzt die Frage nach einer tabellarischen Darstellung von Daten. Die Suche nach einer Entsprechung eines DataGrid aus ASP.NET oder einer DataGridView aus Windows Forms wird schnell zu einem Abenteuer. Tabelle 1 stellt die verschiedenen WPF Container Controls gegenüber, die theoretisch für eine tabellarische Darstellung verwendet werden könnten. Praktisch ist jedoch dafür nur System.Windows .Controls.ListView verwendbar.


Tabelle 1: WPF Standardelemente für Tabellendarstellung
Control Beschreibung
Grid Container, der andere UI-Elemente in Zeilen und Spalten anordnen kann. Da ein Grid kein Scrolling unterstützt ist es nicht alleine für tabellarische Darstellung von Daten geeignet.
ListBox Stellt eine Liste von beliebigen Elementen (nicht nur Text!) dar, in welcher der Benutzer Elemente auswählen kann. Ist für tabellarische Darstellung von Daten nur bedingt geeignet da mehrere Spalten nur aufwendig umgesetzt werden können.
ListView mit GridView Basiert auf ListBox und kommt dem ASP.NET DataGrid (bzw. GridView) am nächsten. Unterstützt die tabellarische Darstellung von Daten in verschiedenen Formaten.
ScrollViewer Basissteuerelement für ListBox und andere, mächtigere Controls. Alleine nicht für tabellarische Darstellung von Daten geeignet.

ListView ist abgeleitet von ListBox. Anzeigelogik und Verwaltung der enthaltenen Unterelemente sind bei ListView strikt getrennt. Um die Anzeigelogik festzulegen, verwendet man einen View Mode, der über die Eigenschaft View gesetzt wird. Die WPF Standardbibliothek liefert einen View Mode mit aus: GridView. Mit GridView kann eine ListView Datenelemente in Form einer Tabelle mit frei definierbaren Spalten darstellen.

<Window x:Class="SimpleDataBind.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleDataBind" Height="300" Width="300"
>
<Grid>
<ListView ItemsSource="{Binding}" x:Name="ContentListView">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Path=FirstName}"
Header="First Name"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=LastName}"
Header="Last Name"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>

Das ListView-Steuerelement ist also hervorragend dafür geeignet, Daten aus einer Datenzugriffs- oder Geschäftslogikschicht tabellarisch darzustellen. Mit ein wenig Code lassen sich auch erweiterte Ansichten wie zum Beispiel gruppierte Listen rasch umsetzen. Unserer Erfahrung nach ist dies in vielen Fällen jedoch nicht genug. Durch Anwendungen wie Microsoft Outlook oder Excel sind es Anwender gewohnt, dass Daten nicht nur in einer Tabelle angezeigt sondern auch geändert werden können. Diese Anforderung ließ sich in der Web- und der Full-Client-Welt mit den dort vorhandenen Steuerelementen auch wunderbar lösen. ListView bringt leider keine Vorkehrungen dafür mit. Im Gegenteil, auf vielen Blogs von Microsoft-Mitarbeitern wird betont, dass die ListView dafür im Augenblick nicht geeignet ist. Wie auch immer, wir werden tagtäglich mit dieser Anforderung konfrontiert und sehen diese Funktion auch als unverzichtbaren Bestandteil von datenorientierten Anwendungen. Und es gibt auch eine Lösung...

Editieren in der ListView
Damit die Zeilen der ListView editierbar werden, greifen wir auf einen einfachen Trick zurück. Im letzten Beispiel oben haben wir bereits gezeigt wie mit GridViewColumn der Inhalt einer jeden Zelle festgelegt werden kann. Die Herausforderung im Falle einer editierbaren ListView ist, dass der Inhalt einer jeden Zelle nicht immer gleich bleibt: Ist die Zeile selektiert soll sie veränderbar sein, d. h. sie muss Steuerelemente zur Eingabe von Daten (z.B. TextBox) enthalten. Alle anderen, nicht editierbaren Zeilen stellen den Text unveränderbar dar (z.B. mit TextBlock). Die darzustellenden Daten sind jedoch im Hintergrund immer die gleichen. Sie
sind über Data Binding einmal an das Eingabesteuerelement und im anderen Fall an den statischen TextBlock gebunden. Abbildung 3 stellt die Logik grafisch dar. In der blau hinterlegten, selektierten Zeile sieht man die TextBox während bei allen anderen Zeilen der Textblock angezeigt ist.


Abb. 3: Ein Control, unterschiedliche Templates

Zur Kapselung der beschriebenen Umschaltlogik wurden für die einzelnen Steuerelemente, die in der ListView zum Einsatz kommen sollen, eigene abgeleitete Varianten implementiert (z.B. EditGridTextBox). Sie bestehen im Wesentlichen aus einem Style der im Normalfall (nicht editierbar) die Daten statisch darstellt. Bei EditGridTextBox wird dafür zum Beispiel ein TextBlock verwendet. Mit Hilfe eines Triggers auf das Property IsSelected wird bei Markierung der Zeile das Template durch ein neues Control-Template ausgetauscht, das statt dem TextBlock eine TextBox verwendet. Dadurch ist die selektierte Zeile veränderbar. Listing 2 zeigt den XAML Code für EditGridTextBox und EditGridNumber. Letzteres verwendet das bereits oben vorgestellte NumberEdit-Control um eine numerische Spalte zu verwalten. Außerdem wird bei diesem Control auf die Template-Switching Logik verzichtet. Stattdessen binden wir das IsEnabled an das IsSelected Property; hier kommt also Data Binding ohne Datenbank sondern als Verbindung zwischen Dependency Properties zum Einsatz.

Listing 2: Umschalten zwischen editierbarer und statischer Zelle
------------------------------------------------------------------------
<!-- EditGridTextBox -->
<Style TargetType="{x:Type local:EditGridTextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EditGridTextBox}">
<!-- TextBlock is used if control is not in edit-mode (IsSelected==false) -->
<TextBlock Text="{Binding Path=Value, RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
</Setter.Value>
</Setter>

<!-- Trigger that switches template if IsSelected becomes true -->
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EditGridTextBox}">
<!-- TextBox is used if control is in edit-mode (IsSelected==true) -->
<TextBox Text="{Binding Path=Value, RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>

<!-- EditGridNumber -->
<Style TargetType="{x:Type local:EditGridNumber}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:EditGridNumber}">
<local:NumberEditControl TextAlignment="Right" Value="{Binding Path=Value, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
IsEnabled="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent}}" CultureName="de-AT" NumberFormat="#,##0.00"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<!--
In this case we do not need a trigger that switches the template if IsSelected becomes true because we
just disable the control in read-only mode (see binding of IsEnabled above).
-->
</Style>

Die eigentliche editierbare ListView ist in der Klasse EditGrid implementiert. Von außen wird EditGrid verwendet wie eine normale ListView. Eine Besonderheit dabei ist aber, dass jetzt eine Einbahnstraße nicht mehr ausreicht. Wir verwenden jetzt Two Way Binding. EditGrid schreibt schließlich auch in die Datenbank zurück:

<controls:EditGrid Name="grdProducts" ItemsSource="{Binding Path=Table}" Grid.Row="2" Grid.Column="0">
<ListView.View>
<GridView>
[...]
<GridViewColumn Header="Name" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<controls:EditGridTextBox Name="ctrName" Value="{Binding Path=Name, Mode=TwoWay}" IsSelected="{Binding Path=IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListViewItem}}}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
[...]
</GridView>
</ListView.View>
</controls:EditGrid>

Die eigentliche Kommunikation mit der Datenbank übernimmt ADO.NET. Das verwendende Programm stellt dem EditGrid ein DataSet mit den enthaltenen Daten und einen DataAdapter mit den SQL-Kommandos zum Datenaustausch mit dem SQL Server zur Verfügung. Den Rest übernimmt WPF Data Binding. Listing 3 zeigt den Aufbau von DataSet und DataAdapter im aufrufenden Programm. Da die Steuerelemente innerhalb der ListView über Two Way Binding an das DataSet gebunden sind werden Änderungen, die der Benutzer in der Benutzerschnittstelle vornimmt, automatisch ins DataSet eingetragen. Die Implementierung von EditGrid muss nur an den richtigen Stellen (z.B. wenn sich die selektierte Zeile ändert) dafür sorgen, dass die Änderungen aus dem DataSet über den DataAdapter in die Datenbank geschrieben werden:

private void EditGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
if (e.RemovedItems.Count > 0)
{
if (e.RemovedItems[0] is DataRowView)
{
if (((DataRowView)e.RemovedItems[0]).Row.RowState != DataRowState.Unchanged)
{
// update the data source using the data adapter
this.DataAdapter.Update(this.DataSet);
}
}
}
}
catch (Exception ex)
{
[...]
}
}

Abbildung 4 stellt den Zusammenhang zwischen Data Binding, DataSet und DataAdapter grafisch dar:

Listing 3: DataAdapter für EditGrid private void LoadData()
-----------------------------------------------------------------------------
{
SqlConnection connection = new SqlConnection(
System.Configuration.ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString);
// create and initialize data adapter. It is used to read and write data from/to the database.
// the product ListView is bound to the data adapter.
// SELECT
SqlDataAdapter dataAdapter = new SqlDataAdapter(
@"select p.ProductNumber, p.[Name], p.MakeFlag, p.ListPrice, p.SellStartDate, p.SellEndDate, p.ThumbNailPhoto
from Production.Product p
where p.FinishedGoodsFlag = 1
order by p.ProductNumber;", connection);

// UPDATE
dataAdapter.UpdateCommand = new SqlCommand(
@"update Production.Product
set ProductNumber = @ProductNumber, [Name] = @Name, MakeFlag = @MakeFlag, ListPrice = @ListPrice,
SellStartDate = @SellStartDate, ThumbNailPhoto = @ThumbNailPhoto
where ProductNumber = @ProductNumber;", connection);
dataAdapter.UpdateCommand.Parameters.Add("@ProductNumber", SqlDbType.NVarChar, 25);
dataAdapter.UpdateCommand.Parameters[0].SourceColumn = "ProductNumber";
dataAdapter.UpdateCommand.Parameters.Add("@Name", SqlDbType.NVarChar, 50);
dataAdapter.UpdateCommand.Parameters[1].SourceColumn = "Name";
[...]

// INSERT
dataAdapter.InsertCommand = new SqlCommand(
@"insert into Production.Product ([Name], ProductNumber, MakeFlag, ListPrice, SellStartDate, ThumbNailPhoto)
values (@Name, @ProductNumber, @MakeFlag, @ListPrice, @SellStartDate, @ThumbNailPhoto);", connection);
dataAdapter.InsertCommand.Parameters.Add("@ProductNumber", SqlDbType.NVarChar, 25);
dataAdapter.InsertCommand.Parameters[0].SourceColumn = "ProductNumber";
dataAdapter.InsertCommand.Parameters.Add("@Name", SqlDbType.NVarChar, 50);
dataAdapter.InsertCommand.Parameters[1].SourceColumn = "Name";
[...]

// DELETE
dataAdapter.DeleteCommand = new SqlCommand(
@"delete from Production.Product
where ProductNumber = @ProductNumber", connection);
SqlParameter parameter = dataAdapter.DeleteCommand.Parameters.Add("@ProductNumber", SqlDbType.NVarChar, 25);
parameter.SourceColumn = "ProductNumber";
parameter.SourceVersion = DataRowVersion.Original;

DataSet dataSet = new DataSet();
dataAdapter.Fill(dataSet, "Products");

grdProducts.DataSet = dataSet;
grdProducts.DataAdapter = dataAdapter;
}


Abb. 4: Zusammenhang Data Binding, DataSet und DataAdapter

Geheimnisse von EditGrid
EditGrid beinhaltet eine Reihe von Tipps und Tricks, die nicht nur im Umgang mit ListViews nützlich sind. Auf zwei möchten wir an dieser Stelle speziell eingehen.


Abb. 5: Eingabefokus auf der falschen Zeile

Das Steuerelement erlaubt es dem Benutzer mit den Cursor-Up- und Cursor-Down-Tasten von Datensatz zu Datensatz zu springen. Dieses Verhalten soll auch für den Fall erhalten bleiben, dass der Anwender mit dem Cursor gerade in einem der Eingabefelder steht. Der entsprechende Event-Handler, der den Code dafür enthält, ist EditGrid.EditGrid_KeyUp. Dort kann man den SelectedIndex verändern. Die Schwierigkeit dabei ist, dass der Eingabefokus auf der zuvor markierten Zeile bleibt (siehe Abbildung 4). Beim Beheben dieses Fehlers stößt man auf das Problem, dass im Event-Handler nur das SelectedItem, also das Datenelement, das hinter der markierten Zeile steckt, verfügbar ist. Es kann nicht zum Verändern des Eingabefokus verwendet werden denn dafür wäre Zugriff auf das entsprechende ListViewItem-Objekt notwendig. Es steht aber leider nicht als Eigenschaft zur Verfügung. Des Rätsels Lösung ist die Methode ItemContainerGenerator.ContainerFromItem. Mit ihr kann das ListViewItem zum SelectedItem ermittelt werden. Damit stehen alle Steuerelemente der aktuell markierten Zeile im Event Handler zur Verfügung:

private void EditGrid_KeyUp(object sender, KeyEventArgs e)
[...]
else if (e.Key == Key.Down)
{
if (!(Keyboard.FocusedElement is ListViewItem))
{
if (this.SelectedIndex < this.Items.Count - 1)
{
this.SelectedIndex++;
Keyboard.Focus((ListViewItem) this.ItemContainerGenerator.ContainerFromItem(this.SelectedItem));
}
}
}
[...]
}

Ein zweiter wichtiger Tipp betrifft die Ausrichtung von Elementen in Content Controls wie ListBox oder ListView. Im Gegensatz zu den meisten Layout Controls (z.B. Grid oder Canvas) werden in Content Controls eingebettete Elemente nicht automatisch so breit und hoch dargestellt wie deren Vater-Element. Die Folge ist, dass sich in unserem Beispiel die Eingabeelemente nicht von selbst an die Breite der entsprechenden Spalte anpassen. Viele XAML-Neulinge versuchen für dieses Problem ihr Glück mit dem Attribut HorizontalAlignment=“Stretch“. Leider liefert es nicht das gewünschte Ergebnis. Stattdessen muss das Attribut Horizontal-ContentAlignment am Content Control verwendet werden. Abbildung 6 demonstriert mit Hilfe von XamlPad den Unterschied anhand einer Listbox in die eine TextBox, ein Button und ein Text-Block eingebettet sind. Im beiliegenden EditGrid Beispiel wird die Ausrichtung der Steuerelemente über einen global definierten Stil in App.xaml gesteuert:

<Application x:Class="EditGridTester.App" [...]>
<Application.Resources>
[...]
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
</Application.Resources>
</Application>


Abb. 6: Anwendung von HorizontalContentAlignment

Zum Schluss
Microsoft Visual Studio wurde seit der Einführung von .NET von Version zu Version um immer mächtigere Assistenten zur Entwicklung datenorientierter Anwendungen ergänzt. Speziell Elemente wie DataGrid bzw. GridView in ASP.NET und DataGridView in WinForms nehmen viel Arbeit im Umgang mit den Klassen aus System.Data.xxxClient ab. Von diesen Annehmlichkeiten muss man sich in der aktuellen Version von WPF leider verabschieden. Unserer Erfahrung nach hält sich der Produktivitätsverlust dadurch jedoch in Grenzen. Schließlich war schon bisher die Grenze der Assistenten bei komplexeren Anwendungen schnell erreicht.

Quasi zur Versöhnung für die fehlenden Wizards liefert WPF den neuen Data-Binding-Mechanismus zur Anbindung der Benutzerschnittstelle an Datenzugriffs- oder Geschäftslogikschicht. Schon nach den ersten Schritten erkennt man, dass diese Technik das Potential hat, wirklich viel Code einzusparen. Ein Tipp aus eigener Erfahrung dazu: Wenn Sie irgendwo meinen, selbst auch nur eine Zeile Code schreiben zu müssen, um Werte aus dem Property eines Objekts in das eines anderen zu kopieren, denken Sie vorher noch einmal genau darüber nach. In so gut wie allen Fällen gibt es einen eleganteren, kürzeren Weg über Data Binding direkt in XAML.

Die ListView von WPF ist ein mächtiges und in vielerlei Hinsicht stark anpassbares Werkzeug. Mit Styles und Templates lässt sich nahezu jedes Aussehen und Verhalten implementieren. Nichts desto trotz stößt man beim Arbeiten in der Praxis immer wieder auf Punkte, die man sich eigentlich schon im Standard eines solchen Steuerelements erhoffen würde. Ein gutes Beispiel ist das Editieren von Daten innerhalb der Tabelle. Wir haben in unserem Beispiel gezeigt, dass es nur wenig Code bedarf, eine wieder verwendbare, editierbare Version einer ListView umzusetzen. Schöner wäre es aber, wenn solche Funktionen von Haus aus vorhanden wären. Insofern sehen wir hier einen großen Markt für Drittanbieter weiterführender Steuerelemente oder Raum für Verbesserungen in zukünftigen Versionen von WPF.


Karin Huber und Rainer Stropek sind Mitgründer von cubido business solutions gmbh, einem Microsoft Gold Certified Partner. Sie arbeiten dort als Software Architekten mit besonderem Schwerpunkt auf SQL Server, 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