Montag, 11. November 2013

WPF Tutorial Teil 4 - Databinding

Ein weiteres nettes Feature von WPF ist Databinding. Damit wird die Möglichkeit beschrieben, die grafische Benutzeroberfläche direkt mit den zu Grunde liegenden Daten zu verbinden - zum Beispiel eine Textbox mit einer Stringvariablen einer Klasse. Je nach Einstellung wird dann eine Änderung des Strings direkt in der Textbox angezeigt und / oder umgekehrt eine Änderung übernommen.

Im heutigen Post möchte ich dazu ein Beispiel vorstellen, welches Daten eines Mitarbeiters verwaltet. Dieser wird durch die Klasse Employee beschrieben, gespeichert werden Vorname, Nachname, Gehalt und Status.
Beim Databinding wird nun immer ein Formular, ein Steuerlement o.ä. mit einer Instanz einer Klasse verknüpft. Hier verknüpfen wir das ganze Formular mit einer Instanz der Klasse Employees, dann können verschiedene Steuerelemente dadrauf die verschiedenen Eigenschaften der Klasse darstellen.
Nach dem Anlegen einer Instanz namens Employee1 legen wir die Datenquelle des Formulars folgendermaßen fest (z.B. im Konstruktor):

this.DataContext = Employee1;

Die Verknüpfung der einzelnen Steuerelemente mit den Variablen des Angestellten können wir nun in der XAML Datei vornehmen, beispielsweise so:

<TextBox Name="txtName" Grid.Row="0" Grid.Column="0" Height="25" Text="{Binding Path=LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>

Wie man sieht, wird der Eigenschaft Text des Steuerelements kein direkter Text zugewiesen, sondern es wird ein Binding angelegt. Path beschreibt den Pfad des Bindings. Da wir für das gesamte Formular Employee1 als Datenquelle festgelegt haben, wird die Textbox hier mit der Variablen LastName von Employee1 verknüpft.
Die Eigenschaft Mode gibt an, wie Änderungen übernommen werden sollen. TwoWay bedeutet, dass Änderungen in der Textbox an die verbundene Variable weitergeleitet werden sollen, und Änderungen in dieser an die Textbox weitergeleitet werden. Weitere Möglichkeiten sind OneWay (Änderung in der Variablen wird in der Textbox angezeigt) und OneWayToSource (Änderung in der Textbox wird an die Variable weitergeleitet).
Die Eigenschaft UpdateSourceTrigger gibt an, wann die Bindung aktiv wird. Im Falle von Explicit werden Änderungen nur propagiert, wenn die Methode UpdateSource() aufgerufen wird. LostFocus gibt an, dass Änderungen übernommen werden, wenn das verbundene Steuerelement den Fokus verliert, und das hier verwendete PropertyChanged drückt aus, dass Änderungen unverzüglich bei Änderungen weitergeleitet werden.

Voraussetzung dafür, dass Databinding verwendet werden kann, ist die Benutzung von Getter und Setter. Bei der Variablen LastName kann das zum Beispiel so aussehen:

private string lastName;
public string LastName
{
    get { return lastName; }
    set
    {
        lastName = value;
    }
}

Die "eigentliche" Variable ist hier lastName. Diese ist aber hinter dem öffentlichen Feld LastName "versteckt", welches beim Abrufen über get den Wert von lastName zurückgibt, oder diesen setzt. Der Vorteil von einer solchen Vorgehensweise ist die Möglichkeit zur Überprüfung und Wahrung der Datenkonsistenz.

Führt man den Code nun so aus, erkennt man ein Problem: Änderungen in der Textbox werden zwar direkt an die Variable weitergegeben, das heißt, sie speichert den neuen Wert, allerdings führen Änderungen in der Variablen nicht zu Änderungen in der Textbox. Dieses müssen wir manuell auslösen. Dafür muss zuerst die Klasse Employee von INotifyPropertyChanged erben. Anschließend erstellen wir eine Benachrichtigungsfunktion:

        public event PropertyChangedEventHandler PropertyChanged;

        private void Notify(string argument)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(argument));
            }
        }

Im Setter der gewünschten Variablen rufen wir dann diese Funktion auf, z.B. also Notify("LastName").
Nun ist das Databinding in beide Richtungen funktionstüchtig.

Eine Sache die ich noch ansprechen möchte ist DataConversion. Im Code ist die Eigenschaft Salary vom Typ int vorhanden und Position vom Typ einer eigenen Aufzählung. Beim Databinding wird dieses automatisch in einen String umgewandelt, was auch kein Problem ist, da .Net Methoden zur Umwandlung kennt. Was aber, wenn man z.B. eine eigene Klasse geschrieben hat und diese in einen String umwandeln möchte? Dann muss man einen Converter schreiben. Aber diesen kann man auch so benutzen, um z.B. die Umwandlung zu personalisieren.
Ich tue dies hier bei der Umwandlung der Aufzählung. Diese enthält 3 Typen: Undefined, Trainee, Manager. Auch wenn Employee1 noch nichts zugewiesen wurde, zeigt das für Position zuständige Textfeld Undefined an (das gleiche gilt für Gehalt: 0). Dies ändern wir nun, so dass bei der Konvertierung "" zurückgegeben wird wenn die Variable auf Undefined steht.
Dafür brauchen wir eine Converter Klasse, welche wir uns selber schreiben und die wie folgt aussieht:

    [ValueConversion(typeof(Employee.Positions), typeof(string))]
    public class EmployeePositionsConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if ((int)value == 0)
                return "";
            else
                return value.ToString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            switch ((string)value)
            {
                case "Trainee":
                    return Employee.Positions.Trainee;
                case "Manager":
                    return Employee.Positions.Trainee;
                default:
                    return Employee.Positions.Undefined;
            }
        }
    }

Über dem Namen geben wir an, zwischen welchen 2 Typen konvertiert wird. Die Funktion Convert() ist dann für die Konvertierung von Employee.Positions nach String zuständig, ConvertBack() für die Rückrichtung. Ich denke die Funktionsweise ist klar.
Wir müssen dem Programm nun jedoch noch sagen,den Converter zu benutzen. Dazu legen wir uns in der XAML Datei im Window Tag zuerst ein Attribute "c" an, welches unseren Namespace speichert:

xmlns:c="clr-namespace:WPFDatabinding"

Dann fügen wir dem Projekt eine Ressource hinzu:

    <Window.Resources>

        <c:EmployeePositionsConverter x:Key="EmployeePositionsConverter"/>
    </Window.Resources>


Die Bedeutung dieses Codes ist folgende: Da in "c" unser Namespace gespeichert ist, greifen wir mit c:EmployeePositionsConverter auf unseren Converter zu und machen ihn unter dem Namen EmployeePositionsConverter zugreifbar.
In der Definition der Textbox können wir ihn dann einfach verwenden:

<TextBox Name="txtPosition" Grid.Row="3" Grid.Column="0" Height="25" Text="{Binding Path=Position, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EmployeePositionsConverter}}"></TextBox>

Nun der komplette Code des Programs - es sind 4 Textfelder und 2 Buttons enthalten. Die Textfelder stellen wie beschrieben die Eigenschaften von Employee1 dar. Bei Klick auf Button 1 wird Employee1 mit Daten gefüllt (er ist dann Hans Wurst), die Textfelder ändern sich entsprechend. In die Textfelder können aber auch neue Daten eingegeben werden, bei Klick auf Button 2 werden die in Employee1 gespeicherten Daten ausgegeben.

MainWindow.xaml:


<Window x:Class="WPFDatabinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:c="clr-namespace:WPFDatabinding"

        Title="MainWindow" Height="350" Width="525"
        >

    <Window.Resources>
        <c:EmployeePositionsConverter x:Key="EmployeePositionsConverter"/>
    </Window.Resources>

    <Grid Height="160" Width="200" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <TextBox Name="txtName" Grid.Row="0" Grid.Column="0" Height="25" Text="{Binding Path=LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
        <TextBox Name="txtFirstName" Grid.Row="1" Grid.Column="0" Height="25" Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
        <TextBox Name="txtSalary" Grid.Row="2" Grid.Column="0" Height="25" Text="{Binding Path=Salary, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
        <TextBox Name="txtPosition" Grid.Row="3" Grid.Column="0" Height="25" Text="{Binding Path=Position, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EmployeePositionsConverter}}"></TextBox>
        <Button Name="btnNew" Grid.Row="4" Grid.Column="0" Width="100" Height="25" Click="btnNew_Click" Margin="-80, 0, 0,0">Hans Wurst</Button>
        <Button Name="btnCheck" Grid.Row="4" Grid.Column="0" Width="50" Height="25" Click="btnCheck_Click"  Margin="80, 0, 0,0">Check</Button>
    </Grid>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.ComponentModel;

namespace WPFDatabinding
{
   ///





   /// Interaktionslogik für MainWindow.xaml
   ///

    public partial class MainWindow : Window
    {
        Employee Employee1 = new Employee();
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = Employee1;
        }

        private void btnNew_Click(object sender, RoutedEventArgs e)
        {
            Employee1.LastName = "Wurst";
            Employee1.FirstName = "Hans";
            Employee1.Salary = 1000;
            Employee1.Position = Employee.Positions.Trainee;
        }

        private void btnCheck_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Employee1.LastName + Environment.NewLine + Employee1.FirstName + Environment.NewLine + Employee1.Salary.ToString() + Environment.NewLine + Employee1.Position.ToString());
        }
    }

    [ValueConversion(typeof(Employee.Positions), typeof(string))]
    public class EmployeePositionsConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if ((int)value == 0)
                return "";
            else
                return value.ToString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            switch ((string)value)
            {
                case "Trainee":
                    return Employee.Positions.Trainee;
                case "Manager":
                    return Employee.Positions.Trainee;
                default:
                    return Employee.Positions.Undefined;
            }
        }
    }

    class Employee : INotifyPropertyChanged
    {
        public enum Positions {Undefined, Manager, Trainee}
        public event PropertyChangedEventHandler PropertyChanged;

        private void Notify(string argument)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(argument));
            }
        }

        private string firstName;
        private string lastName;
        private int salary;
        private Positions position;

        public string FirstName
        {
            get { return firstName; }
            set
            {
                firstName = value;
                Notify("FirstName");
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                lastName = value;
                Notify("LastName");
            }
        }

        public int Salary
        {
            get { return salary; }
            set
            {
                salary = value;
                Notify("Salary");
            }
        }

        public Positions Position
        {
            get { return position; }
            set
            {
                position = value;
                Notify("Position");
            }
        }
    }
}


Auf Udo's Blog gibt es ebenfalls einen Post zu diesem Thema.

Kommentare:

  1. ConvertBack -> case "Manager" gibt "Trainee" zurück ;-)

    AntwortenLöschen
  2. Evtl habe ich auch irgendwo einen Fehler drin, aber mit dem Converter kann ich den Wert nur noch ändern, wenn "Manager" oder "Trainee" z.B. rein kopiert werden.
    D.h. bei einer Eingabe geschieht absolut nichts...

    Würde also bedeuten, dass der wie er hier beschrieben ist, nur für ComboBox o.ä. funktionieren würde

    AntwortenLöschen