-
Notifications
You must be signed in to change notification settings - Fork 24
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
.
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
ColumnDefinition
s 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.
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 aClick
event, aSlider
has aValueUpdated
event. Let it be handled by a method calledSliderValueChanged
.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.
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 thisText
property to be automatically synchronized withslider
'sValue
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.
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 adouble
, whereas the text box expects a string. - Conversely,
ConvertBack
is expected to convert the text box value (astring
denoting a Celsius temperature) into a slider value (adouble
representing the same temperature expressed in Kelvin).
Snapshot: celsius-converter
- Remove the C# code that updates the Celsius text box when the slider changes.
- In
MainWindow.xaml
, create aCelsiusConverter
resource and name itcelsiusConverter
. In order to reference your own classes, you need to specify to which namespace it they belong. Noticexmlns:local="clr-namespace:View"
on line 6: this introduces a shorthand way to refer to classes in theView
namespace (to which yourCelsiusConverter
belongs). In other words, uselocal:CelsiusConverter
to refer from within the XAML file to yourCelsiusConverter
class.- Bind the Celsius text box's
Text
property to the slider'sValue
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
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
andFahrentheitConverter
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., thetargetType
,parameter
andculture
parameters which we ignored). We would also need to work withstring
s instead ofdouble
s, and type safety would be sacrificed due to the fact that everything is casted up toobject
. - 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.