【问题标题】:Special attached property binding works, but not in a DataTemplate特殊附加属性绑定有效,但不适用于 DataTemplate
【发布时间】:2021-06-27 18:17:16
【问题描述】:

目标

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

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

  1. Self 的值应始终是对其设置的元素的引用。因此,如果在Button 上使用Self,则Self 的值应始终返回对所述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)

这会意外地为Button B 返回Nothing,因此永远不会调用UpdateSource。初始化/加载完成后,如果我尝试从断点调用 GetBindingExpression,它会返回预期值,但无论出于何种原因,当目标在 DataTemplate 内初始化时它不会这样做。

【问题讨论】:

    标签: wpf vb.net data-binding dependency-properties


    【解决方案1】:

    您应该能够通过添加以下内容告诉ContentControl 在哪里找到Foo 属性来使其工作:

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

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

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

    【讨论】:

    • 这似乎比解决问题更能解决问题。没有RelativeSource,绑定已经可以正常工作,因为它从MainWindow 继承了DataContext。但是,显式添加RelativeSource 会导致绑定引擎等到稍后才实际尝试解决绑定。结果是,当绑定引擎解析绑定时,GetBindingExpression 不再返回 Nothing
    【解决方案2】:

    当时我无法弄清楚为什么BindingOperations.GetBindingExpression 会返回Nothing,但我能够想出一个可靠的解决方法。我做了一些广泛的测试并绘制了调用堆栈,注意到GetBindingExpression 何时开始返回一个值。我发现在我的设置中,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 的不必要调用。

    【讨论】:

      猜你喜欢
      • 2020-01-29
      • 2013-07-27
      • 2013-08-01
      • 1970-01-01
      • 2020-11-19
      • 2023-03-03
      • 2017-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多