【问题标题】:WPF DependencyProperty... now it works... now it doesn't... how come?WPF DependencyProperty...现在可以工作...现在不能...怎么会?
【发布时间】:2011-01-29 01:29:50
【问题描述】:

我有一个 ClockFace UserControl,它公开了许多属性以使用户能够对其进行样式设置。时钟有两个 Ellipse 对象作为边框;外边框和内边框。

<Ellipse Name="OuterBorder" Panel.ZIndex="5" StrokeThickness="{Binding BorderOuterThickness}" Stroke="{Binding BorderOuteBrush}" />
<Ellipse Name="InnerBorder" Panel.ZIndex="6" StrokeThickness="{Binding BorderInnerThickness}" Margin="{Binding StrokeThickness, ElementName=OuterBorder}" Stroke="{Binding BorderInnerBrush}">

public static readonly DependencyProperty BorderInnerBrushProperty = DependencyProperty.Register("BorderInnerBrush", typeof(Brush), typeof(ClockFace), new 

PropertyMetadata(new LinearGradientBrush(Color.FromRgb(118, 57, 57), Color.FromRgb(226, 185, 185), new Point(0.5, 0), new Point(0.5, 1))));

public Brush BorderInnerBrush
{
    get { return (Brush)GetValue(BorderInnerBrushProperty); }
    set { SetValue(BorderInnerBrushProperty, value); }
}

public static readonly DependencyProperty BorderOuterBrushProperty = DependencyProperty.Register("BorderOuterBrush", typeof(Brush), typeof(ClockFace), new 

PropertyMetadata(new LinearGradientBrush(Color.FromRgb(226, 185, 185), Color.FromRgb(118, 57, 57), new Point(0.5, 0), new Point(0.5, 1))));

public Brush BorderOuterBrush
{
    get { return (Brush)GetValue(BorderOuterBrushProperty); }
    set { SetValue(BorderOuterBrushProperty, value); }
}

这些可以在样式中设置,并在样式切换时正确更新。我认为我会很聪明,并添加一个名为 BorderBrush 的快捷方式属性,该属性将自身传递给 BorderOuterBrush 属性,然后将其自身的副本传递给其渐变反转到 BorderInnerBrush 属性。为了在初始化后(通过切换样式)设置属性时启用此代码,我必须实现一个调用 SetBorderBrushes 方法的 PropertyChangedCallback 方法。

public new static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register("BorderBrush", typeof(Brush), typeof(ClockFace), new 

PropertyMetadata(OnBorderBrushChanged));

private static void OnBorderBrushChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
    ((ClockFace)dependencyObject).SetBorderBrushes((Brush)e.NewValue);
}

public new Brush BorderBrush
{
    get { return (Brush)GetValue(BorderBrushProperty); }
    set { SetValue(BorderBrushProperty, value); }
}

private void SetBorderBrushes(Brush brush)
{
    if (brush != null)
    {
        BorderOuterBrush = brush;
        Brush innerBrush = BorderOuterBrush.Clone();
        if (brush.GetType() == typeof(LinearGradientBrush) || brush.GetType() == typeof(RadialGradientBrush))
        {
            foreach (GradientStop gradientStop in ((GradientBrush)innerBrush).GradientStops)
            {
                gradientStop.Offset = 1 - gradientStop.Offset;
            }
        }
        BorderInnerBrush = innerBrush;
    }
}

当 BorderBrush 属性设置为样式时,一切正常……直到我在运行时切换样式。现在最奇怪的事情发生了……让我解释一下。

我有四种预设样式,它们各自都可以正常工作。其中三个使用两个 BorderInnerBrush 和 BorderOuterBrush 属性,一个使用快捷方式 BorderBrush 属性。我可以使用 ContextMenu 和后面的一些代码在样式之间切换,这些代码从 Resources 访问 xaml 样式并将它们设置为 ClockFace 对象的 Style 属性。

我可以在不使用快捷方式属性的三种样式之间无休止地切换而不会出现问题。我也可以切换到使用快捷方式属性的样式,它看起来很好。这就是奇怪的开始。

切换到使用 BorderBrush 属性的样式后,BorderInnerBrush 和 BorderOuterBrush 属性将停止工作。以各种样式设置的预设 Brush 对象不再设置在两个 Ellipse 对象上。我在内部和外部边框属性中插入了一些 PropertyChangedCallback 方法,看看发生了什么。

当我第一次运行应用程序时,我可以毫无问题地切换到不使用快捷方式属性的三种样式。我在所有三个边框画笔属性的 PropertyChangedCallback 方法上都设置了断点并调试了程序。当切换到这三种样式中的每一种时,内边框属性和外边框属性的回调方法的断点都如您所愿。当切换到使用 BorderBrush 属性的样式时,它的回调方法的断点被命中。 SetBorderBrushes 方法设置了另外两个边框 Brush 对象,因此内边框属性和外边框属性的回调方法的断点会如您所愿地命中。

再次,这是奇怪的部分。在此之后切换到其他三种样式中的任何一种时,内边框属性和外边框属性的回调方法的断点都不再被命中。相反,附加到 BorderBrush 属性的回调方法中的断点被击中,并且 e.NewValue 的值为 null。由于在 SetBorderBrushes 方法中忽略 null 值,因此不会再命中断点。

经过进一步调查,我发现 e.NewValue 为空,因为 BorderBrushProperty DependencyProperty 上没有设置默认值。事实上,在声明中添加一个默认的 Brush 对象后,这就是将在回调方法中的 e.NewValue 中传递的 Brush。虽然这两个内边框属性和外边框属性的回调方法中的断点在这之后会被击中,但这只是因为它们是在SetBorderBrushes方法中设置的。在使用一次 BorderBrush 属性后,样式中的 BorderInnerBrush 和 BorderOuterBrush 对象中设置的 Brush 对象永远不会传递给这些属性。

最后要注意的一点是,正如在样式中没有显式设置值时设置 BorderBrushProperty 属性的默认值一样,内边框和外边框 DependencyProperty 对象的默认值也会在它们的值被设置时设置未在样式中显式设置,但仅当未在样式中设置 BorderBrush 属性时。

我已经被这个问题困扰了好几天,虽然一个简单的解决方案是删除快捷方式属性,但我宁愿找出发生了什么并修复它。我希望我已经提供了足够的信息,让一些明亮的火花能够解决这个问题,所以,如果你有任何想法或问题,请分享。非常感谢。

更新>>

按照 Rick 的建议,我创建了另一对 Brush 属性,并通过附加到其他三个 Brush 属性的回调方法设置它们。使用此设置,我可以在所有样式之间切换而不会出现任何视觉问题……或者我是这么认为的。

我现在可以从使用 BorderBrush(快捷方式)属性的样式切换到设置两个 BorderInner 和 BorderOuter 属性并且边框正确更新的样式,所以感谢 Rick 让我更进一步。从设置 BorderBrush 属性的样式切换到未显式设置任何边框 Brush 的样式时,我仍然遇到问题。

因此,我仍然有同样的问题,即 BorderInnerBrush 和 BorderOuterBrush 属性在从后面的代码设置后不能直接工作。新的是,如果我然后切换到设置两个 Brush 属性的样式,这似乎再次“唤醒它们”......如果我然后切换到不设置边框 Brush 的样式,则内部和外部属性设置它们默认值再次正确。

只有在使用从后面的代码中设置两个边框属性的样式之后,它们才会被设置为样式或使用它们的默认值。这太奇怪了……有人能解决吗?

更新 2 >>>

首先,非常感谢您抽出宝贵时间和示例 Rick。在将 Rick 的示例代码复制并粘贴到一个新项目中之后,我必须在它运行之前进行一些更改......也许是因为我没有 Expression Blend 4?我安装了 SDK 并添加了对上述 dll 的引用,但必须改用以下 XML 命名空间声明:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"

我还必须将每个 ChangePropertyAction 声明稍微更改为:

<ei:ChangePropertyAction TargetName="clock" PropertyName="Style" Value="{StaticResource styleBrush}"/>

然后,它对我来说工作得很好,并且很好地代表了我实际 ClockFace 控件的那一部分,但有两个例外。首先是我的样式是通过事件处理程序从代码后面切换的——这显然不会导致我的问题。第二个区别是我在我的 ActualOuterBrush 和 ActualInnerBrush 版本上设置了默认的 Brush 值,因此控件的用户不必提供边框画笔。

所以我的最后一个问题是,在 Style 中设置 BorderBrush 属性后,没有明确设置边框属性的样式中没有设置默认的内边框和外边框画笔。请记住,在此之前确实设置了默认值。因此,我尝试了 Rick 的示例并添加了一些默认值:

private static readonly DependencyPropertyKey ActualInnerBrushPropertyKey = DependencyProperty.RegisterReadOnly("ActualInnerBrush", typeof(Brush), typeof(Clock), new UIPropertyMetadata(Brushes.Teal));

private static readonly DependencyPropertyKey ActualOuterBrushPropertyKey = DependencyProperty.RegisterReadOnly("ActualOuterBrush", typeof(Brush), typeof(Clock), new UIPropertyMetadata(Brushes.Salmon));

现在这个项目有一个类似的问题......默认值永远不会重置。我对这种行为感到很困惑。

我在这个问题上花了很长时间,虽然我从 Rick 那里学到了很多有用的东西,但我仍然对默认值有这个问题。我决定最简单的做法是删除快捷方式 BorderBrush 属性。 Rick 帮了大忙,我将他的答案评为正确答案,但如果有人阅读本文可以帮助了解默认值,我将不胜感激。

【问题讨论】:

    标签: wpf callback dependency-properties


    【解决方案1】:

    设置属性后,将不再应用样式。通过自己在更改的处理程序中设置内部和外部画笔属性,依赖属性子系统不会意识到更改实际上是由于样式而不是您显式进行的。

    一种解决方案是公开受保护的只读ActualInnerBorderBrushActualOuterBorderBrush 属性,然后让所有三个用户可定义属性在各自的更改处理程序中设置这些实际值。这样,用户可见的属性始终可以“由用户设置”,而不会相互干扰。

    编辑:

    这是五个属性的完整工作实现:

    public class Clock : StackPanel
    {
        public Brush Brush
        {
            get { return (Brush)GetValue(BrushProperty); }
            set { SetValue(BrushProperty, value); }
        }
    
        public static readonly DependencyProperty BrushProperty =
            DependencyProperty.Register("Brush", typeof(Brush), typeof(Clock),
            new UIPropertyMetadata((d, e) => (d as Clock).OnBrushChanged(d, e)));
    
        public void OnBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ActualInnerBrush = e.NewValue as Brush;
            ActualOuterBrush = e.NewValue as Brush;
        }
    
        public Brush InnerBrush
        {
            get { return (Brush)GetValue(InnerBrushProperty); }
            set { SetValue(InnerBrushProperty, value); }
        }
    
        public static readonly DependencyProperty InnerBrushProperty =
            DependencyProperty.Register("InnerBrush", typeof(Brush), typeof(Clock),
            new UIPropertyMetadata((d, e) => (d as Clock).OnInnerBrushChanged(d, e)));
    
        public void OnInnerBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ActualInnerBrush = e.NewValue as Brush;
        }
    
        public Brush OuterBrush
        {
            get { return (Brush)GetValue(OuterBrushProperty); }
            set { SetValue(OuterBrushProperty, value); }
        }
    
        public static readonly DependencyProperty OuterBrushProperty =
            DependencyProperty.Register("OuterBrush", typeof(Brush), typeof(Clock),
            new UIPropertyMetadata((d, e) => (d as Clock).OnOuterBrushChanged(d, e)));
    
        public void OnOuterBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ActualOuterBrush = e.NewValue as Brush;
        }
    
        public Brush ActualInnerBrush
        {
            get { return (Brush)GetValue(ActualInnerBrushProperty); }
            private set { SetValue(ActualInnerBrushPropertyKey, value); }
        }
    
        private static readonly DependencyPropertyKey ActualInnerBrushPropertyKey =
            DependencyProperty.RegisterReadOnly("ActualInnerBrush", typeof(Brush), typeof(Clock), new UIPropertyMetadata());
        public static readonly DependencyProperty ActualInnerBrushProperty = ActualInnerBrushPropertyKey.DependencyProperty;
    
    
        public Brush ActualOuterBrush
        {
            get { return (Brush)GetValue(ActualOuterBrushProperty); }
            private set { SetValue(ActualOuterBrushPropertyKey, value); }
        }
    
        private static readonly DependencyPropertyKey ActualOuterBrushPropertyKey =
            DependencyProperty.RegisterReadOnly("ActualOuterBrush", typeof(Brush), typeof(Clock), new UIPropertyMetadata());
        public static readonly DependencyProperty ActualOuterBrushProperty = ActualOuterBrushPropertyKey.DependencyProperty;
    }
    

    这里有一个小测试程序来证明它有效:

    <Grid>
        <Grid.Resources>
            <Style x:Key="styleBrush" TargetType="local:Clock">
                <Setter Property="Brush" Value="Red"/>
            </Style>
            <Style x:Key="styleInnerOnly" TargetType="local:Clock">
                <Setter Property="InnerBrush" Value="Green"/>
            </Style>
            <Style x:Key="styleInnerOuter" TargetType="local:Clock">
                <Setter Property="InnerBrush" Value="Blue"/>
                <Setter Property="OuterBrush" Value="Yellow"/>
            </Style>
            <Style x:Key="styleEmpty" TargetType="local:Clock"/>
        </Grid.Resources>
        <StackPanel>
            <local:Clock x:Name="clock" Orientation="Horizontal">
                <Rectangle Width="100" Height="100" Fill="{Binding ActualInnerBrush, ElementName=clock}"/>
                <Rectangle Width="100" Height="100" Fill="{Binding ActualOuterBrush, ElementName=clock}"/>
            </local:Clock>
            <StackPanel Orientation="Horizontal">
                <Button Content="Default">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:ChangePropertyAction TargetObject="{Binding ElementName=clock}" PropertyName="Style" Value="{x:Null}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Content="BrushOnly">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:ChangePropertyAction TargetObject="{Binding ElementName=clock}" PropertyName="Style" Value="{StaticResource styleBrush}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Content="InnerOnly">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:ChangePropertyAction TargetObject="{Binding ElementName=clock}" PropertyName="Style" Value="{StaticResource styleInnerOnly}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Content="InnerOuter">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:ChangePropertyAction TargetObject="{Binding ElementName=clock}" PropertyName="Style" Value="{StaticResource styleInnerOuter}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
                <Button Content="Empty">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <ei:ChangePropertyAction TargetObject="{Binding ElementName=clock}" PropertyName="Style" Value="{StaticResource styleEmpty}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
            </StackPanel>
        </StackPanel>
    </Grid>
    

    此示例使用行为。如果您不熟悉行为,请安装 Expression Blend 4 SDK 并添加以下命名空间:

    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    

    并将System.Windows.InteractivityMicrosoft.Expression.Interactions 添加到您的项目中。

    【讨论】:

    • 我尝试了您的解决方案,很抱歉没有解决我的边框问题。我创建了另一对 Brush 属性并从其他 3 个属性中设置它们。使用此设置,我可以在所有样式之间切换而不会出现任何视觉问题...问题是我的 Ellipse 对象绑定到 Brush 属性,因此它们必须公开...然后用户可以设置它们,我也会遇到同样的问题。
    • 没问题:然后将其公开为只读!我建议受保护的唯一原因是它们不会出现在您的消费者的 IntelliSense 列表中。要创建只读依赖属性,请使用 DependencyProperty.RegisterReadOnly 并使用私有 setter。
    • 回调仅在值改变时调用,如果属性有值则样式设置器无效,因此值保持不变。因此,值被卡住的原因和属性更改处理程序似乎停止被调用的原因是由于相同的原因:样式不能显式覆盖设置值。
    • 哦,天哪,我刚刚更新了帖子,但我应该等... ? (请参阅帖子中的更新部分)
    • 再次感谢您抽出宝贵时间提供如此完整的答案。我非常喜欢您使用 Lambda 表达式来允许回调方法是非静态的。不幸的是,即使您的实施也不能解决我的最终问题。请参阅我的帖子更新 2 了解更多详情。
    猜你喜欢
    • 2011-08-22
    • 1970-01-01
    • 2013-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-10
    相关资源
    最近更新 更多