是否可以获得 DependencyProperty 在每个优先级的值,而不仅仅是最终值?

发布于 2024-12-20 14:59:16 字数 1027 浏览 4 评论 0原文

我们试图获取 DependencyProperty 的值(如果未在 XAML 中本地设置)。例如,采用这个 XAML...

<StackPanel Orientation="Vertical"
    TextElement.FontSize="24">

    <TextBlock Text="What size am I?"
        FontSize="{Binding FontSize, RelativeSource={RelativeSource Self}, Converter={StaticResource PercentageConverter}, ConverterParameter=0.75}" />

</StackPanel>

这当然行不通。我所做的是......

<TextBlock Text="What size am I?"
    FontSize="{Binding (TextElement.FontSize), RelativeSource={RelativeSource AncestorType=FrameworkElement}, Converter={StaticResource PercentageConverter}, ConverterParameter=0.5}" />

它检查继承的 FontSize 属性的父级值,但这仅起作用,因为 FontSize 确实 支持继承。我正在寻找适用于任何 DependencyProperty 的解决方案,而不仅仅是可以继承的解决方案。

更新

我将问题的标题更改为更通用,以说明什么对我们有帮助。具体来说,我们希望能够说“对于 DependencyObject 的这个特定实例,每个优先级的 DependencyProperty 的值是多少?”当我们查询 DP 时,我们当然会得到应用所有优先级后的值。我们很乐意说“会高一到两级吗?”等等。

We are trying to get what the value of a DependencyProperty would be if it weren't locally set in the XAML. For instance, take this XAML...

<StackPanel Orientation="Vertical"
    TextElement.FontSize="24">

    <TextBlock Text="What size am I?"
        FontSize="{Binding FontSize, RelativeSource={RelativeSource Self}, Converter={StaticResource PercentageConverter}, ConverterParameter=0.75}" />

</StackPanel>

This of course doesn't work. What I've done instead is this...

<TextBlock Text="What size am I?"
    FontSize="{Binding (TextElement.FontSize), RelativeSource={RelativeSource AncestorType=FrameworkElement}, Converter={StaticResource PercentageConverter}, ConverterParameter=0.5}" />

...which checks the parent's value for the inherited FontSize property but that only works because FontSize does support inheritance. I'm looking for a solution for any DependencyProperty, not just ones that can be inherited.

Update

I changed the title of the question to be a little more generic as to what would be helpful to us. Specifically we'd love to be able to say 'For this specific instance of a DependencyObject, what would the value of a DependencyProperty be for each of the precedence levels?' When we query a DP, we of course get what the value after all precedence is applied. We'd love to say 'What would it be one or two levels higher?', etc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

咿呀咿呀哟 2024-12-27 14:59:16

通用且强大的解决方案应考虑DependencyProperty 值优先级。我不认为可以安全地说,如果一个值没有在本地设置,那么它的值将是默认值 - 该值可以由样式、触发器等提供。

我建议采取查看 DependencyPropertyHelper 类。此类包含一个名为 GetValueSource 的方法,当给定 DependencyObjectDependencyProperty 时,该方法将返回 BaseValueSource 结果告诉您属性的值来自何处(样式、样式触发器、继承、当地的, ETC)。

话虽这么说,我认为编写一个附加行为并不太难,您可以在 XAML 中使用该行为来返回指定 DependencyProperty 的“非本地”值。此行为将检查值源是否是本地的,如果是克隆元素,则将其添加到逻辑树中,并清除本地值。然后,它会从克隆上指定的 DependencyProperty 读取值,并将只读附加的 DependencyProperty 设置为此值。这样,您就可以让 WPF 属性引擎为您完成工作。

希望这有帮助!

编辑:

这是附加行为的概念证明。该行为有 2 个属性:

  1. PropertyToInspectValue - 重视您的 DependencyProperty
    希望检查。
  2. InspectedValue - 为指定的 DependencyProperty 计算的非本地值。

在 XAML 中,您绑定到这 2 个属性(设置一个,读取另一个):

<StackPanel>
    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize" Value="30" />
            </Style>
            <Style x:Key="NamedStyle" TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize" Value="35" />
            </Style>
        </StackPanel.Resources>
        <!-- Without local value - uses default style (30)-->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
        <!-- Without local value - uses named style (35)-->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Style="{StaticResource NamedStyle}"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
    </StackPanel>
    <StackPanel TextElement.FontSize="25">
        <!-- Without local value - uses inherited value (25) -->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
    </StackPanel>
    <!-- Without local value - uses default font size (11) -->
    <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
               Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
               FontSize="15" />
</StackPanel>

上面的示例将正确显示值:30、35、25 和 11。下面代码中的一些限制是 DependencyObject我们正在检查的 必须是 Panel 的子级,并且当前仅复制 Style 属性,但实际上应该克隆所有属性,因为触发器在风格上可以使用任何属性来更改 DependencyProperty 的值。我想深思熟虑...

附加行为:

public static class DependencyPropertyValueHelper
{
    private static bool _isUpdating;

    #region InspectedValue Read-only Attached Dependency Property

    public static object GetInspectedValue(DependencyObject d)
    {
        return d.GetValue(InspectedValueProperty);
    }

    private static readonly DependencyPropertyKey InspectedValuePropertyKey =
        DependencyProperty.RegisterAttachedReadOnly("InspectedValue", typeof (object),
                                                    typeof (DependencyPropertyValueHelper),
                                                    new FrameworkPropertyMetadata(null));

    public static readonly DependencyProperty InspectedValueProperty = InspectedValuePropertyKey.DependencyProperty;

    #endregion

    #region PropertyToInspect Attached Dependency Property

    public static void SetPropertyToInspectValue(DependencyObject d, DependencyProperty dependencyProperty)
    {
        d.SetValue(PropertyToInspectValueProperty, dependencyProperty);
    }

    public static DependencyProperty GetPropertyToInspectValue(DependencyObject d)
    {
        return (DependencyProperty)d.GetValue(PropertyToInspectValueProperty);
    }

    public static readonly DependencyProperty PropertyToInspectValueProperty =
        DependencyProperty.RegisterAttached("PropertyToInspectValue", typeof (DependencyProperty),
                                            typeof (DependencyPropertyValueHelper),
                                            new FrameworkPropertyMetadata(OnPropertyToInspectValuePropertyChanged));

    #endregion

    #region Private ValueChanged Attached Dependency Property

    public static readonly DependencyProperty ValueChangedProperty =
        DependencyProperty.RegisterAttached("ValueChanged", typeof(object),
                                            typeof(DependencyPropertyValueHelper),
                                            new FrameworkPropertyMetadata(OnValuePropertyChanged));

    #endregion

    private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!_isUpdating)
            DetermineNonLocalValue(d, GetPropertyToInspectValue(d));
    }

    private static void OnPropertyToInspectValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dependencyProperty = (DependencyProperty) e.NewValue;
        DetermineNonLocalValue(d, dependencyProperty);

        var binding = new Binding
                          {
                              RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                              Path = new PropertyPath(dependencyProperty.Name),
                          };

        BindingOperations.SetBinding(d, ValueChangedProperty, binding);
    }

    private static void DetermineNonLocalValue(DependencyObject d, DependencyProperty dependencyProperty)
    {
        var element = d as FrameworkElement;

        if (element == null)
            return;

        var valueSource = DependencyPropertyHelper.GetValueSource(element, dependencyProperty);

        if (valueSource.BaseValueSource == BaseValueSource.Local)
        {
            _isUpdating = true;

            var clonedDependencyObject = Activator.CreateInstance(element.GetType()) as FrameworkElement;
            var parent = VisualTreeHelper.GetParent(element) as Panel;

            // Currently only works if parent is a panel
            if (parent != null)
            {
                // Copy any property which could impact the DP's value ...
                // Probably check if this is a control and copy the ControlTemplate too ...
                if (element.Style != null)
                    clonedDependencyObject.Style = element.Style;

                parent.Children.Add(clonedDependencyObject);

                // Let WPF provide us with the non-local value
                element.SetValue(InspectedValuePropertyKey, clonedDependencyObject.GetValue(dependencyProperty));

                parent.Children.Remove(clonedDependencyObject);
            }

            _isUpdating = false;
        }
        else
        {
            element.SetValue(InspectedValuePropertyKey, d.GetValue(dependencyProperty));
        }
    }
}

编辑2

详细说明这种方法。 WPF 没有公开任何我们可以检查的值链(或者至少据我所知)。上面的代码尝试发现如果未在本地设置该值(来自任何值源:继承、样式、触发器等),该值将会是什么。当它发现该值时,它会使用结果填充 InspectedValue 属性,然后可以从中读取结果。

第一次尝试是:

  1. 清除 DP 的本地值,将其存储在某个地方(例如使用 DependencyObject.ClearValue )
  2. 查询 DP 的新值,我们假设该值就是这个值如果不是本地设置的话就会这样。记录该值。
  3. 恢复步骤 1 中的原始值。

此方法失败 - 在步骤 2 中,您的属性不会从默认样式中获取值(例如),因为样式已被应用,并且只需从随机 DependencyPropery 不会触发重新申请。

另一种方法是上面的步骤 2 - 从逻辑树中删除元素,然后将其添加回来(因为当将元素添加到树中时,WPF 会遍历所有可能的值源来确定每个 DP 的值)。这也因同样的原因而失败 - 不能保证样式将被重新应用。

上面附加的行为尝试了第三种方法 - 克隆一个元素,但确保克隆上没有设置有问题的属性,然后将其添加到逻辑树中。此时,WPF 将像处理原始元素一样处理该元素,并将值应用于 DP(继承、触发器、样式等)。然后我们可以读取它的值并将其存储起来。如所示,此方法适用于常见用例,但并不完美,因为如果非本地值来自使用只读属性(如 IsMouseOver)的 Trigger它不会拾取它(并且原始对象状态的深层副本无法解决此问题)。

A generic and robust solution should take into account the full rule set for DependencyProperty value precedence. I don't think that it is safe to say that if a value is not set locally, then its value would be the default value - the value instead could then be provided by a style, a trigger, etc.

I would suggest taking a look at the class DependencyPropertyHelper. This class contains a method called GetValueSource which when given a DependencyObject and a DependencyProperty will return a BaseValueSource result that tells you where the value for the property is coming from (style, style trigger, inherited, local, etc).

That being said, I think it would not be too hard to write an attached behaviour which you could use in XAML to return the "non-local" value for a specified DependencyProperty. This behaviour would check if the value source is local, and if it is clone the element, add it to the logical tree, and clear the local value. It would then read the value from the specified DependencyProperty on the clone, and set a read-only attached DependencyProperty to this value. This way you are letting the WPF property engine do the work for you.

Hope this helps!

Edit:

Here is a proof of concept for the attached behaviour. The behaviour has 2 properties:

  1. PropertyToInspectValue - The DependencyProperty who's value you
    wish to inspect.
  2. InspectedValue - The computed non-local value for the specified DependencyProperty.

In XAML you bind to these 2 properties (setting one, reading the other):

<StackPanel>
    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize" Value="30" />
            </Style>
            <Style x:Key="NamedStyle" TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize" Value="35" />
            </Style>
        </StackPanel.Resources>
        <!-- Without local value - uses default style (30)-->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
        <!-- Without local value - uses named style (35)-->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Style="{StaticResource NamedStyle}"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
    </StackPanel>
    <StackPanel TextElement.FontSize="25">
        <!-- Without local value - uses inherited value (25) -->
        <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
                   Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
                   FontSize="15" />
    </StackPanel>
    <!-- Without local value - uses default font size (11) -->
    <TextBlock l:DependencyPropertyValueHelper.PropertyToInspectValue="TextElement.FontSize"
               Text="{Binding Path=(l:DependencyPropertyValueHelper.InspectedValue), RelativeSource={RelativeSource Self}}"
               FontSize="15" />
</StackPanel>

The example above will correctly show the values: 30, 35, 25, and 11. A couple of limitations in the code below is that the DependencyObject which we are inspecting must be a child of a Panel, and that currently only the Style property is copied over, but really all properties should be cloned since triggers in the style could use any property to change the value a DependencyProperty. Food for thought I guess ...

Attached Behaviour:

public static class DependencyPropertyValueHelper
{
    private static bool _isUpdating;

    #region InspectedValue Read-only Attached Dependency Property

    public static object GetInspectedValue(DependencyObject d)
    {
        return d.GetValue(InspectedValueProperty);
    }

    private static readonly DependencyPropertyKey InspectedValuePropertyKey =
        DependencyProperty.RegisterAttachedReadOnly("InspectedValue", typeof (object),
                                                    typeof (DependencyPropertyValueHelper),
                                                    new FrameworkPropertyMetadata(null));

    public static readonly DependencyProperty InspectedValueProperty = InspectedValuePropertyKey.DependencyProperty;

    #endregion

    #region PropertyToInspect Attached Dependency Property

    public static void SetPropertyToInspectValue(DependencyObject d, DependencyProperty dependencyProperty)
    {
        d.SetValue(PropertyToInspectValueProperty, dependencyProperty);
    }

    public static DependencyProperty GetPropertyToInspectValue(DependencyObject d)
    {
        return (DependencyProperty)d.GetValue(PropertyToInspectValueProperty);
    }

    public static readonly DependencyProperty PropertyToInspectValueProperty =
        DependencyProperty.RegisterAttached("PropertyToInspectValue", typeof (DependencyProperty),
                                            typeof (DependencyPropertyValueHelper),
                                            new FrameworkPropertyMetadata(OnPropertyToInspectValuePropertyChanged));

    #endregion

    #region Private ValueChanged Attached Dependency Property

    public static readonly DependencyProperty ValueChangedProperty =
        DependencyProperty.RegisterAttached("ValueChanged", typeof(object),
                                            typeof(DependencyPropertyValueHelper),
                                            new FrameworkPropertyMetadata(OnValuePropertyChanged));

    #endregion

    private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!_isUpdating)
            DetermineNonLocalValue(d, GetPropertyToInspectValue(d));
    }

    private static void OnPropertyToInspectValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dependencyProperty = (DependencyProperty) e.NewValue;
        DetermineNonLocalValue(d, dependencyProperty);

        var binding = new Binding
                          {
                              RelativeSource = new RelativeSource(RelativeSourceMode.Self),
                              Path = new PropertyPath(dependencyProperty.Name),
                          };

        BindingOperations.SetBinding(d, ValueChangedProperty, binding);
    }

    private static void DetermineNonLocalValue(DependencyObject d, DependencyProperty dependencyProperty)
    {
        var element = d as FrameworkElement;

        if (element == null)
            return;

        var valueSource = DependencyPropertyHelper.GetValueSource(element, dependencyProperty);

        if (valueSource.BaseValueSource == BaseValueSource.Local)
        {
            _isUpdating = true;

            var clonedDependencyObject = Activator.CreateInstance(element.GetType()) as FrameworkElement;
            var parent = VisualTreeHelper.GetParent(element) as Panel;

            // Currently only works if parent is a panel
            if (parent != null)
            {
                // Copy any property which could impact the DP's value ...
                // Probably check if this is a control and copy the ControlTemplate too ...
                if (element.Style != null)
                    clonedDependencyObject.Style = element.Style;

                parent.Children.Add(clonedDependencyObject);

                // Let WPF provide us with the non-local value
                element.SetValue(InspectedValuePropertyKey, clonedDependencyObject.GetValue(dependencyProperty));

                parent.Children.Remove(clonedDependencyObject);
            }

            _isUpdating = false;
        }
        else
        {
            element.SetValue(InspectedValuePropertyKey, d.GetValue(dependencyProperty));
        }
    }
}

Edit 2

To elaborate on this approach. There is no chain of values exposed by WPF which we can inspect (or at least that I know of). The code above attempts to discover what the value would be if it was not set locally (from whatever value source: inherited, styles, triggers, etc). When it discovers this value it then populates the InspectedValue property with the result, which then can be read from.

A first attempt at this would be to:

  1. Clear the local value for the DP, store it away somewhere (e.g. using DependencyObject.ClearValue)
  2. Query the DP for its new value which we will assume is what the value would have been if it was not set locally. Record this value.
  3. Restore the original value from step 1.

This approach fails - in that during step 2 your property will not pick up values from default styles (for example) since the style has already been applied, and simply removing a local value from a random DependencyPropery does not trigger a re-application.

Another approach would be in step 2 above - remove the element from the logical tree, and then add it back (since when elements are added to the tree, WPF goes through all possible value sources to determine the value of each DP). This too fails for the same reason - you are not guaranteed that styles will be re-applied.

The attached behaviour above tries a third approach - clone an element but make sure the the property in question is not set on the clone, and then add it to the logical tree. At this point WPF will then process the element as it would have with the original element and apply a value to the DP (inherited, triggers, style, etc). We can then read its value, and store it away. As demonstrated, this approach works for common use-cases, but is not perfect since if the non-local value was coming from a Trigger using a read-only property like IsMouseOver it would not pick it up (and a deep copy of the original object state would not fix this).

左岸枫 2024-12-27 14:59:16

您应该使用最适合您需要的 GetPropertyMetaData 的重载方法(应该是使用类型的方法),然后您可以使用... DefaultValue 属性检索默认值。

you should use the overloaded method of GetPropertyMetaData that suit best your need (that should be the one using types), and then you can retrieve the default value with... the DefaultValue property.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文