只读依赖 属性 更新但在首次使用时不起作用

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

我正在尝试创建一些 WPF 用户控件以包含在库中以与我的团队共享,但是只读属性对我的工作方式有问题。

对于这个问题我做了一个非常简单的用户控件,有两个DependencyProperties。一个基于 enum,另一个基于选定的 enum 执行操作。 enum 用于选择按钮将使用的样式。

该应用程序是一个常规的 Wpf 应用程序,以 Wpf 用户控件库作为参考。我怀疑控件库 可能 导致了这个问题,所以我认为它与示例相关。

Wpf 控件库1

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 应用程序

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>

下面的输出显示第一个控件 而不是 显示 Style。如果我 运行 应用程序,使用 Live 编辑器将其切换到 Style2,然后再切换回 Style1,WingDings 字体会接管,但在新的 运行 上它不会。这看起来确实像是一个依赖性 属性 问题,但据我所知我的设置是正确的,尤其是因为其他两个控件似乎可以正常工作。

诊断

它不起作用的原因是因为您在 SelectedStyle 属性 更改的处理程序中分配了 ReadOnlyStyle 属性 值。由于 SelectedStyle 的类型 StyleSelector 是一个 enum,并且您没有明确为此 属性 分配默认值,它将具有默认值 default(StyleSelector) 由框架分配,恰好是 StyleSelector.Style1。即使您在第一个控件上将该值显式分配给此 属性,该值也不会真正改变,因此不会调用处理程序,因此 ReadOnlyStyle 仍然是 null,因此您得到你得到的(默认样式的Label)。

解决方案

为了解决这个问题,您应该分配 ReadOnlyStyle 的初始值。但是由于样式保存在资源字典中,您不能在构造函数中执行此操作。分配初始值的一个好点是:

protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
    var style = (Style)userControl1.FindResource("SampleStyle-Font1");
    SetValue(ReadOnlyStylePropertyKey, style);
}

更好的解决方案

"The WPF way" 实现目标的方法是使用触发器。所以首先你可以从你的控件中删除不必要的代码:

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

然后修改你的模板:

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

这里有两件重要的事情是:

  1. Label需要有x:Name才能在Setter.TargetName
  2. 中引用
  3. Trigger.Property 值需要完全限定,因为 ControlTemplate 没有设置 TargetType