I am trying to create a few WPF UserControls to include in a library to share with my team but there is something wrong with how Read-Only properties are working for me.
For this question I made a very simple user control with two DependencyProperties. One that is based on an enum
and the other that performs an action based on the selected enum
. The enum
is being used to choose a style the button will use.
The application is a regular Wpf Application with a Wpf User Control Library as a reference. I have a suspicion that the Control Library might be contributing to the problem so I felt it was relevant to the example.
Wpf Control Library1
Dictionary1.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfControlLibrary1">
<Style x:Key="SampleStyle-Font1">
<Setter Property="TextElement.FontFamily" Value="Wingdings" />
<Setter Property="TextElement.FontSize" Value="30" />
</Style>
<Style x:Key="SampleStyle-Font2">
<Setter Property="TextElement.FontFamily" Value="Elephant" />
<Setter Property="TextElement.FontSize" Value="30" />
</Style>
<Style x:Key="SampleStyle-Font3">
<Setter Property="TextElement.FontFamily" Value="Times New Roman" />
<Setter Property="TextElement.FontSize" Value="30" />
</Style>
</ResourceDictionary>
UserControl1.xaml:
<UserControl x:Class="WpfControlLibrary1.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"
xmlns:local="clr-namespace:WpfControlLibrary1"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="200">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.Template>
<ControlTemplate>
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserControl1}}, Path=Command}">
<StackPanel>
<Label Style="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserControl1}}, Path=ReadOnlyStyle}" Content="{Binding Path=Content, RelativeSource={x:Static RelativeSource.TemplatedParent}}"></Label>
</StackPanel>
</Button>
</ControlTemplate>
</UserControl.Template>
</UserControl>
UserControl1.xaml.cs
namespace WpfControlLibrary1 {
using System.Windows;
using System.Windows.Controls;
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl {
public enum StyleSelector {
Style1,
Style2,
Style3
}
public static DependencyProperty SelectedStyleProperty =
DependencyProperty.Register("SelectedStyle", typeof(StyleSelector), typeof(UserControl1), new PropertyMetadata(ReadOnlyStyle_Changed));
private static readonly DependencyPropertyKey ReadOnlyStylePropertyKey =
DependencyProperty.RegisterReadOnly("ReadOnlyStyle", typeof(Style),
typeof(UserControl1), null);
public UserControl1() {
InitializeComponent();
}
public StyleSelector SelectedStyle {
get => (StyleSelector)GetValue(SelectedStyleProperty);
set => SetValue(SelectedStyleProperty, value);
}
public Style ReadOnlyStyle => (Style)GetValue(ReadOnlyStylePropertyKey.DependencyProperty);
private static void ReadOnlyStyle_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) {
if (!(d is UserControl1 userControl1)) {
return;
}
Style style;
switch (userControl1.SelectedStyle) {
case StyleSelector.Style1:
style = (Style)userControl1.FindResource("SampleStyle-Font1");
break;
case StyleSelector.Style2:
style = (Style)userControl1.FindResource("SampleStyle-Font2");
break;
case StyleSelector.Style3:
style = (Style)userControl1.FindResource("SampleStyle-Font3");
break;
default:
style = (Style)userControl1.FindResource("SampleStyle-Font1");
break;
}
userControl1.SetValue(ReadOnlyStylePropertyKey, style);
}
}
}
Wpf Application
MainWindow.xaml:
<Window x:Class="ReadOnlyDependencyPropertiesWithUserControls.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ReadOnlyDependencyPropertiesWithUserControls"
xmlns:wpfControlLibrary1="clr-namespace:WpfControlLibrary1;assembly=WpfControlLibrary1"
mc:Ignorable="d"
Title="Example" Height="200" Width="400">
<StackPanel>
<wpfControlLibrary1:UserControl1 SelectedStyle="Style1" Content="This is the first control"></wpfControlLibrary1:UserControl1>
<wpfControlLibrary1:UserControl1 SelectedStyle="Style2" Content="This is the second control"></wpfControlLibrary1:UserControl1>
<wpfControlLibrary1:UserControl1 SelectedStyle="Style3" Content="This is the third control"></wpfControlLibrary1:UserControl1>
</StackPanel>
</Window>
The output below shows that the first control does not show a Style
. If I run the application, switch it to Style2 using the Live editor and then back to Style1, the WingDings font does take over, but on a fresh run it does not. It definitely seems like a Dependency Property issue, but as far as I can tell I have the setup correct, especially since the other two controls seem to work.
The reason why it does not work is because you assign the ReadOnlyStyle
property value inside of SelectedStyle
property changed handler. Since SelectedStyle
is of type StyleSelector
which is an enum
, and you don't explicitly assign default value for this property, it will have default value of default(StyleSelector)
assigned by the framework, which happens to be StyleSelector.Style1
. And even if you explicitly assign that value to this property on your first control, the value doesn't really change, ergo the handler is not invoked, ergo ReadOnlyStyle
remains null
, ergo you get what you get (a Label
with default style).
In order to remedy that, you should assign initial value of ReadOnlyStyle
. But since the styles are kept in a resource dictionary, you cannot do that in the constructor. A good point to assign the initial value would be this:
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
var style = (Style)userControl1.FindResource("SampleStyle-Font1");
SetValue(ReadOnlyStylePropertyKey, style);
}
"The WPF way" of achieving your goal would be to use triggers. So first of all you could remove unnecessary code from your control:
public partial class UserControl1 : UserControl
{
public enum StyleSelector
{
Style1,
Style2,
Style3
}
public static DependencyProperty SelectedStyleProperty =
DependencyProperty.Register("SelectedStyle", typeof(StyleSelector), typeof(UserControl1));
public UserControl1()
{
InitializeComponent();
}
public StyleSelector SelectedStyle
{
get => (StyleSelector)GetValue(SelectedStyleProperty);
set => SetValue(SelectedStyleProperty, value);
}
}
Then modify your temlpate:
<ControlTemplate>
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:UserControl1}}, Path=Command}">
<StackPanel>
<Label x:Name="PART_Label" Content="{Binding Path=Content, RelativeSource={x:Static RelativeSource.TemplatedParent}}" />
</StackPanel>
</Button>
<ControlTemplate.Triggers>
<Trigger Property="local:UserControl1.SelectedStyle" Value="Style1">
<Setter TargetName="PART_Label" Property="Style" Value="{StaticResource SampleStyle-Font1}" />
</Trigger>
<Trigger Property="local:UserControl1.SelectedStyle" Value="Style2">
<Setter TargetName="PART_Label" Property="Style" Value="{StaticResource SampleStyle-Font2}" />
</Trigger>
<Trigger Property="local:UserControl1.SelectedStyle" Value="Style3">
<Setter TargetName="PART_Label" Property="Style" Value="{StaticResource SampleStyle-Font3}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Two important things here are:
Label
needs to have x:Name
so it can be referenced in Setter.TargetName
Trigger.Property
value needs to be fully qualified, because ControlTemplate
does not have TargetType
set.