Почему привязка к предку становится активной позже, чем привязка к элементу по его имени или привязка к DataContext?

Я заметил это при попытке установить привязку на короткий промежуток времени в коде. На самом деле, я просто хочу получить значение, предоставляемое связыванием. Поэтому я установил привязку, получил значение целевого свойства и сразу очистил привязку. Все хорошо, пока RelativeSource с режимом FindAncestor не установлен для привязки. В этом случае целевое свойство возвращает значение по умолчанию.

После некоторой отладки я обнаружил, что свойство BindingExpression для привязки FindAncestor имеет свойство Status, установленное в Unattached. Для других типов привязок BindingExpression.Status имеет значение Active.

Я написал код, чтобы проиллюстрировать это.

Window1.xaml

<Window x:Class="Wpf_SetBindingInCode.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window"
        Title="Window1"
        Height="300" Width="300"
        DataContext="DataContext content">
    <StackPanel>
        <Button Content="Set binding" Click="SetBindingButtonClick"/>
        <TextBlock x:Name="TextBlock1"/>
        <TextBlock x:Name="TextBlock2"/>
        <TextBlock x:Name="TextBlock3"/>
    </StackPanel>
</Window>

Window1.xaml.cs

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
    }

    private void SetBindingButtonClick(object sender, RoutedEventArgs e)
    {
        Binding bindingToRelativeSource = new Binding("DataContext")
        {
            RelativeSource = new RelativeSource { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(Window1) },
        };
        Binding bindingToElement = new Binding("DataContext")
        {
            ElementName = "Window"
        };
        Binding bindingToDataContext = new Binding();

        BindingOperations.SetBinding(TextBlock1, TextBlock.TextProperty, bindingToRelativeSource);
        BindingOperations.SetBinding(TextBlock2, TextBlock.TextProperty, bindingToElement);
        BindingOperations.SetBinding(TextBlock3, TextBlock.TextProperty, bindingToDataContext);

        Trace.WriteLine("TextBlock1.Text = \"" + TextBlock1.Text + "\"");
        Trace.WriteLine("TextBlock2.Text = \"" + TextBlock2.Text + "\"");
        Trace.WriteLine("TextBlock3.Text = \"" + TextBlock3.Text + "\"");

        var bindingExpressionBase1 = BindingOperations.GetBindingExpressionBase(TextBlock1, TextBlock.TextProperty);
        var bindingExpressionBase2 = BindingOperations.GetBindingExpressionBase(TextBlock2, TextBlock.TextProperty);
        var bindingExpressionBase3 = BindingOperations.GetBindingExpressionBase(TextBlock3, TextBlock.TextProperty);

        Trace.WriteLine("bindingExpressionBase1.Status = " + bindingExpressionBase1.Status);
        Trace.WriteLine("bindingExpressionBase2.Status = " + bindingExpressionBase2.Status);
        Trace.WriteLine("bindingExpressionBase3.Status = " + bindingExpressionBase3.Status);
    }
}

Код выше производит следующий вывод:

TextBlock1.Text = ""
TextBlock2.Text = "DataContext content"
TextBlock3.Text = "DataContext content"
bindingExpressionBase1.Status = Unattached
bindingExpressionBase2.Status = Active
bindingExpressionBase3.Status = Active

Но, несмотря на это, все три TextBlocks в форме имеют ожидаемые значения - "DataContext content".

Итак, мои вопросы:

  1. Почему привязка RelativeSourceMode.FindAncestor не предоставляет значение сразу после вызова BindingOperations.SetBinding(...)?

  2. Есть ли способ заставить этот вид привязки обновить целевое свойство? Я пытался вызвать bindingExpression.UpdateTarget() - он не работает, как ожидалось.

1 ответ

Решение

Это по замыслу. Чтобы понять почему, давайте посмотрим на код.

Когда Expression устанавливается как значение DependencyProperty Expression.OnAttach называется ( источник). Этот метод переопределяется в BindingExpressionBase класс ( источник):

internal sealed override void OnAttach(DependencyObject d, DependencyProperty dp)
{
    if (d == null)
        throw new ArgumentNullException("d");
    if (dp == null)
        throw new ArgumentNullException("dp");

    Attach(d, dp);
}

internal void Attach(DependencyObject target, DependencyProperty dp)
{
    // make sure we're on the right thread to access the target
    if (target != null)
    {
        target.VerifyAccess();
    }

    IsAttaching = true;
    AttachOverride(target, dp);
    IsAttaching = false;
}

AttachOverride Метод тоже виртуален и переопределяется в BindingExpression ( источник).

internal override bool AttachOverride(DependencyObject target, DependencyProperty dp)
{
    if (!base.AttachOverride(target, dp))
        return false;

    // listen for InheritanceContext change (if target is mentored)
    if (ParentBinding.SourceReference == null || ParentBinding.SourceReference.UsesMentor)
    {
        DependencyObject mentor = Helper.FindMentor(target);
        if (mentor != target)
        {
            InheritanceContextChangedEventManager.AddHandler(target, OnInheritanceContextChanged);
            UsingMentor = true;

            if (TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.Attach))
            {
                TraceData.Trace(TraceEventType.Warning,
                                    TraceData.UseMentor(
                                        TraceData.Identify(this),
                                        TraceData.Identify(mentor)));
                }
            }
        }

        // listen for lost focus
        if (IsUpdateOnLostFocus)
        {
            Invariant.Assert(!IsInMultiBindingExpression, "Source BindingExpressions of a MultiBindingExpression should never be UpdateOnLostFocus.");
            LostFocusEventManager.AddHandler(target, OnLostFocus);
        }

        // attach to things that need tree context.  Do it synchronously
        // if possible, otherwise post a task.  This gives the parser et al.
        // a chance to assemble the tree before we start walking it.
        AttachToContext(AttachAttempt.First);
        if (StatusInternal == BindingStatusInternal.Unattached)
        {
            Engine.AddTask(this, TaskOps.AttachToContext);

            if (TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.AttachToContext))
            {
                TraceData.Trace(TraceEventType.Warning,
                                    TraceData.DeferAttachToContext(
                                        TraceData.Identify(this)));
        }
    }

    GC.KeepAlive(target);   // keep target alive during activation (bug 956831)
    return true;
}

В приведенном коде видно, что после всех действий BindingExpression может быть еще Unattached, Давайте посмотрим, почему это так в нашей ситуации. Для этого нам нужно определить, где меняется статус. Это может сделать IL Spy, который показывает, что статус изменяется в AttachToContext ( источник).

// try to get information from the tree context (parent, root, etc.)
// If everything succeeds, activate the binding.
// If anything fails in a way that might succeed after further layout,
// just return (with status == Unattached).  The binding engine will try
// again later. For hard failures, set an error status;  no more chances.
// During the "last chance" attempt, treat all failures as "hard".
void AttachToContext(AttachAttempt attempt)
{
    // if the target has been GC'd, just give up
    DependencyObject target = TargetElement;
    if (target == null)
        return;     // status will be Detached

    bool isExtendedTraceEnabled = TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.AttachToContext);
    bool traceObjectRef = TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.SourceLookup);

    // certain features should never be tried on the first attempt, as
    // they certainly require at least one layout pass
    if (attempt == AttachAttempt.First)
    {
        // relative source with ancestor lookup
        ObjectRef or = ParentBinding.SourceReference;
        if (or != null && or.TreeContextIsRequired(target))
        {
            if (isExtendedTraceEnabled)
            {
                TraceData.Trace(TraceEventType.Warning,
                                    TraceData.SourceRequiresTreeContext(
                                        TraceData.Identify(this),
                                        or.Identify()));
            }

            return;
        }
    }

В комментариях говорится, что для некоторых функций требуется как минимум один проход макета, и что один из них RelativeSource с поиском предка ( источник).

internal bool TreeContextIsRequired(DependencyObject target)
{
    return ProtectedTreeContextIsRequired(target);
}

/// <summary> true if the ObjectRef really needs the tree context </summary>
protected override bool ProtectedTreeContextIsRequired(DependencyObject target)
{
    return  (   (_relativeSource.Mode == RelativeSourceMode.FindAncestor
        ||  (_relativeSource.Mode == RelativeSourceMode.PreviousData)));
}

Поскольку контекст дерева требуется для RelativeSource BindingExpression является Unattached, Поэтому значение свойства не обновляется сразу.

взывать UpdateLayout на любом UIElement заставить обновление макета и присоединение BindingExpression"S.

Другие вопросы по тегам