EDM: 1:n-Beziehungen mit DataGrid

Wenn Sie ein Abonnement des Magazins 'DATENBANKENTWICKLER' besitzen, können Sie sich anmelden und den kompletten Artikel lesen.
Anderenfalls können Sie das Abonnement hier im Shop erwerben.

EDM: 1:n-Beziehungen mit DataGrid

Unter Access haben wir 1:n-Beziehungen einfach in einem Haupt- und einem Unterformular abgebildet, wobei wir beiden einfach die Datenquellen und gebundenen Steuerelemente zugewiesen haben – den Rest hat Access automatisch erledigt. Unter C# und WPF ist das ein wenig mehr Arbeit, aber nach der Lektüre dieses Artikels haben Sie das Wissen, das für die Anzeige zweier per 1:n-Beziehung verknüpfter Tabellen in einem Fenster beziehungsweise einer Seite und einem DataGrid als Unterformular-Ersatz nötig ist.

Beispieldaten

Als Beispiel wollen wir uns die Kategorien und die damit verknüpften Produkte ansehen. Dabei sollen die beiden Felder einer Kategorie im Fenster/in der Seite selbst angezeigt werden, die dazugehörigen Produkte in einem DataGrid unter den Kategoriedaten. Wir verwenden die SQLite-Datenbank Bestellverwaltung.db und ein daraus abgeleitetes Entity Data Model namens BestellverwaltungEntities.edmx als Datenquelle. Die verwendeten Auflistungen beziehungsweise Entitäten heißen Kategorien, Kategorie, Produkte und Produkt.

Einbau in die Bestellverwaltung

In einigen weiteren Beiträgen verwenden wir die Bestellverwaltung als Beispielanwendung. Diese verwendet im Office-Stil ein Ribbon zur Auswahl der verschiedenen Bereiche und Funktionen. Die einzelnen Bereiche etwa zur Anzeige einer Kundenliste oder einer Kundendetailansicht auf Seiten (Page) statt auf eigenen Fenstern (Window) erstellt und je nach angeklickter Ribbon-Schaltfläche in einem Frame-Element eingeblendet – also etwa wie die Unterformulare in einem Unterformular-Steuerelement in Access. Wir fügen also in diesem Artikel weitere Page-Elemente hinzu, die wir dann nach dem Anklicken der ebenfalls noch anzulegenden Ribbon-Schaltflächen angezeigt werden sollen.

Ribbon-Einträge

Als Erstes legen wir die drei Ribbon-Einträge an, die Sie in Bild 1 sehen. Die Elemente für diese Ribbon-Gruppe sehen wie folgt aus:

Übersicht der Kategorien

Bild 1: Übersicht der Kategorien

<RibbonGroup Header="Kategorien">	//MainWindow.xaml
     <RibbonButton Name="btnKategorieuebersicht" Label="Übersicht" Click="btnKategorieuebersicht_Click"
         LargeImageSource="images/elements4.png"></RibbonButton>
     <RibbonButton Name="btnNeueKategorie" Label="Neue Kategorie" Click="btnNeueKategorie_Click" 
         LargeImageSource="images/elements4_new.png"></RibbonButton>
     <RibbonButton Name="btnKategorieLoeschen" Label="Kategorie löschen" Click="btnKategorieLoeschen_Click" 
         LargeImageSource="images/elements4_delete.png"></RibbonButton>
</RibbonGroup>

Wir haben für jedes Element ein Bild hinterlegt, dass wir gleichzeitig zum Ordner images des Projekts hinzugefügt haben.

Übersichtsseite für die Kategorien

Die Kategorien werden in der Übersichtsseite KategorieUebersicht.xaml in einem DataGrid-Element aufgelistet. Das Füllen dieses DataGrid-Elements erfolgt genauso, wie wir es bereits im Artikel EDM: Kunden verwalten mit Ribbon für die Anzeige der Kunden realisiert und im Artikel Bestellveraltung planen angepasst haben – nur, dass wir diesmal auf die Entitätsliste der Kategorien statt der Kunden zugreifen und nur die Felder ID und Bezeichnung anzeigen.

Detailseite für die Kategorien

Der interessante Teil folgt nun, nämlich die Detailseite für eine Kategorie. Eine Kategorie enthält zwar nur die beiden Felder ID und Bezeichnung, was sich genau so leicht abbilden lässt wie in der Detailansicht für Kunden (Kundendetails.xaml) – nur mit weniger Feldern. Allerdings wollen wir ja zu jeder Kategorie auch noch die Liste der Produkte anzeigen, die der jeweiligen Kategorie zugeordnet sind! Und das wollen wir wiederum mit einem DataGrid-Element erledigen.

Imgrunde brauchen wir also eine Kombination der bereits einmal gezeigten Detailansicht für die Kunden (diesmal für die Kategorien) und einer Übersicht für die Produkte. Der Unterschied bei der Übersichtsseite diesmal ist, dass wir nicht mehr einfach alle Elemente der Produkte-Liste anzeigen können, sondern nur noch diejenigen Elemente, die der aktuellen Kategorie zugeordnet sind. Die Definition der Seite mit den Kategoriedetails und der Produktliste startet mit ein paar Page.Resources-Elementen, welche globale Attribute der verschiedenen Steuerelemente festlegen und die wir hier ebenso gekürzt haben wie die Definition des Grids:

<Page x:Class="Bestellverwaltung.Kategoriedetails" ...Title="Kategoriedetails">	//Kategoriedetails.xaml
     <Page.Resources>...</Page.Resources>
     <Grid>
         <Grid.ColumnDefinitions>...</Grid.ColumnDefinitions>
         <Grid.RowDefinitions>...</Grid.RowDefinitions>

Danach folgt das Label für die ID der Kategorie und die TextBox, die wir an das Feld kategorie.ID binden:

         <Label Content="ID:" Grid.Column="0" />
         <TextBox x:Name="txtID" Grid.Column="1" HorizontalAlignment="Left" Text="{Binding kategorie.ID, Mode=TwoWay,             ValidatesOnExceptions=true}" Width="50" IsEnabled="False" BorderBrush="Transparent" />

Die Bezeichnung erhält ebenfalls ein Label und eine Bindung an das Feld kategorie.Bezeichnung – beide landen in jeweils einer Spalte, genau wie die Steuerelemente für die ID:

         <Label Content="Bezeichnung:" Grid.Column="0" Grid.Row="1" />
         <TextBox x:Name="txtBezeichnung" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="1" Text="{Binding kategorie.Bezeichnung, Mode=TwoWay, ValidatesOnDataErrors=true}" />

Schließlich folgt das DataGrid-Element namens dgProdukte, das in der folgenden Zeile landet und sich über zwei Spalten erstrecken soll (Grid.ColumnSpan="2"). Als ItemsSource für das DataGrid legen wir das Element Produkte fest. Das automatische Generieren der Spalten sowie das Anzeigen einer leeren Spalte zum Hinzufügen von Elementen deaktivieren wir:

         <DataGrid x:Name="dgProdukte" Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" ItemsSource="{Binding Produkte}" AutoGenerateColumns="false" CanUserAddRows="False">

Die beiden Spalten des DataGrid-Elements binden wir an die Felder ID und Bezeichnung:

             <DataGrid.Columns>
                 <DataGridTextColumn Binding="{Binding Path=ID}" Header="ID" />
                 <DataGridTextColumn Binding="{Binding Path=Bezeichnung}" Header="Produkt" />
             </DataGrid.Columns>

Außerdem wollen wir, wie schon bei den übrigen Übersichten, ein Öffnen der Produktdetails per Doppelklick erlauben und fügen dazu einen EventSetter hinzu:

             <DataGrid.Resources>
                 <Style TargetType="DataGridRow">
                     <EventSetter Event="MouseDoubleClick" Handler="Row_DoubleClick"/>
                 </Style>
             </DataGrid.Resources>
         </DataGrid>

Schließlich folgen noch die beiden Schaltflächen zum Speichern und Verwerfen der aktuellen Änderungen:

         <StackPanel Orientation="Horizontal" Grid.Row="6" Grid.ColumnSpan="4" >
             <Button x:Name="btnSpeichern" Margin="3" Padding="3" Click="btnSpeichern_Click" Height="23" Content="Speichern"></Button>
             <Button x:Name="btnVerwerfen" Margin="3" Padding="3" Click="btnVerwerfen_Click" Height="23" Content="Verwerfen"></Button>
         </StackPanel>
     </Grid>
</Page>

Der Entwurf sieht in der XAML-Ansicht nun wie in Bild 2 aus. Im Entwurf haben wir nun schon einige Bindungen gesehen, die wir nun in der Code behind-Klasse bereitstellen wollen. Diese Klasse enthält zunächst eine Variable für das DBContext-Element, über das wir auf das Entity Data Model zugreifen (dbContext). Außerden finden wir hier die Definition für die Objekte, an die wir die Elemente der XAML-Seite binden wollen. Die erste ist das Kategorie-Objekt, das wir in der öffentlichen Variablen kategorie speichern.

Entwurf der Kategoriedetail-Seite

Bild 2: Entwurf der Kategoriedetail-Seite

Die zweite ist das ObservableCollection-Objekt Produkte, deren Inhalt wie in der als privat deklarierten Variablen produkte speichern und über entsprechende Eigenschaften für den Zugriff von außen bereitstellen:

public partial class Kategoriedetails : Page {	//Kategoriedetails.xaml.cs
     private BestellverwaltungEntities dbContext;
     public Kategorie kategorie { get; set; }
     private ObservableCollection<Produkt> produkte;
     public ObservableCollection<Produkt> Produkte {
         get {
             return produkte;
         }
         set {
             produkte = value;
         }
     }
     ...
}

Die Konstruktor-Methode Kategoriedetails, die beim Erstellen eines Objekts auf Basis unserer Klasse aufgerufen wird, erwartet als Parameter den Primärschlüsselwert der ID einer Kategorie. Wird dieser nicht übergeben, setzt die Methode ihn auf 0 fest – dies bedeutet, dass ein neues, leeres Kategorie-Element angezeigt werden soll.

Die Methode erstellt ein neues Objekt auf Basis der Klasse BestellverwaltungEntities und stellt den DataContext auf die Code behind-Klasse (this) ein, damit die XAML-Elemente an die per Eigenschaft zugängig gemachten Elemente der Code behind-Klasse zugreifen können. Ist die mit dem Parameter kategorieID übergebene ID der Kategorie nicht gleich 0, sucht die Find-Methode in der Kategorien-Auflistung nach dem entsprechenden Element. Anderenfalls wird ein neues Kategorie-Element angelegt.

public Kategoriedetails(long kategorieID = 0) {	//Kategoriedetails.xaml.cs
     InitializeComponent();
     dbContext = new BestellverwaltungEntities();
     DataContext = this;
     if (kategorieID != 0) {
         kategorie = dbContext.Kategorien.Find(kategorieID);
     }
     else {
         kategorie = new Kategorie();
         dbContext.Kategorien.Add(kategorie);
     }
     produkte = new ObservableCollection<Produkt>(dbContext.Produkte.Where(d => d.KategorieID == kategorie.ID));
}

Danach folgt der interessante Schritte: Wir füllen das DataGrid, und zwar nur mit den Entitäten, die einem bestimmten Suchkriterium entsprechen.

Dieses soll nur die Entitäten liefern, deren Feld KategorieID dem Wert des Feldes ID der angezeigten Kategorie-Entität entspricht. Dazu verwenden wir einen entsprechenden Linq-Ausdruck (siehe auch Artikel LINQ to Entities: Daten abfragen):

produkte = new ObservableCollection<Produkt>(dbContext.Produkte.Where(d=>d.KategorieID==kategorie.ID));

Dies war die Leseprobe dieses Artikels.
Melden Sie sich an, um auf den vollständigen Artikel zuzugreifen.

Bitte geben Sie die Zeichenfolge in das nachfolgende Textfeld ein

Die mit einem * markierten Felder sind Pflichtfelder.

Ich habe die Datenschutzbestimmungen zur Kenntnis genommen.