次の方法で共有


Globally updating binding sources in WPF

WPF data bindings do not always immediately update the source when the target (typically a WPF control, such as a TextBox) changes. An extremely common case of this is the default behavior of bindings on the Text property of a TextBox. Specifically, the UpdateSourceTrigger of bindings on this property is set to LostFocus by default. This means the source of the binding will be updated when the TextBox loses logical focus within its focus scope (see https://msdn.microsoft.com/en-us/library/aa969768.aspx#Logical_Focus for an overview of logical focus and focus scopes in WPF). This means that there can be times when the source data is temporarily out of sync with the target (the control). In certain scenarios, this can result in unexpected data loss. A typical situation is as follows:

  1. Data is loaded from some data source
  2. A TextBox is bound to that data
  3. The Text of the TextBox is changed
  4. A ToolBar Button, MenuItem, or shortcut key is invoked to submit the data back to the data source
  5. The data source remains unchanged!

Typically a ToolBar or a Menu has its own logical focus scope, and therefore interacting with them does not remove logical focus from the TextBox. If a shortcut key is pressed, this also typically will not remove logical focus from the TextBox. Since logical focus was not lost, the binding source was not updated, and so the seemingly changed data was not submitted back to the data source.

One simple solution is to exhaustively update all binding sources before submitting the data. This can be a bit tricky, so I'll walk through the code required for this.

First, I'll implement a couple DependencyObject extension methods that make traversing the visual tree simpler:

public static IEnumerable<DependencyObject> EnumerateVisualChildren(this DependencyObject dependencyObject)
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
    {
        yield return VisualTreeHelper.GetChild(dependencyObject, i);
    }
}

public static IEnumerable<DependencyObject> EnumerateVisualDescendents(this DependencyObject dependencyObject)
{
    yield return dependencyObject;

    foreach (DependencyObject child in dependencyObject.EnumerateVisualChildren())
    {
        foreach (DependencyObject descendent in child.EnumerateVisualDescendents())
        {
            yield return descendent;
        }
    }
}

Next, I'll implement a DependencyObject extension method that updates the binding sources for all DependencyPropertys on that DependencyObject and all of its visual descendents:

public static void UpdateBindingSources(this DependencyObject dependencyObject)
{
    foreach (DependencyObject element in dependencyObject.EnumerateVisualDescendents())
    {
        LocalValueEnumerator localValueEnumerator = element.GetLocalValueEnumerator();
        while (localValueEnumerator.MoveNext())
        {
      BindingExpressionBase bindingExpression = BindingOperations.GetBindingExpressionBase(element, localValueEnumerator.Current.Property);
            if (bindingExpression != null)
            {
                bindingExpression.UpdateSource();
         }
        }
    }
}

All that's needed at this point is to invoke the UpdateBindingSources extension method on an appropriate DependencyObject (such as the top level Window) before submitting the data. This will ensure that the values in all binding targets have been pushed back to the binding sources. If the application in question has multiple Windows that should all have their binding sources updated, one more extension method may be useful:

public static void UpdateBindingSources(this Application application)
{
    foreach (Window window in application.Windows)
    {
        window.UpdateBindingSources();
    }
}

Optionally, this extension method could be called before submitting the data:

Application.Current.UpdateBindingSources();

It probably seems like this could be incredibly slow, but in practice, I've found that this causes no noticeable delay in a medium sized application with a significant number of bindings. If this became a performance bottleneck, it might be possible to optimize specifically around the case of UpdateTrigger.LostFocus by carefully examining the WPF FocusManager to determine the actual set of elements that currently have logical focus and only updating binding sources on those elements.

It's also worth noting that this generic solution can be incorrect in certain binding scenarios. For example, an application might be using Bindings with the UpdateSourceTrigger set to Explicit and use some special criteria to decide when to commit the binding changes. Obviously in such a case, an indiscriminant call to the UpdateBindingSources extension method might thwart those efforts. Again, in practice I've found this generic solution to solve the underlying problem without significant side effects.

If you have an alternative solution to this problem, I'd love to hear about it. Other feedback is of course welcome as well.

Comments

  • Anonymous
    June 15, 2010
    EnumerateVisualDescendents should also yield return child; inside the first foreach loop. Otherwise, only leaf nodes are returned, and it does not work.

  • Anonymous
    June 15, 2010
    Good catch; thanks!

  • Anonymous
    December 07, 2013
    Good job! It saved me a few hours. Thx. :)