25.3 Navigieren, Filtern, Sortieren und Gruppieren
Neben der reinen Datendarstellung spielen oft auch andere Aspekte eine wichtige Rolle. Beispielsweise wollen die Anwender nicht alle Daten sehen, sondern diese nach gewissen Kriterien filtern. Oder die Anwender möchten die Daten sortieren und zwischen den Daten wahlfrei navigieren können. Das alles mit der WPF umzusetzen ist nicht besonders schwierig, weil die wichtigsten Komponenten dazu von der WPF bereitgestellt werden.
Binden Sie eine Auflistung von Daten an ein WPF-Steuerelement, binden Sie im Grunde genommen nicht direkt an die Daten. Stattdessen erzeugt die WPF einen View auf die Collection, die wie ein Wrapper agiert. Der automatisch erstellte View vom Typ CollectionView implementiert das Interface ICollectionView. Von CollectionView gibt es drei verschiedene Ableitungen:
- ItemCollection
- ListCollectionView
- BindingListCollectionView
Je nachdem, um welche Art von Liste der Wrapper gelegt wird, wird ein passender View erzeugt. Der folgenden Tabelle können Sie Details dazu entnehmen.
Klasse | Beschreibung |
Diese Klasse wird nur von den Steuerelementen verwendet, die von ItemsControl abgeleitet sind. Diese Klasse ist ohne Konstruktor und wird von den Steuerelementen intern verwendet. Die Eigenschaft Items ist vom Typ ItemCollection. |
|
Dieser View ist für einfache Auflistungen gedacht, die das Interface IEnumerable implementieren. |
|
Mit diesem View werden die Auflistungen gekapselt, die das Interface IList implementieren. |
|
Mit diesem View werden die Auflistungen gekapselt, die das Interface IBindingList oder IBindingListView implementieren. Ein typischer Vertreter ist die Klasse System.Data.DataView. |
Ein CollectionView hat neben dem Sortieren, Filtern und Gruppieren einer Liste auch noch eine andere wichtige Aufgabe: Er verfolgt das aktuell in einem ItemsControl ausgewählte Item. Da sich mehrere Steuerelemente an denselben View binden können, werden neu ausgewählte Listeneinträge automatisch mit den Inhalten der Steuerelemente synchronisiert, die denselben View binden.
Interessant ist auch die Tatsache, dass die drei Klassen ItemCollection, ListCollectionView und BindingListCollectionView die Schnittstelle IEditableCollectionView implementieren. Über dieses Interface werden den Views Methoden wie beispielsweise AddNew, Remove, RemoveAt oder auch CancelEdit verfügbar gemacht.
Die durch die WPF automatisch implizit erzeugte ICollectionView wird auch als DefaultView bezeichnet. Sie wird erstellt, wenn der ItemsSource-Eigenschaft eines ItemsControl-Elements eine Auflistung zugewiesen wird. Es sei angemerkt, dass Sie auch explizit einen CollectionView erstellen und diesen der ItemsSource-Eigenschaft angeben können. Die WPF ihrerseits erzeugt dann keinen DefaultView.
Die Referenz auf den CollectionView kann man auch mit Programmcode abrufen. Dazu dient die statische Methode GetDefaultView der Klasse CollectionViewSource. Der Methode wird als Argument die Datenquelle übergeben, z. B.:
ICollectionView view = CollectionViewSource.GetDefaultView(liste);
Listing 25.17 Die Referenz auf den DefaultView abrufen
25.3.1 Navigieren
Haben Sie eine Collection von Datenobjekten an ein Fenster gebunden und beabsichtigen Sie, nur die jeweiligen Eigenschaftswerte eines Datenobjekts in ContentControls anzuzeigen, müssen Sie eine Navigation zu den anderen Datenobjekten ermöglichen. Umgesetzt wird so etwas in der Regel mit Navigationsschaltflächen, die eine Navigation zum nächsten Listenelement und zurück zum vorherigen ermöglichen. Üblicherweise wird meist auch die Navigation zum ersten und zum letzten Datenobjekt in der Liste bereitgestellt.
Über die Schnittstelle ICollectionView werden zur Umsetzung dieser Anforderungen zahlreiche Methoden angeboten. Dazu gehören:
In Abbildung 25.4 ist das nächste Beispielprogramm dargestellt, in dem einige wichtige Methoden der Navigation verwendet werden.
Abbildung 25.4 Ausgabe des Beispielprogramms »NavigationSample«
Grundlage des Beispiels ist eine ADO.NET-Datenbankabfrage der Datenbank Northwind. Es wird dabei die Tabelle Products abgefragt. Wenn Sie den Code der ADO.NET-Abfrage nicht verstehen, ist das nicht entscheidend zum Verständnis des Beispiels.
In einer ListBox werden alle Artikel aus der Tabelle mit ihrem Artikelbezeichner (Spalte ProductName) angezeigt. Wird zur Laufzeit ein neuer Eintrag ausgewählt, werden im rechten Teilbereich des Fensters einige Detailinformationen zum ausgewählten Artikel in Textboxen angezeigt. Die Navigation zum nächsten oder zum vorherigen Datensatz wird mit Hilfe von zwei Schaltflächen im oberen Bereich des Fensters unterstützt. Darüber hinaus wird die Position des aktuell ausgewählten Datensatzes nebst der Gesamtanzahl eingespielt.
Sehen wir uns zunächst den C#-Programmcode in der Code-Behind-Datei des Fensters an.
// Beispiel: ..\Kapitel 25\NavigationSample
public partial class MainWindow : Window
{
private BindingListCollectionView view;
public MainWindow() {
// Tabelle aus der Datenbank abrufen
SqlConnection con = new SqlConnection();
con.ConnectionString = @"Server=.\SQLEXPRESS;Database=Northwind; " +
"Integrated Security=SSPI";
DataSet ds = new DataSet();
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Products", con);
da.Fill(ds);
InitializeComponent();
this.DataContext = ds.Tables[0];
view = (BindingListCollectionView)CollectionViewSource.
GetDefaultView(this.DataContext);
view.CurrentChanged += view_CurrentChanged;
}
}
Listing 25.18 Vorläufiger Programmcode in der Code-Behind-Datei
Es ist auf Klassenebene ein Feld vom Typ BindingListCollectionView deklariert, das im Konstruktor initialisiert wird. Die Entscheidung für diesen Typ hängt damit zusammen, dass wir später ein DataSet mit der Tabelle der Produkte vorliegen haben, die wir in einen DataView umwandeln müssen. Gemäß Tabelle 25.1 kommt daher nur der BindingListCollectionView als View-Objekt in Frage.
Nach der Datenbankabfrage und dem Aufruf der Methode InitializeComponent wird die Tabelle an den Datenkontext des Window gebunden. Anschließend wird die BindingListCollectionView-Variable wie oben beschrieben initialisiert.
Die Klasse CollectionView vererbt ihren Ableitungen neben Eigenschaften und Methoden auch einige Ereignisse. Eines davon ist CurrentChanged, das ausgelöst wird, wenn sich das aktuell ausgewählte Element im View geändert hat. Wir binden an das Ereignis einen Ereignishandler, um die aktuelle Position innerhalb der Liste anzuzeigen.
Widmen wir uns nun der ListBox. Hier zunächst der XAML-Code, um die Bindung an die Datenquelle zu verstehen:
<ListBox Name="lstProducts"
DisplayMemberPath="ProductName"
ItemsSource="{Binding}"
SelectionChanged="lstProducts_SelectionChanged" />
Listing 25.19 Datenbindung der ListBox
Mit der Eigenschaft ItemsSource wird auf die Bindung des Window zurückgegriffen, und mit DisplayMemberPath wird die Spalte angegeben, die in der ListBox angezeigt werden soll. Es wird zudem das Ereignis SelectionChanged behandelt, da wir den View davon in Kenntnis setzen müssen, dass sich die ausgewählte Datenzeile geändert hat. Hier auch dazu sofort der Ereignishandler:
private void lstProducts_SelectionChanged(object sender,
SelectionChangedEventArgs e){
view.MoveCurrentTo(lstProducts.SelectedItem);
}
Listing 25.20 Festlegung des neuen aktuellen Elements
Einfach zu verstehen sind die beiden Ereignishandler der Schaltflächen zur Navigation zum nächsten oder vorherigen Artikel im View.
private void cmdNext_Click(object sender, RoutedEventArgs e) {
view.MoveCurrentToNext();
}
private void cmdPrevious_Click(object sender, RoutedEventArgs e) {
view.MoveCurrentToPrevious();
}
Listing 25.21 Ereignishandler der Navigationsschaltflächen
Jetzt fehlt nur noch der Ereignishandler des Events CurrentChanged.
void view_CurrentChanged(object sender, EventArgs e) {
txtPosition.Text = "Datensatz " + (view.CurrentPosition + 1).ToString() +
" von " + view.Count.ToString();
cmdPrevious.IsEnabled = view.CurrentPosition > 0;
cmdNext.IsEnabled = view.CurrentPosition < view.Count - 1;
}
Listing 25.22 Der Ereignishandler des Ereignisses »CurrentChanged«
Der View liefert in der Eigenschaft CurrentPosition die Indexposition des aktuellen Elements der Liste. Da diese Eigenschaft die Ordinalposition liefert, müssen wir das durch die Addition der Zahl 1 zur Ausgabe der Position des Listenelements berücksichtigen. Zudem nutzen wir CurrentPosition auch dazu aus, im Bedarfsfall eine der beiden Navigationsschaltflächen zu deaktivieren, falls der erste oder der letzte Datensatz aus der Liste das aktuell ausgewählte Element ist.
25.3.2 Sortieren
Die Sortierung nach einer Eigenschaft der durch einen View beschriebenen Elemente ist nicht schwierig. Dazu stellt die Schnittstelle ICollectionView den implementierenden Klassen mit SortDescriptions eine passende Eigenschaft bereit, der das gewünschte Sortierkriterium angegeben wird. Da diese Eigenschaft eine Liste beschreibt, können Sie auch mehrere Sortierkriterien angeben.
Jedes Sortierkriterium wird durch ein Objekt vom Typ SortDescription beschrieben. Um ein Sortierkriterium zu definieren, übergeben Sie dem Konstruktor zwei Argumente. Im ersten Argument wird die Eigenschaft genannt, nach der die Liste sortiert werden soll. Das zweite Argument beschreibt die Richtung der Sortierung mit der Enumeration ListSortDirection, die die beiden Member Ascending (für eine aufsteigende Reihenfolge) und Descending (für eine absteigende Reihenfolge) enthält.
Um Ihnen zu zeigen, wie Sie eine Sortierung umsetzen können, greifen wir auf das Beispielprogramm NavigationSample des letzten Abschnitts zurück und wollen die Produkte, die in der ListBox angezeigt werden, dem Artikelnamen nach sortieren. Den erforderlichen Programmcode können wir grundsätzlich in jedem passenden Ereignishandler implementieren, in unserem Beispiel bietet sich aber der Konstruktor bestens an.
// Beispiel: ..\Kapitel 25\ViewSortSample
public MainWindow()
{
[...]
this.DataContext = ds.Tables[0];
view = (BindingListCollectionView)CollectionViewSource.
GetDefaultView(this.DataContext);
view.CurrentChanged += view_CurrentChanged;
SortDescription sort = new SortDescription("ProductName",
ListSortDirection.Ascending);
view.SortDescriptions.Add(sort);
lstProducts.SelectedIndex = 0;
}
Listing 25.23 Sortieren eines Views
Zur Umsetzung mehrerer Sortierkriterien werden mehrere SortDescription-Objekte benötigt, die nacheinander abgearbeitet werden. So ließe sich die Produktliste zuerst nach Kategorien sortieren (Eigenschaft CategoryID) und danach innerhalb der Kategorien nach dem Preis (Eigenschaft UnitPrice).
[...]
SortDescription sortCat = new SortDescription("CategoryID",
ListSortDirection.Ascending);
SortDescription sortPrice = new SortDescription("Unitprice",
ListSortDirection.Ascending);
view.SortDescriptions.Add(sortCat);
view.SortDescriptions.Add(sortPrice);
[...]
Listing 25.24 Mehrere Sortierkriterien
25.3.3 Filtern
Das Filtern ermöglicht uns, aus einer Liste nur diejenigen Elemente anzuzeigen, die einer bestimmten Bedingung genügen. Hinsichtlich des Filterns eines Views müssen wir jedoch zwei Fälle unterscheiden: das Filtern einer herkömmlichen Collection und das Filtern einer ADO.NET-DataTable. Wir wollen uns beide Varianten ansehen.
Das Filtern einer Collection
Das Filtern einer Collection erfolgt mit der Eigenschaft Filter des Views. Am besten sehen wir uns zuerst die Definition der Filter-Eigenschaft an:
public virtual Predicate<Object> Filter { get; set;}
Die Eigenschaft erwartet einen Delegaten vom Typ Predicate, der auf eine Filtermethode zeigt, die Sie bereitstellen müssen. Auch die Definition des Delegates müssen wir uns ansehen, um den Filter zu verstehen:
Der Rückgabewert ist bool. Innerhalb der Filtermethode wird jedes Listenelement aufgerufen und dahingehend einer Prüfung unterzogen, ob es den Bedingungen entspricht oder nicht. Im ersten Fall muss die Filtermethode true zurückliefern. Um beispielsweise aus einer Auflistung von Person-Objekten, in denen die Eigenschaften Name, Alter und Ort beschrieben werden, alle Personen herauszufiltern, die in einer bestimmten Stadt wohnen, könnte der Code wie folgt aussehen:
// Beispiel: ..\Kapitel 25\CollectionFilterSample
public partial class MainWindow : Window {
private List<Person> liste = new List<Person>();
public MainWindow() {
FillListe();
InitializeComponent();
lstPersons.ItemsSource = liste;
ListCollectionView view = CollectionViewSource.GetDefaultView(
lstPersons.ItemsSource) as ListCollectionView;
view.Filter = new Predicate<object>(FilterPersons);
}
public bool FilterPersons(object item) {
Person pers = (Person)item;
return (pers.Ort == "Essen");
}
private void FillListe() {
liste.Add(new Person { Name = "Müller", Alter=43, Ort = "Duisburg" });
liste.Add(new Person { Name = "Meier", Alter=43, Ort = "Bonn" });
liste.Add(new Person { Name = "Schmidt", Alter=43, Ort = "Essen" });
[...]
}
}
class Person {
public string Name { get; set; }
public int Alter { get; set; }
public string Ort { get; set; }
}
Listing 25.25 Festlegen eines Filters
Der XAML-Code zum Testen des hardcodierten Filters ist denkbar einfach:
<Window ...
Title="MainWindow" Height="150" Width="150">
<Grid>
<ListBox Name="lstPersons" DisplayMemberPath="Name"></ListBox>
</Grid>
</Window>
Listing 25.26 XAML-Code des Beispiels »CollectionFilterSample«
Auch wenn dieses Beispiel sehr gut funktioniert, hat es einen gravierenden Nachteil. Es ist hinsichtlich der Filterbedingung unflexibel, da ein Benutzer nicht in der Lage ist, das Filterkriterium zur Laufzeit der Anwendung dynamisch festzulegen.
Das wollen wir natürlich anders gestalten und bedienen uns dazu eines simplen Tricks. Wir implementieren die Filtermethode in einer separaten Klasse. Innerhalb der Klasse legen wir eine Eigenschaft fest, die die Filterbedingung beschreibt. Bevor der Filter gesetzt wird, muss die Klasse instanziiert werden und ihr dabei die Bedingung übergeben werden. Dazu eignet sich der Konstruktor. Innerhalb der Filtermethode wird die Bedingung beim Setzen des Filters ausgewertet.
// Beispiel: ..\Kapitel 25\CollectionDynamicFilterSample
class FilterPersonByCity {
public string Ort { get; set; }
public FilterPersonByCity(string city) {
Ort = city;
}
public bool FilterPersons(object item) {
Person pers = (Person)item;
return (pers.Ort == Ort);
}
}
Listing 25.27 Wrapper-Klasse um die Filtermethode
Diese Klasse wollen wir nun testen und ergänzen das Fenster des Beispiels CollectionFilterSample um eine TextBox und eine Schaltfläche. Der Anwender kann eine Stadt in die TextBox eintragen. Beim Klicken auf die Schaltfläche wird der Filter entsprechend neu gesetzt und aktiviert, und in der ListBox werden alle Personen angezeigt, die der Bedingung entsprechen. Bleibt die TextBox leer, wird der Filter durch Setzen auf null gelöscht.
public partial class MainWindow : Window {
private List<Person> liste = new List<Person>();
private ListCollectionView view;
public MainWindow() {
FillListe();
InitializeComponent();
lstPersons.ItemsSource = liste;
view = CollectionViewSource.GetDefaultView
(lstPersons.ItemsSource) as ListCollectionView;
}
private void btnSetFilter_Click(object sender, RoutedEventArgs e) {
FilterPersonByCity filter = new FilterPersonByCity(txtCity.Text);
if (txtCity.Text == "")
view.Filter = null;
else
view.Filter = new Predicate<object>(filter.FilterPersons);
}
}
Listing 25.28 Änderungen und Ergänzungen im Code des »Window«
Filtern einer »DataTable«
Das Filtern eines Views vom Typ ListCollectionView ist mit der Eigenschaft Filter möglich. Auch die Klasse BindingListCollectionView, die im Zusammenhang mit einer ADO.NET-DataTable verwendet wird, stellt eine Filter-Eigenschaft zur Verfügung. Allerdings nur offiziell, denn diese Eigenschaft wird von BindingListCollectionView nicht weiter unterstützt und wirft eine Ausnahme, wenn man dennoch versucht, sie zu benutzen.
Stattdessen wird von BindingListCollectionView die Eigenschaft CustomFilter unterstützt. Diese Eigenschaft erwartet den Filter in Form einer Zeichenfolge. Das macht den Einsatz sehr einfach, da die Zeichenfolge entsprechend der aus SQL bekannten Filterbedingung als WHERE-Klausel formuliert wird.
Das folgende Beispielprogramm nutzt erneut die Tabelle Products der Northwind-Datenbank. Der Filter wird in einer TextBox festgelegt und durch Klicken auf eine Schaltfläche aktiviert.
// Beispiel: ..\Kapitel 25\DataTableFilterSample
<Window ...
Title="DataTableFilter" Height="250" Width="550">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox Name="lstProducts" DisplayMemberPath="ProductName"
Margin="10"></ListBox>
<StackPanel Grid.Column="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center"
Foreground="Wheat" Text="UnitPrice > " />
<TextBox Grid.Column="1" Name="txtUnitPrice" Margin="5"
Background="AntiqueWhite"></TextBox>
</Grid>
<Button Name="btnSetFilter" Margin="5" Click="btnSetFilter_Click">
Filter setzen
</Button>
</StackPanel>
</Grid>
</Window>
Dazu gehört der folgende C#-Code:
public partial class MainWindow : Window {
private BindingListCollectionView view;
public MainWindow() {
[...]
da.Fill(ds);
InitializeComponent();
lstProducts.ItemsSource = ds.Tables[0].DefaultView;
view = (BindingListCollectionView)CollectionViewSource.
GetDefaultView(lstProducts.ItemsSource);
}
private void btnSetFilter_Click(object sender, RoutedEventArgs e) {
view.CustomFilter = "UnitPrice > " + txtUnitPrice.Text;
}
}
Listing 25.29 Code des Beispielprogramms »DataTableFilterSample«
Sie können in der TextBox auch eine Filterbedingung festlegen, die mehrere Filterbedingungen mit AND oder OR verknüpft. Das sollte auch nicht erstaunen, da der Filter gemäß den Regeln der SQL-WHERE-Klausel formuliert wird (siehe Abbildung 25.5).
Abbildung 25.5 Das Beispielprogramm mit mehreren Filtern
25.3.4 Gruppieren
Das Gruppieren der angezeigten Daten erfolgt ebenfalls über ein View-Objekt. Um präziser zu sein, die Schnittstelle ICollectionView stellt dafür allen Ableitungen die Eigenschaft GroupDescriptions zur Verfügung, die vom Typ ObservableCollection<GroupDescription> ist. Da die Klasse GroupDescription ihrerseits selbst abstrakt definiert ist, gibt es mit PropertyGroupDescription eine spezialisierte Klasse, die minimal eine Zeichenfolge entgegennimmt, die die Eigenschaft angibt, nach der gruppiert werden soll, z. B.:
view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryID"));
In dieser Anweisung wird die Tabelle Products nach der CategoryID gruppiert.
Um die Gruppierung anzeigen zu können, vererbt die Klasse ItemsControl allen Ableitungen die Eigenschaft GroupStyle. Auch diese Eigenschaft beschreibt eine Collection, diesmal vom Typ ObservableCollection<GroupStyle>. Die GroupStyle-Klasse verfügt über die Eigenschaft HeaderTemplate, mit der die Darstellung der Kopfzeile einer Gruppe als DataTemplate beschrieben wird.
Angenommen, wir würden beabsichtigen, die in der Tabelle Products enthaltenen Artikel der Kategorie nach zu gruppieren und in einer ListBox anzuzeigen.
Abbildung 25.6 Ausgabe des Beispielprogramms »SimpleGroupingSample«
Der XAML-Code dazu sieht wie folgt aus:
// Beispiel: ..\Kapitel 25\SimpleGroupingSample
<Window ... Title="MainWindow" Height="350" Width="300">
<Window.Resources>
<CollectionViewSource x:Key="groupView">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="CategoryID" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<ListBox Grid.Row="1" Margin="7,3,7,10" Name="lstProducts"
ItemsSource="{Binding Source={StaticResource groupView}}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ProductName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"
Foreground="White" Background="LightGreen"
Margin="0,5,0,0" Padding="3"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
</Grid>
</Window>
Jetzt folgt natürlich auch noch der C#-Code:
public MainWindow() {
// Tabelle Products in den Speicher laden
InitializeComponent();
CollectionViewSource viewSource =
(CollectionViewSource)this.FindResource("groupView");
viewSource.Source = ds.Tables[0].DefaultView;
}
Listing 25.30 Code des Beispielprogramms »SimpleGroupingSample«
Im Konstruktor des Window wird zuerst die Tabelle Products mit ADO.NET-Code in den Speicher geladen und anschließend als View einem in der XAML-Datei definierten CollectionViewSource-Objekt als Datenquelle übergeben.
In der XAML-Datei ist das CollectionViewSource-Objekt im Resource-Abschnitt definiert. Mit PropertyGroupDescription wird die Gruppierung aller Artikel anhand der Eigenschaft CategoryID festgelegt.
<CollectionViewSource x:Key="groupView">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="CategoryID" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
Listing 25.31 Gruppierungseigenschaft festlegen
Die Eigenschaft GroupStyle legt den Stil der Gruppendarstellung fest. Dazu wird der Eigenschaft HeaderTemplate ein DataTemplate übergeben, das ein TextBlock-Element enthält, das gebunden werden muss.
Hinsichtlich der Bindung des TextBlocks muss man allerdings ein wenig in die Trickkiste greifen, um zum gewünschten Ziel zu kommen (die CategoryID in der Kopfzeile anzuzeigen). Sie dürfen nämlich nicht an die Eigenschaft CategoryID binden, wie zuerst zu vermuten wäre. Stattdessen erfolgt die Bindung an die Name-Eigenschaft des PropertyGroupDescription-Objekts:
<TextBlock Text="{Binding Path=Name}" ... />
Erweiterte Gruppierung
Sehen wir uns noch einmal Abbildung 25.6 an. Die Werte der Spalte CategoryID als Text in das HeaderTemplate zu schreiben ist mit Sicherheit nicht das, was wir dem Anwender anbieten wollen. Hier wäre der Kategoriename hinter CategoryID wünschenswert. Dieser verbirgt sich in der Spalte CategoryName der Tabelle Categories, die sich in einer 1:n-Beziehung zur Tabelle Products befindet.
Das Problem könnte sehr einfach mit einer SQL-JOIN-Abfrage gelöst werden. Diesen Weg möchte ich Ihnen aber nicht zeigen, weil er zu wenig Allgemeingültigkeit hat. Wir wollen uns stattdessen den gewünschten Kategoriebezeichner direkt aus der Tabelle Categories besorgen und den durch CategoryID beschriebenen Zahlenwert durch den Namen der Kategorie ersetzen. Hier bietet es sich an, einen passenden Konverter zu schreiben, der genau diese Anforderung erfüllt.
// Beispiel: ..\Kapitel 25\AdvancedGroupingSample
public class GroupingConverter : IValueConverter {
private DataSet ds = new DataSet();
public GroupingConverter() {
SqlConnection con = new SqlConnection();
con.ConnectionString = @"...";
string sql = "SELECT CategoryID, CategoryName FROM Categories";
SqlDataAdapter da = new SqlDataAdapter(sql, con);
da.FillSchema(ds, SchemaType.Source);
da.Fill(ds);
}
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
DataRow row = ds.Tables[0].Rows.Find((int)value);
return row["CategoryName"].ToString();
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Listing 25.32 Die »Converter«-Klasse
Im XAML-Code geben wir nun den Namespace dieser Klasse mit
xmlns:local="clr-namespace:AdvancedGroupingSample"
bekannt und erstellen im Resource-Abschnitt des Fensters ein Objekt dieser Klasse:
<Window.Resources>
<local:GroupingConverter x:Key="converter" />
[...]
</Window.Resources>
Nun folgt noch der letzte Schritt, denn dem PropertyGroupDescription-Objekt muss das Converter-Objekt mit der Eigenschaft Converter bekannt gegeben werden:
<PropertyGroupDescription PropertyName="CategoryID"
Converter="{StaticResource converter}" />
Das Beispiel SimpleGroupingSample auf der Buch-DVD wurde zudem um die Sortierung ergänzt. Dabei werden zunächst alle Kategorien sortiert, und innerhalb derselben wird nach dem Produktnamen sortiert:
<CollectionViewSource x:Key="groupView">
[...]
<CollectionViewSource.SortDescriptions>
<sys:SortDescription PropertyName="CategoryID"
Direction="Ascending"/>
<sys:SortDescription PropertyName="ProductName"
Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
Da der Typ SortDescription keinem XML-Namespace zugeordnet ist, müssen die Datei WindowsBase.dll und der CLR-Namespace System.ComponentModel, in dem diese Klasse definiert ist, mit
xmlns:sys="clr-namespace:System.ComponentModel;assembly=WindowsBase"
bekannt gegeben werden.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.