Search code examples
c#wpfbindingrelativesource

Why binding to an ancestor becomes active later than binding to an element by its name or binding to the DataContext?


I noticed this while trying to set binding for a short period of time in code. In fact, I just want to get value provided by binding. So I set the binding, get value of the target property and immediately clear the binding. Everything is good until the RelativeSource with mode FindAncestor is set for the binding. In this case the target property returns its default value.

After some debugging I discovered that the BindingExpression for the FindAncestor binding has its property Status set to Unattached. For other types of bindings BindingExpression.Status is set to Active.

I've written some code to illustrate this.

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);
    }
}

The code above produces the following output:

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

But despite this all three TextBlocks on the form has expected values - "DataContext content".

So my questions are:

  1. Why the RelativeSourceMode.FindAncestor binding does not provide the value immediately after BindingOperations.SetBinding(...) is called?

  2. Is there any way to force this kind of binding to update the target property? I tried to call bindingExpression.UpdateTarget() - it doesn't work like expected.


Solution

  • It's by design. To understand why, let's look into the code.

    When an Expression is set as a value of a DependencyProperty the Expression.OnAttach is called (source). This method is overriden in the BindingExpressionBase class (source):

    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;
    }
    

    The AttachOverride method is virtual too and it's overriden in the BindingExpression (source).

    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;
    }
    

    In the listed code we can see that after all actions BindingExpression can be still Unattached. Let's see why it is so in our situation. For that we need to determine where the status is changed. This can be done by IL Spy which shows that the status is changed in the AttachToContext (source).

    // 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;
            }
        }
    

    It is said in the comments that some features requires at least one layout pass and that one of them is RelativeSource with ancestor lookup (source).

    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)));
    }
    

    Because a tree context is required for the RelativeSource the BindingExpression is Unattached. Therefore the property value isn't updated immediately.

    Invoke UpdateLayout on any UIElement to force layout updating and attaching BindingExpression's.