Search code examples
c#wpfmvvmuser-controlsdependency-properties

Read-only Dependency Property updates but does not work on first use


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 first control should use the WingDings font, but does not


Solution

  • Diagnosis

    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).

    Solution

    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);
    }
    

    Better solution

    "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:

    1. The Label needs to have x:Name so it can be referenced in Setter.TargetName
    2. Trigger.Property value needs to be fully qualified, because ControlTemplate does not have TargetType set.