特殊的附加属性绑定有效,但不适用于DataTemplate [英] Special attached property binding works, but not in a DataTemplate

查看:81
本文介绍了特殊的附加属性绑定有效,但不适用于DataTemplate的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的目标是能够通过绑定为我的代码提供对UI元素的引用(而不是给该元素一个 Name 或必须手动遍历可视化树来查找它)).

My goal is to be able to give my code a reference to a UI element via a binding (as opposed to giving the element a Name or having to walk through the visual tree manually to find it).

为此,我创建了一个特殊的附加依赖项属性,称为 Self .它基于此答案中的代码.它有两种特殊的用途:

To that end, I've created a special attached dependency property called Self. It's based off the code from this answer. It's intended to be special in two ways:

  1. Self 的值应始终是对其设置元素的引用.因此,如果在 Button 上使用 Self ,则 Self 的值应始终返回对所述 Button 的引用.
  2. Self 属性绑定后,应将其值压入绑定源.
  1. The value of Self should always be a reference to the element on which it is set. So if Self is used on a Button, the value of Self should always return a reference to said Button.
  2. The Self property, when bound to, should push its value onto the binding source.

基本上,您应该可以这样做:

Basically, you should be able to do this:

<Button Name="A" local:BindingHelper.Self="{Binding Foo.Button}"/>

然后,将为 Foo.Button 提供 Button A 对象作为其值.

And Foo.Button would then be given the Button A object as its value.

为此,我改编了先前提到的答案中的代码,并创建了以下代码:

To accomplish this, I've adapted the code from the previously mentioned answer and created this:

Public Class BindingHelper
    Public Shared ReadOnly SelfProperty As DependencyProperty =
            DependencyProperty.RegisterAttached("Self", GetType(DependencyObject), GetType(BindingHelper),
                                                New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                              AddressOf Self_PropertyChanged, AddressOf Self_CoerceValue))

    Public Shared Function GetSelf(element As DependencyObject) As DependencyObject
        Return element.GetValue(SelfProperty)
    End Function
    Public Shared Sub SetSelf(element As DependencyObject, value As DependencyObject)
        element.SetValue(SelfProperty, value)
    End Sub

    Private Shared Sub Self_PropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        If e.NewValue IsNot d Then UpdateSelfValue(d)
    End Sub

    Private Shared SelfCoercionInProgress As New HashSet(Of DependencyObject)

    Private Shared Function Self_CoerceValue(d As DependencyObject, baseValue As Object) As Object
        If baseValue IsNot d AndAlso Not SelfCoercionInProgress.Contains(d) Then
            SelfCoercionInProgress.Add(d)
            UpdateSelfValue(d)
            SelfCoercionInProgress.Remove(d)
        End If

        Return d
    End Function

    Private Shared Sub UpdateSelfValue(d As DependencyObject)
        Dim B = BindingOperations.GetBindingExpression(d, SelfProperty)

        If B IsNot Nothing AndAlso B.Status <> BindingStatus.Detached Then
            B.UpdateTarget()
            SetSelf(d, d)
            B.UpdateSource()
        Else
            SetSelf(d, d)
        End If
    End Sub
End Class

用于测试和错误重现的代码

一个简单的 MainWindow.xaml.vb :

Class MainWindow
    Public Property Foo As New Foo

    Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
        'You can put a breakpoint here
    End Sub
End Class

Public Class Foo
    Public Property Button As Button
End Class

MainWindow.xaml :

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:VBTest"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">

    <StackPanel>
        <Button Name="A" local:BindingHelper.Self="{Binding Foo.Button}" Click="Button_Click">A</Button>

        <ContentControl Content="{Binding Foo}">
            <ContentControl.ContentTemplate>
                <DataTemplate>
                    <!--<Button Name="B" local:BindingHelper.Self="{Binding Button}" Click="Button_Click">B</Button>-->
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>
    </StackPanel>
</Window>

请注意, DataTemplate 中的 B 行已被注释掉.如果您运行上面的代码,它将按预期工作. Foo.Button 被赋予对 Button A 的引用.

Notice that the B line in the DataTemplate is commented out. If you run the above code, it works as intended. Foo.Button is given a reference to Button A.

现在,注释掉 A 行,并取消注释 B 行.从理论上讲,它应该工作完全相同,我所做的只是将 Button 移到 DataTemplate 中,但是由于某些原因,将 Foo.Button 永远不会引用 Button B .这是我需要帮助弄清楚的部分.无法在 DataTemplate 中使用它,我永远不能在 ItemsControl 中使用它.

Now, comment out the A line and un-comment the B line. In theory, it should work exactly the same, all I've done is move the Button into a DataTemplate, but for some reason Foo.Button is never given a reference to Button B. This is the part I need help figuring out. Without being able to use this in a DataTemplate, I can never use it in an ItemsControl.

问题似乎与以下情况有关:

The problem seems to have something to do with:

Dim B = BindingOperations.GetBindingExpression(d, SelfProperty)

这意外地为 Button B 返回 Nothing ,所以从不调用 UpdateSource .初始化/加载完成后,如果我尝试从断点调用 GetBindingExpression ,它将返回期望值,但是由于某种原因,当在中初始化目标时,它不会这样做DataTemplate .

This unexpectedly returns Nothing for Button B, so UpdateSource is never called. After initialization/loading is finished, if I try calling GetBindingExpression from a breakpoint, it returns the expected value, but for whatever reason it doesn't do so when the target is being initialized inside a DataTemplate.

推荐答案

我无法弄清楚 BindingOperations.GetBindingExpression 当时为何不返回 Nothing 的原因,但是我能够提出一个可靠的解决方法.我做了一些广泛的测试,并绘制出了调用堆栈,并记下了 GetBindingExpression 开始返回值的时间.我发现在我的设置中, Self_PropertyChanged 将被调用两次,但是 GetBindingExpression 仅在第二轮工作.因此,我添加了代码以跟踪在上一次调用 Self_PropertyChanged 的过程中是否存在绑定,如果现在不存在,则再次执行更新过程.

I couldn't figure out exactly why BindingOperations.GetBindingExpression was returning Nothing at that point, but I was able to come up with a reliable work-around. I did some extensive testing and mapped out the call stack, noting when GetBindingExpression started to return a value. I found that in my setup, Self_PropertyChanged would be called twice, but GetBindingExpression would only work on the second round. So I added code to track whether or not a binding existed during the previous call to Self_PropertyChanged, and if it didn't but one does exist now, then I run through the update procedure again.

这是所有感兴趣的人的完整工作代码:

Here's the full, working code for anyone interested:

Public Class BindingHelper
    Public Shared ReadOnly SelfProperty As DependencyProperty =
        DependencyProperty.RegisterAttached("Self", GetType(DependencyObject), GetType(BindingHelper),
                                            New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                            AddressOf Self_PropertyChanged, AddressOf Self_CoerceValue))

    Public Shared Function GetSelf(element As DependencyObject) As DependencyObject
        Return element.GetValue(SelfProperty)
    End Function
    Public Shared Sub SetSelf(element As DependencyObject, value As DependencyObject)
        element.SetValue(SelfProperty, value)
    End Sub

    Private Shared Sub Self_PropertyChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim NeedToChange = e.NewValue IsNot d
        Dim B As BindingExpression

        If NeedToChange OrElse HadNoBindingOnLastChange.Contains(d) Then
            B = UpdateSelfValue(d, NeedToChange)
        Else
            B = GetActiveSelfBinding(d)
        End If

        If B Is Nothing Then
            HadNoBindingOnLastChange.Add(d)
        Else
            HadNoBindingOnLastChange.Remove(d)
        End If
    End Sub

    Private Shared SelfCoercionInProgress As New HashSet(Of DependencyObject)
    Private Shared HadNoBindingOnLastChange As New HashSet(Of DependencyObject)
    Private Shared UpdatingWithBindingInProgress As New HashSet(Of DependencyObject)

    Private Shared Function Self_CoerceValue(d As DependencyObject, baseValue As Object) As Object
        If baseValue IsNot d AndAlso Not SelfCoercionInProgress.Contains(d) Then
            SelfCoercionInProgress.Add(d)
            UpdateSelfValue(d, True)
            SelfCoercionInProgress.Remove(d)
        End If

        Return d
    End Function

    Private Shared Function GetActiveSelfBinding(d) As BindingExpression
        Dim B = BindingOperations.GetBindingExpression(d, SelfProperty)
        If B IsNot Nothing AndAlso B.Status = BindingStatus.Detached Then Return Nothing
        Return B
    End Function

    Private Shared Function UpdateSelfValue(d As DependencyObject, AlwaysSetSelf As Boolean) As BindingExpression
        Dim B = GetActiveSelfBinding(d)

        If B IsNot Nothing Then
            If UpdatingWithBindingInProgress.Add(d) Then
                B.UpdateTarget()
                SetSelf(d, d)
                UpdatingWithBindingInProgress.Remove(d)
            End If
        ElseIf AlwaysSetSelf Then
            SetSelf(d, d)
        End If

        Return B
    End Function
End Class

我已经对此进行了测试,它可以在 DataTempalte 的内部或外部运行,并且还可以将源属性自动更改回 Self 的值(如果所说的源)属性在初始化后更改(前提是源支持更改通知).

I've tested this, and it works inside or outside a DataTempalte, and also works to automatically change the source property back to the value of Self if said source property changes after initialization (provided the source supports change notification).

有趣的是,通过我的测试,我发现完全不需要调用 UpdateSource ,因为 SetSelf 总是导致源代码被更新.我删除了该调用,从而消除了对源的 Set Get 以及 Self_CoerceValue 的不必要的调用.

Interestingly, through my testing I found that the call to UpdateSource was completely unnecessary, since SetSelf always resulted in the source being updated. I removed that call which eliminated an unnecessary round of calls to the source's Set and Get, as well as Self_CoerceValue.

这篇关于特殊的附加属性绑定有效,但不适用于DataTemplate的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆