特殊附加 属性 绑定有效,但在 DataTemplate 中无效

Special attached property binding works, but not in a DataTemplate

目标

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

为此,我创建了一个名为 Self 的特殊附加依赖项 属性。它基于 this answer 中的代码。它旨在以两种方式变得特别:

  1. Self 的值应该始终是对其设置的元素的引用。因此,如果 Self 用于 ButtonSelf 的值应始终 return 引用所述 Button.
  2. Self 属性 绑定时,应将其值推送到绑定源。

基本上,您应该能够做到这一点:

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

然后 Foo.Button 将被赋予 Button A 对象作为其值。

主要代码

为此,我改编了 previously mentioned answer 中的代码并创建了这个:

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.

现在,注释掉 A 行并取消注释 B 行。从理论上讲,它应该完全相同,我所做的就是将 Button 移动到 DataTemplate,但由于某种原因,Foo.Button 从未被引用 Button ] B。这是我需要帮助弄清楚的部分。如果无法在 DataTemplate 中使用它,我将永远无法在 ItemsControl 中使用它。

到目前为止我的进度

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

Dim B = BindingOperations.GetBindingExpression(d, SelfProperty)

这意外地 returns Nothing for Button B,所以 UpdateSource 永远不会被调用。 initialization/loading 完成后,如果我尝试从断点调用 GetBindingExpression,它会 returns 预期值,但无论出于何种原因,当目标在内部初始化时它不会这样做DataTemplate.

你应该能够让这个工作通过告诉 ContentControl 在哪里找到 Foo 属性 通过添加:

RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}

<ContentControl Content="{Binding Foo, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">

其他一切都与上面的示例完全相同。

当时我无法弄清楚为什么 BindingOperations.GetBindingExpression returning Nothing,但我想出了一个可靠的解决方法。我做了一些广泛的测试并绘制了调用堆栈,记录了 GetBindingExpression 何时开始 return 一个值。我发现在我的设置中,Self_PropertyChanged 会被调用两次,但 GetBindingExpression 只会在第二轮起作用。所以我添加了代码来跟踪在上一次调用 Self_PropertyChanged 期间是否存在绑定,如果不存在但现在确实存在,那么我会再次 运行 通过更新过程。

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

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 的值,如果说源属性 初始化后更改(前提是源支持更改通知)。

有趣的是,通过我的测试,我发现调用 UpdateSource 是完全没有必要的,因为 SetSelf 总是导致源被更新。我删除了那个调用,它消除了对源 SetGet 以及 Self_CoerceValue.

的不必要的一轮调用