Introduction
WPF is an extremely powerful UI development package. Part of the reason for its power is that WPF architects came up with completely new concepts that give software development a new dimension. These concepts are not used outside of WPF and many people switching to programming WPF from other languages and packages might have difficulty understanding them in the beginning.
The purpose of this series of articles is to teach these concepts and demonstrate their usage via very simple C#/WPF samples.
The topics discussed here might be somewhat disconnected from each other, so, perhaps you should try to read only one topic at a time.
First article in the series covered Dependency Properties (DPs), Attached Properties (APs) and Bindings: WPF Fundamental Concepts in Easy Samples Part 1: Dependency Properties, Attached Properties and Bindings.
In this installment I plan to cover
- MultiBindings
- ControlTemplates
- Styles
Other concepts:
- DataTemplates
- MVVM
- Visual Trees
- Routed and Attached Events
- Behaviors
will be discussed in future installments.
My purpose here is not to provide a full description of all the features of the described concepts, rather I'll concentrate on what I consider most important features that I myself use several times per day. The less important features can be learned by the developers on their own as they build the applications using WPF functionality.
This article is aimed at people who have some basic WPF knowledge but are looking to improve their WPF skills and practices. In particular I assume that people reading this article are familiar with XAML and to some degree have come across the above mentioned concepts, e.g. they might not have created any dependency properties themselves, but they are aware that such concept exists.
I also assume the readers read or at least looked through the first article in the series: WPF Fundamental Concepts in Easy Samples Part 1: Dependency Properties, Attached Properties and Bindings and have some understanding of Lookless or Custom WPF Controls explained in WPF Lookless Controls.
MultiBindings
Bindings and MultiBindings
As was explained in WPF Fundamental Concepts in Easy Samples Part 1: Dependency Properties, Attached Properties and Bindings, WPF Binding
creates a relationship between two properties (the source and the target property). The source property is taken with respect to the source object (it can be given by a complex path that specifies a property within a property etc... with the root of the path being the source object). The target property is always a dependency or attached property on the target object.
Most often, Binding
ensures that the target property value matches that of the source property value - they do not have to be equal, if, e.g. a value converter is used.
Two way bindings, maintain the relationship between source and target values whenever each one of them changes.
WPF MultiBinding
is very similar to the Binding
, but allows to use multiple sources for the same target, so that the target changes whenever each one of the source value changes. These multiple sources are combined into one value via a multi value converter (which implements IMultiValueConverter
interface).
Just like Binding
, the MultiBinding
can work both ways (in TwoWay
mode) and from target to multiple sources (in OneWayToSource
mode), but these two modes are rarely used because, usually one cannot uniquely recreate multiple sources from a single target.
Concatenation MultiBinding Sample
This sample is located within ConcatenationMultiBinding
project.
If you run the project you'll see 3 editable TextBox
es at the top row and one TextBlock
underneath. After you type some texts in all of these text boxes, the TextBox
will display the concatenation of these texts:
Here is the relevant XAML code:
<Grid VerticalAlignment="Center">
<Grid.Resources>
<local:ConcatenationMultiValueConverter x:Key="TheConcatenationMultiValueConverter"/>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox x:Name="Input1"
Grid.Column="0"
HorizontalAlignment="Stretch"
Margin="2,0"/>
<TextBox x:Name="Input2"
Grid.Column="1"
HorizontalAlignment="Stretch"
Margin="2,0" />
<TextBox x:Name="Input3"
Grid.Column="2"
HorizontalAlignment="Stretch"
Margin="2,0" />
</Grid>
<TextBlock Grid.Row="1"
HorizontalAlignment="Stretch"
Background="White"
Margin="2"
Height="23">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TheConcatenationMultiValueConverter}">
<Binding Path="Text"
ElementName="Input1"/>
<Binding Path="Text"
ElementName="Input2" />
<Binding Path="Text"
ElementName="Input3" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
The part that interests us most is the part containing the MultiBinding
:
<TextBlock Grid.Row="1"
HorizontalAlignment="Stretch"
Background="White"
Margin="2"
Height="23">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TheConcatenationMultiValueConverter}">
<Binding Path="Text"
ElementName="Input1"/>
<Binding Path="Text"
ElementName="Input2" />
<Binding Path="Text"
ElementName="Input3" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The Text
dependency property of the TextBlock
is multi-bound to the Text
properties of three TextBox
es named "Input1", "Input2" and "Input3". The multi value converter provided by the resource "TheConcatenationMultiValueConverter" combines those texts into the output value by concatenating them.
Let us take a look at ConcatenationMultiValueConverter
class:
public class ConcatenationMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string result = "";
foreach(object val in values)
{
if (val == null)
continue;
result += val.ToString();
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can see that IMultiValueConverter
interface is very similar to IValueConverter
interface used to convert the source value to the target value by the usual (single value) Binding
, explained in WPF Fundamental Concepts in Easy Samples Part 1: Dependency Properties, Attached Properties and Bindings. The only difference is that its Convert(...)
method accepts an array of values instead of a single value as the first parameter and also its ConvertBack(...)
method returns an array of objects instead of a single object.
Resolving the Problem of Changing Format With MultiBinding
The tremendous power of MultiBinding
is also highlighted by an example below, showing how, with its help, one can imitate the binding for a property that is neither dependency nor attached property.
Assume that we want to display some values (date or time or price or integer) using various formats. It would be reasonable to use a WPF Binding
to display the value and to use the Binding
's StringFormat
property to set the format.
Unfortunately the Binding
is not a dependency object and correspondingly its StringFormat
property is not a dependency property and therefore cannot be a target of another Binding
, so once the Binding
's StringFormat
is set, there is no easy and WPF friendly way to change it. This is where MultiBinding
comes to the rescue.
Running FormatMultiBindingSample
project will produce a window with a TextBox
for entering format. Once you enter a valid C# format e.g. "{0:yyyy/MM/dd}" as shown on the picture below, the January 25th, 2015 date will appear in that format:
Changing the format string will result in changed date representation (as long as the format is valid, of course). E.g. using "{0:MMMM/dd, yyyy}", will result in "January/25, 2015" displayed.
Here is the relevant XAML code:
<Window.Resources>
<sys:DateTime x:Key="TheDate">2015-01-25</sys:DateTime>
<local:FormatMultiValueConverter x:Key="TheFormatMultiValueConverter"/>
</Window.Resources>
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock Text="Enter Date Format: " />
<TextBox x:Name="TheFormatTextBox" Width="120"/>
</StackPanel>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="1"
Margin="0,5">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TheFormatMultiValueConverter}">
<Binding Source="{StaticResource TheDate}" />
<Binding Path="Text"
ElementName="TheFormatTextBox" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
The date we want to format is defined as a resource.
The MultiBinding
binds to the date as the first parameter and to the format (which is editable) as the second. FormatMultiValueConverter
is employed to produce the target value. Here is the converter's code:
public class FormatMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 0)
return null;
object val = values[0];
if (values.Length == 1)
return val;
string format = values[1] as string;
if (string.IsNullOrEmpty(format))
return val;
try
{
return string.Format(format, val);
}
catch
{
return val;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
As explained in the code's comments, the converter interprets the first parameter as the object to format, and the second parameter as the formatting string and uses string.Format(format, val)
method to produce the target value.
Because of the MultiBinding
, the target value will be updated if either the source object value or the format value changes.
Note that the MultiBinding
converter is more generic than the sample and can be used to format any objects, not only the dates.
ControlTemplates
Discussion about ControlTemplates, Styles and DataTemplates
There are several concepts that come with WPF that can and should be employed for XAML code reuse. The most prominent among them are
ControlTemplate
s - used for creating a visual representation for WPF lookless controls.
Style
s - used for specifying the customization properties for WPF elements and controls.
DataTemplate
s - used for providing visual representation for non-visual, non-WPF objects (including the famous View Models).
All three of the constructs have ability to define triggers that can change dependency and attached properties on objects defined within the constructs based on some condition being fulfilled and then revert the property back once the condition is no longer valid.
I already described the working of the ControlTemplate
s in WPF Lookless Controls article, so here I will only provide a refresher sample and show how to use property triggers in it.
Simple ControlTemplate Sample with Property Triggers
Here is what you see if you run SimpleControlTemplateWithTriggers
project, click the check box on the left side and place the mouse over the check box:
If you move the mouse not to be on top of the check box, the text's background will change to white, and if you uncheck the check box, the text will not be visible any more.
This is a visual representation of a very simple lookless control SimpleLooklessControl
that defines only one property (dependency property) - TheText
:
public class SimpleLooklessControl : Control
{
#region TheText Dependency Property
public string TheText
{
get { return (string)GetValue(TheTextProperty); }
set { SetValue(TheTextProperty, value); }
}
public static readonly DependencyProperty TheTextProperty =
DependencyProperty.Register
(
"TheText",
typeof(string),
typeof(SimpleLooklessControl),
new PropertyMetadata(null)
);
#endregion TheText Dependency Property
}
Here is the relevant XAML code:
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="TheBooleanToVisibilityConverter"/>
<ControlTemplate TargetType="local:SimpleLooklessControl"
x:Key="TheSimpleLooklessControlTemplate">
<Border BorderBrush="Black"
BorderThickness="1"
Background="{TemplateBinding Background}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<CheckBox x:Name="TheVisibilityCheckBox"
Margin="2,0,10,0"
VerticalAlignment="Center" />
<TextBlock x:Name="TheText"
Text="{TemplateBinding TheText}"
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Visibility="{Binding Path=IsChecked,
ElementName=TheVisibilityCheckBox,
Converter={StaticResource TheBooleanToVisibilityConverter}}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
-->
<Trigger SourceName="TheVisibilityCheckBox"
Property="IsMouseOver"
Value="True">
<Setter TargetName="TheText"
Property="Background"
Value="Yellow" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
<local:SimpleLooklessControl Width="200"
Height="25"
TheText="Hello World"
Template="{StaticResource TheSimpleLooklessControlTemplate}"/>
</Grid>
Note that we use {TemplateBinding TheText}
to bind the Text
property of the TextBlock
within the template to the dependency property TheText
of the lookless control which uses the template to visualize itself. As was explained in WPF Lookless Controls, TemplateBinding
is just a shorthand for the usual WPF Binding
used with RelativeSource
mode set to TemplatedParent
, so the same binding could have been rewritten as {Binding Path=TheText, RelativeSource={RelativeSource TemplatedParent}}
Let us take a closer look at the trigger code:
<Trigger SourceName="TheVisibilityCheckBox"
Property="IsMouseOver"
Value="True">
<Setter TargetName="TheText"
Property="Background"
Value="Yellow" />
</Trigger>
The trigger condition is specified by the attributes of the Trigger
tag: SourceName
attribute specifies the name of the element whose property the trigger observes (in our case it is our checkbox). Property
attribute specifies the name of the observed dependency or attached property (in our case it is IsMouseOver
dependency property defined on any WPF element). Value
specifies the observed property's value under which the trigger fires its setter(s). So, in our case, the trigger condition reads - fire setters when IsMouseOver
property is true on the element called "TheVisibilityCheckBox".
The setter attributes mirror those of the condition. TargetName
- is the name of the affected element within the ControlTemplate
. Property
is the name of the changed property. Value
attribute specifies the new value for the property that the setter sets it to. In our case the setter reads - change the Background
property on the element named "TheText" to "Yellow" color.
The SourceName
for the trigger condition does not have to be specified. In that case the trigger will be looking for a property defined on the whole control. Analogously, the TargetName
does not have to be specified on the setter. In such case, the setter will change the corresponding property on the whole control also.
Even though the sample uses a single trigger with a single setter, the ControlTemplate
might have multiple triggers and each trigger can have multiple setters.
Note, that instead of the binding with converter, we could have also used the property trigger to control the visibility of the TextBlock
when the button is checked via the following trigger:
<Trigger SourceName="TheVisibilityCheckBox"
Property="IsChecked"
Value="True">
<Setter TargetName="TheText"
Property="Visibility"
Value="Visible" />
</Trigger>
In that case, the default Visibility
of the TextBlock
should have been set to Collapsed
.
Many times the triggers can be used instead of the Binding
s with custom converters (which allows to avoid creating a custom converter C# class).
Other Types of Triggers
There are other, more complex triggers that can be used in ControlTemplates
, Styles
and DataTemplates
. They are:
- DataTriggers
- MultiTrigger
- MultiDataTriggers
DataTriggers allow to specify more complex conditions based on the bindings to properties. They are rarely used in ControlTemplates
and more often used in DataTemplates
. We will talk about them in the next installment that is going to discuss the DataTemplate
s.
MultiTrigger and MultiDataTrigger are used rarely (in my view) and I am not going to include the relevant samples into this series of articles. In general, they allow to specify multiple conditions instead of a single condition, so that the trigger fires only when all of those multiple conditions are satisfied.
WPF Styles
Introduction to Styles
Styles is another very important concept used throughout WPF in order to reduce XAML complexity and increase re-use of XAML parts. Styles can be considered as dictionaries of dependency (and attached) property values. They can be reused on multiple WPF controls and elements.
WPF Styles allow single inheritance, under which the sub-style inherits all the properties from the superstyle (but can override them if chooses).
Simple Style Sample with Inheritance
Assume that a lot of text blocks in your application will have Segoe UI
font. You create a style that only defines that font:
<Style x:Key="BaseTextStyle"
TargetType="TextBlock">
<Setter Property="FontFamily"
Value="Segoe UI"/>
</Style>
Then you derive the rest of your styles from it. e.g. the HeaderStyle
for header and PlainTextStyle
for plain text:
<Style x:Key="HeaderStyle"
TargetType="TextBlock"
BasedOn="{StaticResource BaseTextStyle}">
<Setter Property="FontSize"
Value="20"/>
<Setter Property="FontWeight"
Value="Bold"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
</Style>
<Style x:Key="PlainTextStyle"
TargetType="TextBlock"
BasedOn="{StaticResource BaseTextStyle}">
<Setter Property="FontSize"
Value="12" />
<Setter Property="FontWeight"
Value="Normal" />
<Setter Property="HorizontalAlignment"
Value="Left" />
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
Value="Yellow"/>
</Trigger>
</Style.Triggers>
</Style>
The styles are derived by setting their BasedOn
property to point to the super-style.
You can set the TextBlock
s in you code to use the styles by utilizing their Style
property:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="This is a Header"
Style="{StaticResource HeaderStyle}"/>
<TextBlock Text="This is a plain text"
Style="{StaticResource PlainTextStyle}"
Grid.Row="1">/>
SimpleStyleWithInheritanceSample
project shows how to create a Style inheritance hierarchy. Here is what you see when you run the project:
If you place the mouse on top of the plain text, its background will change to yellow. This is achieved with the help of the Style trigger:
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
Value="Yellow"/>
</Trigger>
</Style.Triggers>
Note, that the style triggers do not have SourceName
property and the style trigger setters do not have TargetName
property - style property triggers can only change the properties on the element on which the style is defined.
If you know that most of your text is plain (not a header), you can turn "PlainTextStyle" into the default style by removing the Key
from it. In such case you do not need to set Style
property on the TextBlock
if you want it to have a default style:
...
<Window.Resources>
...
<Style TargetType="TextBlock"
BasedOn="{StaticResource BaseTextStyle}">
<Setter Property="FontSize"
Value="12" />
...
</Style>
<Window.Resources/>
...
<TextBlock Text="This is a plain text"
Grid.Row="1">/>
Defining Styles in a Separate Resource Dictionary
In majority of cases, it is better to define your styles in a separate resource dictionary so that they could be visible in many different XAML files. The resource dictionary can be specified in the same project or in a different project.
Creating and using the styles defined in separate resource dictionaries has been described in WPF Lookless Controls and I refer the readers to that article for a refresher.
Summary
This article continues discussion of the basic WPF concepts that might be helpful to every WPF developer. In the next article in this series I plan to discuss the Data Templates and the MVVM pattern.