## 21.7 WPF und ADO.NET
Anmerkung
In diesem Abschnitt werden wir uns mit der Bindung an Daten beschäftigen, die aus einer Datenbank stammen. Das Thema Datenbanken wird uns erst später in diesem Buch begegnen. Möglicherweise werden Ihnen daher einige Klassen und der datenbankspezifische Code noch vollkommen unbekannt sein. Als Autor steht man aber des Öfteren vor dem Problem, den Weg eines thematisch sinnvollen Aufbaus des Buches verlassen zu müssen, weil Themen getriebeartig ineinandergreifen. Sollten Sie mit ADO.NET bzw. Datenbankzugriffen unter .NET ganz allgemein noch keine Erfahrungen haben, sollten Sie mit dem nächsten Kapitel fortfahren und erst später die folgenden Seiten lesen.
In der Praxis werden Sie in der Regel weniger einzelne Objekte oder Listen an Steuerelemente binden. Vielmehr wird es sich eher um Daten aus Datenbanken handeln, die – in unterschiedlicher Form aufbereitet – ihren Weg in die Benutzeroberfläche nehmen. Deshalb sei in diesem letzten Abschnitt des Kapitels anhand einiger Beispiele gezeigt, wie Sie die Daten an die Steuerelemente binden. Dazu werden wir zuerst mit ADO.NET-Objekten arbeiten, anschließend auch noch mit LINQ to SQL. ### 21.7.1 Ausgabe in einer ListBox Das erste, einfach gehaltene Beispiel soll den Inhalt der Tabelle Products der Northwind-Datenbank in einem ListBox-Steuerelement anzeigen. Angezeigt werden sollen die Inhalte der Spalten ProductName, UnitPrice und UnitsInStock. Um alle drei Angaben in einem Item zusammenzufassen, wird die Angabe eines DataTemplate notwendig sein. Das Ergebnis zur Laufzeit sehen Sie in Abbildung 21.6. Lassen Sie uns zuerst die Daten bereitstellen. Das ist im Grunde genommen sehr simpel: Wir besorgen uns zuerst das DataSet und binden dieses an den Datenkontext der ListBox:
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 21\WPF_ADOSample1
    // ---------------------------------------------------------
    public MainWindow() {
      InitializeComponent();
      SqlConnection con = new SqlConnection( @"...");
      SqlCommand cmd = new SqlCommand(
              "SELECT ProductName, UnitPrice, UnitsInStock FROM Products", con);
      DataSet ds = new DataSet();
      SqlDataAdapter da = new SqlDataAdapter(cmd);
      da.Fill(ds);
      listBox1.DataContext = ds;
    }
Hier klicken, um das Bild zu vergrößern Abbildung 21.6 Ausgabe des Beispiels »WPF_ADOSample1« Sehen wir uns nun den XAML-Code an:
    <Window ...>
      <Window.Resources>
        <DataTemplate x:Key="listBoxTemplate">
          <StackPanel Margin="3">
            <DockPanel>
              <TextBlock Text="Artikel:" DockPanel.Dock="Left"
                         Margin="5,0,10,0"/>
              <TextBlock Text="             " />
              <TextBlock Text="{Binding ProductName}"
                         Foreground="Blue" FontWeight="Bold" />
            </DockPanel>
            <DockPanel>
              <TextBlock Text="Preis:" DockPanel.Dock="Left" Margin="5,0,5,0"/>
              <TextBlock Text="                 " />
              <TextBlock Text="{Binding UnitPrice}"  FontWeight="Bold" />
            </DockPanel>
            <DockPanel>
              <TextBlock Text="Lagerbestand:" DockPanel.Dock="Left"
                         Margin="5,0,5,0"/>
              <TextBlock Text="   " />
              <TextBlock Text="{Binding UnitsInStock}" FontWeight="Bold" />
            </DockPanel>
            <DockPanel>
              <TextBlock Text="----------------------------------------------"/>
            </DockPanel>
          </StackPanel>
        </DataTemplate>
      </Window.Resources>
      <Grid>
        <ListBox Margin="17,8,15,26" Name="listBox1"
                 ItemsSource="{Binding Tables[0]}"
                 ItemTemplate="{StaticResource listBoxTemplate}" />
      </Grid>
    </Window>
Die ListBox müssen wir mit der Eigenschaft ItemSource an die Datenquelle binden. Dabei wird die Tabelle im DataSet angegeben:
    ItemsSource="{Binding Tables[0]}"
Sie können dem Datenkontext auch direkt die gewünschte Tabelle übergeben und müssen dann nur die Bindung von ItemSource an den Datenkontext beschreiben:
    listBox1.DataContext = ds.Tables[0];
    ...
    ItemsSource="{Binding}"
Jeder Listeneintrag eines Produkts wird durch ein StackPanel in DataTemplate beschrieben. Da wir mit jedem Listeneintrag auch drei Ausgabezeilen produzieren wollen, wird jede Zeile durch ein DockPanel beschrieben, das in sich wieder drei TextBlock-Elemente hat. Das TextBlock-Element, das entweder den Produktbezeichner, den Preis oder den Lagerbestand anzeigen soll, binden wir an die entsprechende Spalte der gebundenen Tabelle, z. B. so:
    <TextBlock Text="{Binding ProductName}"
### 21.7.2 Eine Tabelle im DataGrid-Steuerelement #### Bindung des DataGrid Tabellensteuerelemente gehören sicherlich zu den am häufigsten eingesetzten Controls in grafischen Benutzeroberflächen. Mit Einführung von Visual Studio 2008 und dem .NET-Framework 3.5 waren viele Entwickler überrascht, dass Microsoft für die WPF ein solches Steuerelement nicht vorgesehen hatte. Nur Drittanbieter oder das später veröffentlichte WPF-Toolkit konnten diese Lücke füllen. In Visual Studio 2010 wird nun ein WPF-Tabellensteuerelement standardmäßig angeboten. Wie nicht anders zu erwarten, glänzt dieses Steuerelement durch zahllose Eigenschaften, die zusammen mit den WPF-typischen Möglichkeiten nur noch wenige Wünsche offenlassen (zumindest so weit diese nicht ganz spezifischer Natur sind). Es ist sehr einfach, das DataGrid-Control mit einer ADO.NET-Datenquelle zu verbinden. Sie übergeben das DataSet der Eigenschaft DataContext entweder des Windows oder des DataGrids, zum Beispiel:
    public MainWindow() {
      InitializeComponent();
      ...
      da.Fill(ds, "Orders");
      this.DataContext = ds;
    }
Die Variable da beschreibt den DataAdapter, und ds beschreibt das DataSet. Da ein DataGrid zu den Listensteuerelementen zählt, wird mit der Eigenschaft ItemSource die Bindung an die Datenquelle beschrieben. Mit der Eigenschaft AutoGenerateColumns legen Sie fest, ob die Spalten automatisch erzeugt werden sollen. Im einfachsten Fall können Sie hier true angeben, den Rest erledigt das Steuerelement selbst.
    <DataGrid ItemsSource="{Binding Orders}"
              AutoGenerateColumns="true" HorizontalAlignment="Left"
              Name="dataGrid1"/>
In Abbildung 21.7 sehen Sie die Ausgabe des Fensters zur Laufzeit. Hier klicken, um das Bild zu vergrößern Abbildung 21.7 Das DataGrid-Steuerelement #### Spalten modifizieren Wenn Sie die Eigenschaft AutoGenerateColumns auf false festlegen, müssen Sie selbst für die Anzeige der Spalten sorgen. Was sich im ersten Moment nach unnötigem Codierungsaufwand anhört, eröffnet weitreichende Möglichkeiten in der individuellen Gestaltung der einzelnen Spalten. Per Vorgabe werden alle Spalte als reine Textspalten angezeigt. Daneben werden auch Spalten vom Typ Hyperlink, CheckBox und ComboBox angeboten, beschrieben durch die Klassen DataGridHyperlinkColumn, DataGridCheckBoxColumn und DataGridComboBoxColumn. Sollte Ihnen auch das noch nicht ausreichen, können Sie mit DataGridTemplateColumn auch eigene Spaltentypen definieren. DataGridTemplateColumn ermöglicht es mit seinen Eigenschaften CellTemplate und CellEditingTemplate sogar, Templates für den Anzeige- und Änderungszustand bereitzustellen. Damit ist noch nicht alles beschrieben. Mit der Eigenschaft RowsDetailsTemplate kann unterhalb der ausgewählten Datenzeile ein Bereich geöffnet werden, der beliebige Informationen beinhalten kann (siehe Abbildung 21.8). Wir wollen uns auch sofort das Beispielprogramm ansehen, das zu der Ausgabe des Fensters in Abbildung 21.8 führt. Dabei gehen wir noch einen Schritt weiter und machen uns zur Aufgabe, in der Spalte Lieferdatum im Editiermodus das Steuerelement DatePicker einzublenden (siehe Abbildung 21.9). Hier klicken, um das Bild zu vergrößern Abbildung 21.8 DataGrid mit Detailbereich Hier klicken, um das Bild zu vergrößern Abbildung 21.9 Das DatePicker-Steuerelement zum Editieren Per Vorgabe wird danach neben dem Datum auch die Uhrzeit angezeigt. Da uns die Uhrzeit allerdings bei der Anzeige des Lieferdatums nicht interessiert, soll ein Konverter bereitgestellt werden, der die Uhrzeit ausblendet.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 21\WPF_ADOSample2
    // ---------------------------------------------------------
    <Window ... xmlns:local="clr-namespace:WPF_ADOSample2">
    <Window.Resources>
       <local:DateConverter x:Key="dateConverter"></local:DateConverter>
    </Window.Resources>
    <StackPanel>
      <DataGrid ItemsSource="{Binding Orders}" AutoGenerateColumns="False"
                Name="dataGrid1" SelectionChanged="dataGrid1_SelectionChanged"
                AlternatingRowBackground="#FFBBF0D1">
        <DataGrid.RowDetailsTemplate>
          <DataTemplate>
            <StackPanel>
              <TextBlock Text="BestellNr.:"/>
              <TextBlock Text="  "/>
              <TextBlock Text="{Binding OrderID}" />
              <TextBlock Text="  "/>
              <TextBlock Text="{Binding ShipPostalCode}"/>
              <TextBlock Text="  "/>
              <TextBlock Text="{Binding ShipCity}"/>
            </StackPanel>
          </DataTemplate>
        </DataGrid.RowDetailsTemplate>
        <DataGrid.Columns>
          <DataGridTextColumn Header="Bestell-Nr." Binding="{Binding OrderID}"/>
          <DataGridTemplateColumn Header="Lieferdatum">
            <DataGridTemplateColumn.CellEditingTemplate>
              <DataTemplate>
                <DatePicker SelectedDate="{Binding ShippedDate}" />
              </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
            <DataGridTemplateColumn.CellTemplate>
              <DataTemplate>
                <TextBlock Text="{Binding Path=ShippedDate,
                           ConverterCulture=de-DE,
                           Converter={StaticResource dateConverter}}" />
              </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
          </DataGridTemplateColumn>
          <DataGridTextColumn Header="Adresse"
                              Binding="{Binding ShipAddress}" />
          <DataGridTextColumn Header="Stadt" Binding="{Binding ShipCity}" />
          <DataGridTextColumn Header="Postleitzahl"
                              Binding="{Binding ShipPostalCode}" />
          <DataGridTextColumn Header="Land"
                              Binding="{Binding ShipCountry}" />
        </DataGrid.Columns>
      </DataGrid>
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="150" />
          <ColumnDefinition Width="150" />
          <ColumnDefinition Width="150" />
        </Grid.ColumnDefinitions>
         <Button Name="btnUpdate" Click="btnUpdate_Click" Content="Speichern" />
      </Grid>
    </StackPanel>
    </Window>
Der XAML-Code wurde hier um die Layout-Eigenschaftseinstellungen gekürzt, die im Zusammenhang mit der Datenbindung unwichtig sind. Betrachten wir nun einzelne Passagen im Detail. Mit
    <DataGridTemplateColumn.CellEditingTemplate>
      <DataTemplate>
        <DatePicker SelectedDate="{Binding ShippedDate}" />
      </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
wird die Spalte Lieferdatum im Editiermodus beschrieben. Definiert wird dieser durch ein DataTemplate, das an die Eigenschaft CellEditingTemplate übergeben wird und ein DatePicker-Control anzeigt. Der Anwender kann aus dem Control ein anderes Datum auswählen. Die Eigenschaft CellTemplate des DataGridTemplateColumn-Objekts steht für den Anzeigemodus der Spalte Lieferdatum.
    <DataGridTemplateColumn.CellTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding Path=ShippedDate,
                   ConverterCulture=de-DE,
                   Converter={StaticResource dateConverter}}" />
      </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
Innerhalb des Templates stellen wir mit der Eigenschaft ConverterCulture sicher, dass für die Anzeige des Datums das übliche deutsche Format verwendet wird. Hat der Benutzer aus dem DatePicker-Steuerelement ein anderes Datum ausgewählt, würde dieses zusammen mit der Uhrzeit angezeigt. Da die Uhrzeit nicht interessiert, filtern wir sie mit dem Converter dateConverter heraus. Die Klasse des Converters ist folgendermaßen codiert:
    class DateConverter : IValueConverter {
      public object Convert(object value, Type targetType, object parameter,
                            System.Globalization.CultureInfo culture)
      {
        return ((DateTime)value).ToShortDateString();
      }
      public object ConvertBack(object value, Type targetType, object parameter,
                                System.Globalization.CultureInfo culture)
      {
        return DateTime.Parse(value.ToString());
      }
    }
Die Converter-Klasse ist im Resources-Abschnitt des Window eingetragen. Kommen wir zum nächsten interessanten Teilbereich des XAML-Codings: dem Detailbereich der ausgewählten Datenzeile im DataGrid. Dieser wird durch die Eigenschaft RowDetailsTemplate des Tabellensteuerelements beschrieben, in der ein DataTemplate eingebettet ist.
    <DataGrid.RowDetailsTemplate>
      <DataTemplate>
        <StackPanel>
          <TextBlock Text="BestellNr.:"/>
          <TextBlock Text="  "/>
          <TextBlock Text="{Binding OrderID}" />
             ...
          </StackPanel>
        </DataTemplate>
     </DataGrid.RowDetailsTemplate>
Es bleibt natürlich Ihnen überlassen, wie Sie diesen Bereich darstellen. In unserem Beispiel werden nur einige Informationen der ausgewählten Datenzeile im Großformat angezeigt. Zum Schluss bleibt uns noch, einen Blick in den Code-Behind zu werfen. Im Konstruktor wird die Verbindung zur Datenbank aufgebaut und das DataSet gefüllt. Eine Schaltfläche im Window gestattet es uns, die Änderungen in die Datenbank zu schreiben. Es genügt hierzu der Aufruf der Methode Update des SqlDataAdapters. Um zu zeigen, wie Sie auf den Inhalt einer bestimmten Zelle des selektierten Datensatzes zugreifen können, wurde das Ereignis SelectionChanged des DataGrids programmiert. Das Ereignis wird immer dann ausgelöst, wenn sich die Datenzeilenauswahl ändert. Der Ereignishandler benutzt das Ereignis, um die Bestellnummer in die Titelleiste des Window einzutragen.
    private void btnUpdate_Click(object sender, RoutedEventArgs e) {
      da.Update(ds, "Orders");
    }
    private void dataGrid1_SelectionChanged(object sender,
                 SelectionChangedEventArgs e) {
      string orderID =
             ((DataRowView)dataGrid1.SelectedItem)["OrderID"].ToString();
      this.Title = "Bestell-Nr.: " + orderID;
    }
### 21.7.3 WPF und LINQ to SQL Das letzte Beispiel, mit dem ich Ihnen die Datenbindung unter WPF zeigen möchte, ist ein Beispiel, bei dem LINQ to SQL zum Einsatz kommt. Das Beispiel ist sehr einfach gehalten und glänzt mehr durch sein farbenfrohes Layout als durch besondere Finessen. Es werden einfach nur die Kunden abgefragt, die in Deutschland wohnen. Angezeigt wird das Resultat der Abfrage in einem ListView-Steuerelement.
    // ---------------------------------------------------------
    // Beispiel: ...\Kapitel 21\WPF_LINQ_To_SQL
    // ---------------------------------------------------------
    public MainWindow() {
      InitializeComponent();
      NorthwindDataContext db = new NorthwindDataContext();
      var customers = from c in db.Customers
                      where c.Country == "Germany"
                      select c;
      listView1.ItemsSource = customers;
    }
Dazu noch der XAML-Code:
    <Window ... >
      <Window.Resources>
        <DataTemplate x:Key="ShowCustomer">
          <StackPanel Orientation="Horizontal">
            <StackPanel Margin="10">
              <TextBlock Text="{Binding ContactName}" FontWeight="Bold"/>
              <TextBlock Text="{Binding ContactTitle}" FontStyle="Italic"/>
              <TextBlock Text="{Binding ContactName}"/>
            </StackPanel>
          </StackPanel>
        </DataTemplate>
      </Window.Resources>
      <ScrollViewer>
        <DockPanel>
          <ListView Name="listView1"
                    ItemTemplate="{StaticResource ShowCustomer}">
            <ListView.Background>
              <RadialGradientBrush>
                <GradientStop Color="Aqua"  Offset="0"/>
                <GradientStop Color="Bisque" Offset="1"/>
              </RadialGradientBrush>
            </ListView.Background>
          </ListView>
        </DockPanel>
      </ScrollViewer>
    </Window>
Hier klicken, um das Bild zu vergrößern