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:
Why the RelativeSourceMode.FindAncestor binding does not provide the value immediately after BindingOperations.SetBinding(...) is called?
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.
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.