Introduction
Suppose you have a WPF User Control that is composed of other WPF controls some of which have Dependency Properties (DPs) that you'd like to initialize and data bind to. The main point is that these DPs are already defined in the sub-controls. This article explores various ways to make use of these and highlights the various limitations of each method.
Background
I actually started off writing another article exploring the different ways to implement a customized ComboBox
. One of these was a User Control. I'd been using DPs with the various approaches and ran into an issue with initializing a DP within a sub-control of a User Control from XAML. This required accessing the sub-control via the User Control's Content
property. This didn't appeal as it meant that anybody using the control would have to have intimate knowledge of its structure, which seemed to violate the rule of encapsulation. Therefore I thought I'd experiment with other approaches and see what the rest of CodeProject and Internet had to offer. The result is this article.
Using the code
The various approaches are shown by examining different bits of code. These all follow the same format of a basic WPF application containing MainWindow.xaml with a default code-behind page and a User Control called UserControl1
. The User Control is the same for each version, being a ComboBox
specialzied to allow the selection of a red, green, or blue brush. This is then bound to a label which displays the text 'Look at me' in the selected colour. Depending on the sample, the code-behind will be the default or contain additions. The accompanying zip is a VS2010 solution containing each version as a separate project. A screenshot of the application is shown at the top.
Firstly, let's look at a basic implementation of the User Control. This is a very simple customization which uses a ComboBox
to display a list of three colours: Red, Green, and Blue. The ComboBox
is the User Control's Content
, and the ComboBox
's template for displaying individual items is overridden for displaying instances of SolidColorBrush
as a 20x20 rectangle. The ComobBox
is then bound to an Array
of SolidColorBrush
es created as a resource within the User Control. There is absolutely nothing exciting about this. There is no additional code-behind other than that which is generated by default. In the solution, this is the Initial project.
<UserControl x:Class="Initial.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<x:Array x:Key="SomeBrushes" Type="{x:Type SolidColorBrush}">
<SolidColorBrush>Red</SolidColorBrush>
<SolidColorBrush>Green</SolidColorBrush>
<SolidColorBrush>Blue</SolidColorBrush>
</x:Array>
</UserControl.Resources>
<ComboBox Name="combo" ItemsSource="{StaticResource SomeBrushes}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="SolidColorBrush">
<Rectangle Width="20" Height="20" Fill="{Binding}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>
The next section of XAML to look at is that of the main window. This has an instance of the UserControl1
and a Label
to which the foreground colour is bound to the SelectedItem
DP of the ComboBox
embedded within the User Control.
<Window x:Class="Initial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Initial"
Title="MainWindow" Height="350" Width="525">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
</StackPanel>
</Window>
It is the last part of the previous sentence that is less than desirable. In order to enable the binding, the Label
has to know the control type embedded within the User Control. This violates the principle of encapsulation. In some ways, it would be good if WPF/XAML would allow the content of User Controls to be opaque or private, much like C++ and C# allowing a class author to declare implementation details private but provide a public interface; quite possibly with just Dependency Properties forming the majority of the public interface.
When the project is run, the ComoBox
is initialized in a blank state. This is because the SelectedIndex
property has not been set. Given that a binding could be established to the SelectedItem
property, it would appear that adding Content.SelectedIndex="0"
to src:UserControl
would work. However, this does not. The most likely reason is that whereas the bindings happen at run time and the specified path is a string which is used to find the underlying property via Reflection, when setting a property, this must be known at compile time. This is the other issue which led to looking at different ways to access Dependency Properties within User Controls.
Moving on to the first alternative implementation, which is the MakeDPInherit project. The main difference is additions to the code-behind file for UserControl1
, which is shown below:
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty SelectedIndexProperty;
static UserControl1()
{
SelectedIndexProperty = ComboBox.SelectedIndexProperty.AddOwner(
typeof(UserControl1), new FrameworkPropertyMetadata() { Inherits = true });
ComboBox.SelectedIndexProperty.OverrideMetadata(typeof(ComboBox),
new FrameworkPropertyMetadata() { Inherits = true });
}
public int SelectedIndex
{
get { return (int)GetValue(SelectedIndexProperty); }
set { SetValue(SelectedIndexProperty, value); }
}
public UserControl1()
{
InitializeComponent();
}
}
The change is to allow the SelectedIndex
property to be set. As this dependency property already exists on the underlying ComboBox
, the desire is to make this visible directly from the control. This is achieved by having the control register as an owner. Just doing this alone however will not result in the SelectedIndex
being set properly. This is because by default the SelectedIndex
property does not inherit the value set on the same DP at a higher level. To enable this as well as setting the Inherits
property of FrameworPropertyMetadata
for the registration at the UserControl
level, it was also necessary to override the existing metadata at the ComboBox
level; otherwise the setting would not propagate downwards. These changes means the following in MainWindow.xaml now work.
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center"
VerticalAlignment="Center" SelectedIndex="0"/>
The amount of code required to enable the use of SelectedIndex
combined with the need to modify the metadata of the underlying Dependency Property prompted a search for a simpler mechanism. This is contained in the Passthru project. This contains no further changes to MainWindow.xaml and UserControl1.xaml, but the code-behind for UserControl1
becomes:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public int SelectedIndex
{
get { return ((ComboBox)Content).SelectedIndex; }
set { ((ComboBox)Content).SelectedIndex = value; }
}
}
All this approach does is provide a publically accessible setter and getter for the underlying dependency property. This is essentially an application of the proxy pattern. This is significantly simpler and for a moment appears to be the perfect solution. However, whilst it solves the issues with setting SelectedIndex
and allows the UserControl
to be the target of a data binding, problems arise when it is the source of data binding.
In the examples shown so far, UserControl1
has not actually been the source of data binding. The line:
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
which establishes the binding of the selected colour to the foreground of the Label
is between the Dependency Property within the ComboBox
that is embedded within the UserControl
; i.e., exactly the situation that we're trying to avoid.
If an additional control is added to MainWindow.xaml, in this case a slider which creates a binding between its own current value and the SelectedIndex
property, things don't work too well. What happens is that any update made to the Slider
is reflected in the ComboBox
but despite the two-way binding, changes made to the ComboBox
are not reciprocated. This is because the SelectedIndex
accessor is not a proper Dependency Property so when the underlying Dependency Property is modified, these updates are not communicated to the binding as the binding is to a public property on UserControl1
which does not implement INotifyPropertyChanged
.
<Slider Height="23" Name="slider1" Width="100" Minimum="0" Maximum="2"
Value="{Binding ElementName=UC1, Path=SelectedIndex, Mode=TwoWay}"/>
A possible workaround might be to move the target of binding to UserControl1
, keeping it as two-way, e.g.:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center" VerticalAlignment="Center"
SelectedIndex="{Binding ElementName=slider1, Path=Value, Mode=TwoWay}"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
<Slider Height="23" Name="slider1" Width="100" Minimum="0" Maximum="2" Value="0"/>
</StackPanel>
Note how initialization is by setting Value="0"
on slider1
.
However, this causes a compilation error as bindings can only be established against Dependency (and Attached) properties whereas in this case, SelectedIndex
is just a standard property. Therefore, in order to support binding properly, there is no choice but to use an actual Dependency Property of some form. As such, the MakeDPInherit sample is worth revisiting.
Applying the change above to this example does not cause any compilations but nor does it work! The problem this time is that even with the inheritance property set in metadata, for instances of types in a hierarchy that implements the Dependency Property (via AddOwner
for all but types that actually register it), if the value is set lower down in the hierarchy, then this overrides any value set higher. In this case, when the selection is changed in the ComboBox
, this only changes the value of the Dependency Property instance of the ComboBox
and the value does not propagate upwards, to UserControl1
in this case. As the binding is made against the Dependency Property on UserControl1
, there is no change to report. Close, but not quite there!
Whilst researching this, the following CodeProject article was discovered. This is similar to the previous mechanism but rather than adding an additional owner for a Dependency Property, it created a new one but with the same name. The additional step was to create an internal binding between the new registered Dependency Property and the one on the sub-control. This technique is shown below. This is present in the EnableBinding sample project.
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty SelectedIndexProperty;
static UserControl1()
{
SelectedIndexProperty = DependencyProperty.RegisterAttached("SelectedIndex",
typeof(int), typeof(UserControl1),
new FrameworkPropertyMetadata() { BindsTwoWayByDefault = true });
}
public int SelectedIndex
{
get { return ((ComboBox)Content).SelectedIndex; }
set { ((ComboBox)Content).SelectedIndex = value; }
}
public UserControl1()
{
InitializeComponent();
Binding b = new Binding("SelectedIndex");
b.Source = this;
b.Mode = BindingMode.TwoWay;
combo.SetBinding(ComboBox.SelectedIndexProperty, b);
}
public IEnumerable ItemsSource
{
get { return ((ComboBox)Content).ItemsSource; }
set { ((ComboBox)Content).ItemsSource = value; }
}
}
Another change compared to the previous examples is the addition of the Passthru style implementation for the ItemsSource
Dependency Property. This is to show that this rather important property can be used in this fashion. As it stands, it can't be bound to, but it's fine for loading a static resource. As such, the Array
of SolidColorBrush
has been moved to MainWindow.xaml and ItemsSource
referenced here, as can be seen below:
<Window x:Class="EnableBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:EnableBinding"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<x:Array x:Key="SomeBrushes" Type="{x:Type SolidColorBrush}">
<SolidColorBrush>Red</SolidColorBrush>
<SolidColorBrush>Green</SolidColorBrush>
<SolidColorBrush>Blue</SolidColorBrush>
</x:Array>
</Window.Resources>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Top">
<src:UserControl1 x:Name="UC1" HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{StaticResource SomeBrushes}" SelectedIndex="0"/>
<Label Content="Look at me"
Foreground="{Binding ElementName=UC1,Path=Content.SelectedItem}"/>
<Slider Height="23" Name="slider1" Width="100" Minimum="0"
Maximum="2"
Value="{Binding ElementName=UC1, Path=SelectedIndex, Mode=TwoWay}"/>
</StackPanel>
</Window>
In order to establish the two-way binding between the Slider
and UserControl1
, it doesn't matter which control the binding is created on as both fields are Dependency Properties. In the project, both versions are provided.
Note that in the solution, there is an additional project that just shows the original example modified to use the synchronized Dependency Property mechanism. This is entitled MakeDPSync.
Points of interest
The use of a UserControl
to provide the custom control is probably quite wrong. Of all the mechanisms available from WPF, this is the most ill-suited to this particular application, and I wouldn't actually use it.
None of these mechanisms is anywhere near perfect as they all involve some sort of compromise, be it lack of functionality or the addition of what appears to be code duplicating the underlying Dependency Properties implementation. Additionally, if multiple Dependency Properties need supporting, then as the required code is closely tied to the actual names of the Dependency Properties, a generic solution is difficult to implement.
If a UserControl
must be used and Dependency Properties of sub-controls need modifying, then as this article shows, there are various options available. If a full binding support is required, then the only real option is to use the final approach, but if the requirements are only that the Dependency Properties be set (either at compile time or as binding target), then the mechanism shown in the Passthru sample will suffice.
History
- 9 July 2010 - First version.