-
Notifications
You must be signed in to change notification settings - Fork 24
reusable view
The model layer is reusable: the temperature scale classes can be reused in any project that requires such functionality.
The view model layer is reusable: you can build different GUI's on top of it, make use of other libraries (e.g. WinForms, Universal App) or target other platforms (mobile development using Xamarin.)
The view layer itself is application specific: it knows what the application is, it knows the view model as it needs to bind to it, etc. However, the layer can consist of multiple components, and some of these can made in such a way as to be reusable elsewhere.
Examples of reusable view components abound: buttons, text boxes, list boxes, dialog boxes, ... All these can be parameterized: for example, buttons can be made to look however you want, and when you click them, you can have an arbitrary piece of code be executed.
In this section, we'll show you how to develop a reusable GUI control. Admittedly, given the small size of the project, there are not really any good candidates, but we'll force it a bit and choose to turn the scale related controls (the text block, text box and two buttons) into a separate component.
To the view project, add a new "User Control" named TemperatureScaleControl
. This creates two files:
TemperatureScaleControl.xaml
TemperatureScaleControl.xaml.cs
- From
MainWindow.xaml
, move the temperature scale related controls toTemperatureScaleControl.xaml
. This consists of everything between the<DataTemplate>...</DataTemplate>
tags. Replace it by<local:TemperatureScaleControl />
.- Also move all necessary styles.
Run your application. Everything should still work.
Snapshot: temperature-scale-control
Currently, the TemperatureScaleControl
receives its data from the DataContext
. This is not a particularly good solution as it is very implicit: nowhere does the TemperatureScaleControl
clearly declare what inputs in requires. Compare this to Button
, where you can look up the documentation or use Visual Studio's autocomplete and see it has properties such as Content
, Background
, Command
, and so on. In order to bring similar user-friendliness to our TemperatureScaleControl
, we'll need to explicitly declare which properties it expects.
Our TemperatureScaleControl
can be parameterized in many ways:
- Its header, which currently shows the name of the temperature scale.
- The header's background color, font, font size, etc.
- The value its text box shows.
- The text box's text alignment, font, font size, etc.
We could define properties for all these, but that would make this tutorial unnecessarily repetitive. Instead, we'll focus on the two most important aspects: the header's text and the text box's value.
If we were to look at how standard controls (Button
, TextBlock
, TextBox
, Slider
, ...) worked internally, we'd see that they don't rely on regular properties as we've been using now. Instead, their properties are dependency properties. These are not some new language feature, but merely classes defined by WPF. You can see them as Cell
s on steroids, as they offer much more functionality:
-
Like
Cell
s, they are observable. -
They can inherit their value from their parent. We relied on this when using
DataContext
s. -
Their value can be bound in multiple ways at once. For example,
- you could directly specify it in the element itself (
<TextBlock Background="Red" />
) - you could at the same time have a style setting the same property (
<Setter Property="Background" Value="Blue">
) - you could also add an animation that activates when the user hovers his mouse over the control
Dependency properties have a way of telling which of these values "wins."
- you could directly specify it in the element itself (
-
They can validate their value.
-
They can coerce their value into a valid range.
-
Attached dependency properties (like
Grid.Row
andGrid.Column
) can be added to any control.
Based on this, you'd think dependency properties are complex beasts. And you would be right about that. That is why we'll limit ourselves to the very basics, since this tutorial does not have the ambition to make you a WPF guru, but to improve your programming skills in general.
Let's simply start with the header. In TemperatureScaleControl.xaml.cs
, add a definition for the Header
dependency property. Note that Visual Studio offers a shortcut using code snippets: simply type propdp
and press TAB
. Make sure you fill in the right values where needed. The final code should look like this:
public partial class TemperatureScaleControl : UserControl
{
public TemperatureScaleControl()
{
InitializeComponent();
}
+ public string Header
+ {
+ get { return (string)GetValue(HeaderProperty); }
+ set { SetValue(HeaderProperty, value); }
+ }
+ public static readonly DependencyProperty HeaderProperty =
+ DependencyProperty.Register("Header", typeof(string), typeof(TemperatureScaleControl), new PropertyMetadata(""));
}
We need to tell the text box to get its text from this Header
dependency property. Go to TemperatureScaleControl.xaml
.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
- <TextBlock Grid.Row="0" Text="{Binding Name}" Style="{StaticResource labelStyle}" />
+ <TextBlock Grid.Row="0" Text="{Binding Path=Header, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" Style="{StaticResource labelStyle}" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="-" Style="{StaticResource buttonStyle}" Command="{Binding Decrement}" />
<TextBox Grid.Column="1"
Style="{StaticResource textBoxStyle}"
Text="{Binding Path=Temperature.Value, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="2" Content="+" Style="{StaticResource buttonStyle}" Command="{Binding Increment}" />
</Grid>
</Grid>
While we could have given the user control a name and used that in the binding (ElementName=name
), we made use of RelativeSource
which enables us to tell WPF to look up the tree for a control of type UserControl
.
Lastly, we need to go to MainWindow.xaml
to bind Header
to the DataContext
:
<StackPanel>
<ItemsControl ItemsSource="{Binding Scales}">
<ItemsControl.ItemTemplate>
<DataTemplate>
- <local:TemperatureScaleControl />
+ <local:TemperatureScaleControl Header="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Slider x:Name="slider" Value="{Binding Path=TemperatureInKelvin.Value}" Minimum="0" Maximum="1000" />
</StackPanel>
Build and run to check if everything works.
Snapshot: header
Create a second dependency property.
- Name it
Value
.- Its type is
double
.- Bind the text box to it.
- Update
MainWindow.xaml
.
Snapshot: value
If you run your application, you will notice that editing the text boxes has no effect on the other controls. This is due to the fact that if you define a new dependency property, the default mode of binding is one way, i.e., the binding will only read values from its target but not update it. In order to fix this, you can explicitly mention you want a two-way binding in MainWindow.xaml
:
{Binding ..., Mode=TwoWay}
You can also change the default mode of bindings on Value
:
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value",
typeof(double),
typeof(TemperatureScaleControl),
- new PropertyMetaData(0.0));
+ new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
Snapshot: default-binding-mode