stritch000
stritch000

Reputation: 455

WPF binding OneWayToSource sets source property to "" when the DataContext is changed

I have a OneWayToSource binding that is not behaving as I expected when I set the DataContext of the target control. The property of the source is being set to default instead of the value of the target control's property.

I've created a very simple program in a standard WPF window that illustrates my problem:

XAML

<StackPanel>
  <TextBox x:Name="tb"
    Text="{Binding Path=Text,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}"
    TextChanged="TextBox_TextChanged"/>

  <Button Content="Set DataContext" Click="Button1_Click"/>
</StackPanel>

MainWindow.cs

public partial class MainWindow : Window
{
   private ViewModel _vm = new ViewModel();

   private void Button1_Click(object sender, RoutedEventArgs e)
   {
      Debug.Print("'Set DataContext' button clicked");
      tb.DataContext = _vm;
   }

   private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
   {
      Debug.Print("TextBox changed to " + tb.Text);
   }
}

ViewModel.cs

public class ViewModel
{
   private string _Text;
   public string Text
   {
      get { return _Text; }
      set
      {
         Debug.Print(
            "ViewModel.Text (old value=" + (_Text ?? "<null>") + 
            ", new value=" + (value ?? "<null>") + ")");
         _Text = value;
      }
   }
}

The TextBox tb starts out with a null DataContext and therefore the binding is not expected to do anything. So if I type something in the text box, say "X", the ViewModel.Text property remains null.

If I then click the Set DataContext button I would have expected the ViewModel.Text property to be set to the "X" of the TextBox.Text property. Instead it is set to "". Certainly the binding is working because if I then type "Y" in the text box, after the "X", it sets the ViewModel.Text property to "XY".

Here is an example of the output (the last two lines are counter-intuitive because of the order of evaluation, but they definitely both occur immediately after typing the "Y"):

TextBox changed to X
'Set DataContext' button clicked
ViewModel.Text (old value=<null>, new value=)
ViewModel.Text (old value=, new value=XY)
TextBox changed to XY

Why is the ViewModel.Text property being set to "" instead of "X" when the DataContext is set?

What am I doing wrong? Am I missing something? Have I misunderstood something about binding?

Edit: I would have expected the output to be:

TextBox changed to X
'Set DataContext' button clicked
ViewModel.Text (old value=<null>, new value=X)
ViewModel.Text (old value=X, new value=XY)
TextBox changed to XY

Upvotes: 6

Views: 3765

Answers (5)

Smagin Alexey
Smagin Alexey

Reputation: 345

You need Attached property:

public static readonly DependencyProperty OneWaySourceRaiseProperty = DependencyProperty.RegisterAttached("OneWaySourceRaise", typeof(object), typeof(FrameworkElementExtended), new FrameworkPropertyMetadata(OneWaySourceRaiseChanged));

        public static object GetOneWaySourceRaise(DependencyObject o)
        {
            return o.GetValue(OneWaySourceRaiseProperty);
        }

        public static void SetOneWaySourceRaise(DependencyObject o, object value)
        {
            o.SetValue(OneWaySourceRaiseProperty, value);
        }

        private static void OneWaySourceRaiseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == null)
                return;

            var target = (FrameworkElement)d;
            target.Dispatcher.InvokeAsync(() =>
        {
            var bindings = target.GetBindings().Where(i => i.ParentBinding?.Mode == BindingMode.OneWayToSource).ToArray();
            foreach (var i in bindings)
            {
                i.DataItem.SetProperty(i.ParentBinding.Path.Path, d.GetValue(i.TargetProperty));
            }
        });

And set binding in XAML:

extendends:FrameworkElementExtended.OneWaySourceRaise="{Binding}"

where {Binding} - is binding to DataContext. You need:

    public static IEnumerable<BindingExpression> GetBindings<T>(this T element, Func<DependencyProperty, bool> func = null) where T : DependencyObject
            {
                var properties = element.GetType().GetDependencyProperties();
                foreach (var i in properties)
                {
                    var binding = BindingOperations.GetBindingExpression(element, i);
                    if (binding == null)
                        continue;
                    yield return binding;
                }
            }


private static readonly ConcurrentDictionary<Type, DependencyProperty[]> DependencyProperties = new ConcurrentDictionary<Type, DependencyProperty[]>();
    public static DependencyProperty[] GetDependencyProperties(this Type type)
            {
                return DependencyProperties.GetOrAdd(type, t =>
                {
                    var properties = GetDependencyProperties(TypeDescriptor.GetProperties(type, new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.All) }));
                    return properties.ToArray();
                });
            }

            private static IEnumerable<DependencyProperty> GetDependencyProperties(PropertyDescriptorCollection collection)
            {
                if (collection == null)
                    yield break;
                foreach (PropertyDescriptor i in collection)
                {
                    var dpd = DependencyPropertyDescriptor.FromProperty(i);
                    if (dpd == null)
                        continue;
                    yield return dpd.DependencyProperty;
                }
            }

Upvotes: 0

dev hedgehog
dev hedgehog

Reputation: 8791

Its a bug or perhabs not. Microsoft claims its by design. You first type x and then you kill DataContext by clicking on Button hence why the TextBox holds x and your viewModel.Text property gets newly initialized (its empty). When on datacontext changed getter will still be called. In the end you have no chance to fix this.

You can however use two way and let it be.

Upvotes: 2

TRS
TRS

Reputation: 2097

There is a bug in .NET 4 with one way to source bindings that it calls getter for OneWayToSource bindings thats why you are having this problem.You can verify it by putting breakpoint on tb.DataContext = _vm; you will find setter is called and just after that getter is called on Text property.You can resolve your problem by manually feeding the viewmodel values from view before assigning the datacontext..NET 4.5 resolves this issue. see here and here too

private void Button1_Click(object sender, RoutedEventArgs e)
{
   Debug.Print("'Set DataContext' button clicked");       
    _vm.Text=tb.Text;
    tb.DataContext = _vm;
}

Upvotes: 0

Rang
Rang

Reputation: 1372

TextBox has a Binding in it's TextProperty and when you set TextBox's DataContext, TextBox will update it's source (viewmodel.Text) , no matter which type of the UpdateSourceTrigger.

It's said that the first output in viewmodel

"ViewModel.Text (old value=<null>, new value=)"

is not triggered by UpdateSourceTrigger=PropertyChanged.

It's just a process of init:

private string _Text;
public string Text
{
    get { return _Text; }
    set
    {
        Debug.Print(
           "ViewModel.Text (old value=" + (_Text ?? "<null>") +
           ", new value=" + (value ?? "<null>") + ")");
        _Text = value;
    }
}

Because it's not triggered by UpdateSourceTrigger=PropertyChanged, the viewmodel will not know the value of TextBox.Text.

When you type "Y",the trigger of PropertyChanged will working,so the viewmodel read text of TextBox.

Upvotes: 0

Nitin Purohit
Nitin Purohit

Reputation: 18580

Here you will have to UpdateSource like below:

 private void Button1_Click(object sender, RoutedEventArgs e)
   {

      Debug.Print("'Set DataContext' button clicked");
      tb.DataContext = _vm;
      var bindingExp = tb.GetBindingExpression(TextBox.TextProperty);
      bingExp.UpdateSource();
   }

Upvotes: 0

Related Questions