Skip to content

reusable view

Frederic Vogels edited this page Feb 24, 2018 · 1 revision

Reusable view components

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.

User Controls

To the view project, add a new "User Control" named TemperatureScaleControl. This creates two files:

  • TemperatureScaleControl.xaml
  • TemperatureScaleControl.xaml.cs

Exercise

  • From MainWindow.xaml, move the temperature scale related controls to TemperatureScaleControl.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.

Dependency Properties

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 Cells on steroids, as they offer much more functionality:

  • Like Cells, they are observable.

  • They can inherit their value from their parent. We relied on this when using DataContexts.

  • 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."

  • They can validate their value.

  • They can coerce their value into a valid range.

  • Attached dependency properties (like Grid.Row and Grid.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

Exercise

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