Skip to content

bindings

Frederic Vogels edited this page Feb 15, 2018 · 2 revisions

Bindings

In this section, we discuss one of the most powerful features of WPF: bindings. For this, we need to reevaluate the design of our GUI somewhat.

We will replace the three buttons by a single slider. All three text boxes should show the same temperature in different scales at all times. The slider can then be used to increase or decrease the temperature.

To get a preview, switch to the snapshot tagged fahrenheit-binding.

Exercise

Remove the buttons from the XAML file. You might want to keep the Click-handling methods for a while, as they contain code that might come in handy later. Don't forget to remove the redundant ColumnDefinitions too.

Snapshot: buttonectomy

We now add a slider. A slider has a Value that can range from some Minimum to some Maximum, which can be specified by us. The slider value will represent a certain temperature which needs to be shown in the three scales. For simplicity, we will let the slider coincide with the Kelvin scale. 0K is the lowest possible temperature, so that will correspond to the leftmost position. There is no highest possible temperature, so let's just pick 1000K.

Exercise

Add a slider. It should be the last child in the StackPanel, i.e., it should be positioned below all the other controls.

  • Name it slider.
  • Set its Minimum property to 0.
  • Set its Maximum property to 1000.
  • Just like a Button has a Click event, a Slider has a ValueUpdated event. Let it be handled by a method called SliderValueChanged.
  • SliderValueChanged should read the slider's value, convert it to °C; °F and K, and update the text boxes accordingly.
  • Remove the old Click handling methods.

Run your program and check that moving the slider does indeed update all three text boxes. Note that it doesn't work the other way around: if you change a text box's contents, the slider does not adapt its position. This of course is to be expected, but it is a shortcoming which we'll tackle later on.

Snapshot: slider

As explained before, the slider's value coincides with the Kelvin scale: if the slider is positioned on 0, the Kelvin text box should show 0. If the slider's value is 500, so must the text box's be. In essence, the Kelvin text box simply shows the slider's value. It is possible to encode this relationship directly, using bindings.

Exercise

SliderValueChanged contains code that updates the Kelvin text box. Remove this code.

Run your application and verify that the Kelvin text box does indeed remain empty.

Snapshot: unresponsive-kelvin

Update MainWindow.xaml as follows:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Text="Kelvin" Style="{StaticResource labelStyle}" />
-       <TextBox Grid.Row="1" x:Name="kelvinTextBox" Style="{StaticResource textBoxStyle}" />
+       <TextBox Grid.Row="1" Text="{Binding ElementName=slider, Path=Value}" Style="{StaticResource textBoxStyle}" />
    </Grid>
    <Slider x:Name="slider" Minimum="0" Maximum="1000" ValueChanged="SliderValueChanged" />

Run your program. Notice the following details:

  • Although we removed the C# code that updates the Kelvin text box, it still gets updated. This is due to the binding we put in place. Text="{Binding ElementName=slider, Path=Value}" means "I want this Text property to be automatically synchronized with slider's Value property.
  • The binding works both ways: when the slider's value changes, the text box will be updated, and conversely, when you change the text box's value, the slider will jump to the corresponding position. Note that when you try this, you will have to unfocus the text box, i.e., change the text box's value, then click on another control.

Apply the following change:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Text="Kelvin" Style="{StaticResource labelStyle}" />
-       <TextBox Grid.Row="1" Text="{Binding ElementName=slider, Path=Value}" Style="{StaticResource textBoxStyle}" />
+       <TextBox Grid.Row="1" Text="{Binding ElementName=slider, Path=Value, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource textBoxStyle}" />
    </Grid>

Run it to see what adding UpdateSourceTrigger has changed.

ValueConverters

Right now, there are two distinct ways we keep our controls synchronized:

  • A binding to sync the Kelvin text box with the slider bidirectionally.
  • C# code to sync the Celsius and Fahrenheit text boxes with the slider, but only unidirectionally.

It would be cleaner if we could handle all in the same way and bidirectionally, preferably making use of bindings due to their relative simplicity. The problem of course is that we cannot just bind the Celsius and Fahrenheit textboxes to the slider, as a conversion needs to happen in between. Fortunately, bindings let you specify such translations using value converters.

Let's bind the Celsius text box to the slider. We actually need to provide two translations:

  • When the user moves the slider, its value (temperature expressed in Kelvin) has to be converted into degrees Celsius.
  • When the user modifies the text box's contents, the opposite conversion needs to occur, i.e., from Celsius to Kelvin.

This is exactly what a value converter lets you do. Add the following code to your project. You can put in inside MainWindow.xaml.cs (below the definition of the MainWindow class), or you can create a new file; C# does not really care.

public class CelsiusConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var kelvin = (double)value;
        var celsius = kelvin - 273.15;

        return celsius.ToString();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var celsius = double.Parse((string) value);
        var kelvin = celsius + 273.15;

        return kelvin;
    }
}

The IValueConverter resides in the System.Windows.Data namespace, so you will need to add

using System.Windows.Data;

at the top of the file.

Take a good look at this new class. It implements IValueConverter, which is the interface you need to implement if you want a binding to recognize it. This interface specifies two methods: Convert and ConvertBack.

  • Convert will be called to convert the slider value (Kelvin) into a text box value (Celsius). Mind the types: the slider's value is a double, whereas the text box expects a string.
  • Conversely, ConvertBack is expected to convert the text box value (a string denoting a Celsius temperature) into a slider value (a double representing the same temperature expressed in Kelvin).

Snapshot: celsius-converter

Exercise

  • Remove the C# code that updates the Celsius text box when the slider changes.
  • In MainWindow.xaml, create a CelsiusConverter resource and name it celsiusConverter. In order to reference your own classes, you need to specify to which namespace it they belong. Notice xmlns:local="clr-namespace:View" on line 6: this introduces a shorthand way to refer to classes in the View namespace (to which your CelsiusConverter belongs). In other words, use local:CelsiusConverter to refer from within the XAML file to your CelsiusConverter class.
  • Bind the Celsius text box's Text property to the slider's Value property.
  • Have the binding update itself as soon as the user modifies the text box's contents.
  • Tell the binding to use celsiusConverter as a converter. Note that we need to refer to the object, not the class.

Snapshot: celsius-binding

Exercise

Have Fahrenheit work with bindings too. Clean up your code: remove the SliderValueChanged method.

Snapshot: fahrenheit-binding

We now implemented a functional application. However, design-wise, a number of improvements need to be made. For example,

  • The temperature scale conversion logic is hidden inside a CelsiusConverter and FahrentheitConverter class. Say we need the same logic elsewhere, it would be quite clumsy to reuse these classes: we would need to invent dummy parameter values (i.e., the targetType, parameter and culture parameters which we ignored). We would also need to work with strings instead of doubles, and type safety would be sacrificed due to the fact that everything is casted up to object.
  • The GUI should not "know" there are three temperature scales. It's as if you're writing a sorting function that assumes the list always contains exactly three items to be sorted. Just as we'd want a sorting function to be able to deal with an arbitrary number of items, adding new temperature scales should not require any changes to be made in the GUI.
  • There's still a lot of duplication going on in MainWindow.xaml. The set of controls associated with a temperature scale are all the same, save for some details.

We will fix these shortcomings in the next section.

Clone this wiki locally