Monday, November 11, 2013

WPF Tutorial Part 4 - Databinding

Another nice feature of WPF is Databinding. With that we mean the possibility of connecting the graphical user interface directly with the underlying data - for example a textbox with a string variable of a class. Depending on the settings then a change in the string is directly shown in the textbox and / or the otherway around.

In today's post I want to present an example for this, which manages data of an employee. An employee is represented by the class Employee, saved are first name, last name, salary and position.
In databinding always one form, control or the like is connected to an instance of a class. Here we connect the whole form with an instance of the class Employee, then the different controls on it can represent the different properties of the class.
After creating an instance with the name Employee1 we set the datasource of the form as follows (for examle in the constructor):

this.DataContext = Employee1;

The linking of the single controls to variables of the employee we do in the XAML file, for example like this:

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

As you can see, the property Text of the control is not directly a text assigned but a binding is created. Path describes the path of the binding. Since we set Employee1 as data source for the whole form, the textbox is here linked to the variable LastName of Employee1.
The property Mode describes, how changes are forwarded. TwoWay means, that changes in the textbox are to be forwarded to the connected variable and changes in this to the textbox. Further possibilites are OneWay (changes in the variable are propagated to the textbox) and OneWayToSource (changes in the textbox are forwarded to the variable).
The property UpdateSourceTrigger describes, when the binding becomes active. In the case of Explicit changes are only propagated, when the function UpdateSource() is called. LostFocus says, that changes are only accepted when the connected control loses focus, and the here used PropertyChanged describes, that changes are to be forwarded immediately.

Requirement for using databinding is the use of a getter and setter.
For the variable LastName this can look for example as follows:

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

The "actual" variable is here lastName. But this is "hidden" behind the public field LastName, which can return the value via get or set it via set.
The advantage of this is the possibility of checking and maintaining data consistency.

If you run the code like this, you will find a problem: Changes in the textbox are forwarded to the variable, meaning it saves the new value, but changes in the variable are not displayed in the textbox. We have to trigger this manually. For that first the class Employee has to inherit from INotifyPropertyChanged. Subsequently, we create a notify function:

        public event PropertyChangedEventHandler PropertyChanged;

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

In the setter of the variable we then call this function, so e.g. Notify("LastName").
Now the databinding works in both directions.

One last thing I want to mention is DataConversion. In the code there is a property Salary of type int and Position of the type of a custom enumeration. In the databinding these are automatically converted to a string, which is no problem, since .Net knows conversions for this. But what, if you have written a custom class and want to convert this to a string for representation in a textbox? Then you have to write a converter. But of course you can also use one like that, e.g. for customizing the conversion.
I do this here for the conversion of the enumeration. It contains 3 values: Undefined, Trainee and Manager. Although Employee1 was not assigned anything, the textbox responsible for Position displays Undefined (the same holds for salary: 0). We now change this, so that "" is returned in the conversion when the variable is set to Undefined.
For that we need a converter class, which we write ourselves:

    [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;
            }
        }
    }

Above the name we declare, between which 2 types we convert. The function Convert() is then responsible for converting from Employee.Positions to String, ConvertBack() for the other way. I think the principle should be clear.
We now have to tell the program to use this converter. For that we first create in the XAML file in the Window tag an attribute named "c" for example, which saves our namespace:

xmlns:c="clr-namespace:WPFDatabinding"

Then we add a new ressource to the project:

    <Window.Resources>

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


The meaning of this code is the following: Since in "c" our namespace is saved, we access the converter using c:EmployeePositionsConverter and make it accesible under the name EmployeePositionsConverter. In the definition of the textbox we then can simply use it:

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

Now the complete code of the program - there are 4 textboxes and 2 buttons contained. The textboxes represent the properties of Employee1 as described. After clicking Button 1 Employee1 is filled with data (he then is "Hans Wurst"), the textboxes change accordingly. We can also enter data in the textboxes, when clicking Button 2 the data saved in Employee1 are displayed.

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");
            }
        }
    }
}


On Udo's Blog there is also a post regarding this topic.

No comments:

Post a Comment