4

I have done a buch of test with Binding Mode = OneWayToSource lately and I still don't know why certain things happen.

As example I set a value on a dependency property in class constructor. Now when Binding is initalizing the Target property gets set to its default value. Means the dependency property gets set to null and I lose the value I initalized in constructor.

Why is that happening? The Binding Mode is not working the way the name describes it. It shall only update the Source and not Target

Here is code:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MyViewModel();
    }

    private void OnClick(object sender, RoutedEventArgs e)
    {
        this.DataContext = new MyViewModel();
    }
}

This is XAML:

<StackPanel>
        <local:MyCustomControl Txt="{Binding Str, Mode=OneWayToSource}"/>
        <Button Click="OnClick"/>
</StackPanel>

This is MyCustomControl:

    public class MyCustomControl : Control
    {
        public static readonly DependencyProperty TxtProperty =
            DependencyProperty.Register("Txt", typeof(string), typeof(MyCustomControl), new UIPropertyMetadata(null));

        static MyCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
        }

        public MyCustomControl()
        {
           this.Txt = "123";
        }

        public string Txt
        {
           get { return (string)this.GetValue(TxtProperty); }

           set { this.SetValue(TxtProperty, value); }
        }
     }

This is ViewModel:

    public class MyViewModel : INotifyPropertyChanged
    {
        private string str;

        public string Str
        {
            get { return this.str; }
            set
            {
                if (this.str != value)
                {
                    this.str = value; this.OnPropertyChanged("Str");
                }
            }
         }

        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null && propertyName != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
     }
ninja hedgehog
  • 405
  • 4
  • 16

2 Answers2

4
this.Txt = "123";

This is replacing your binding with a local value. See dependency property value precedence. You're essentially calling DependencyObject.SetValue when you really want DependencyProperty.SetCurrentValue. In addition, you need to wait until later in the life cycle to do this, otherwise WPF will update Str twice: once with "123" and then again with null:

protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
    this.SetCurrentValue(TxtProperty, "123");
}

If you do this in your user control's constructor, it executes when WPF instantiates it, but is promptly replaced when WPF loads and deserializes and applies your BAML.

Update: Apologies, I misunderstood your exact issue but now have a repro for it, copied below. I was missing the part where you subsequently update the DataContext. I fixed this by setting the current value when the data context changes, but in a separate message. Otherwise, WPF neglects to forward the change onto your new data source.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace SO18779291
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.setNewContext.Click += (s, e) => this.DataContext = new MyViewModel();
            this.DataContext = new MyViewModel();
        }
    }

    public class MyCustomControl : Control
    {
        public static readonly DependencyProperty TxtProperty =
            DependencyProperty.Register("Txt", typeof(string), typeof(MyCustomControl), new UIPropertyMetadata(OnTxtChanged));

        static MyCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
        }

        public MyCustomControl()
        {
            this.DataContextChanged += (s, e) =>
            {
                this.Dispatcher.BeginInvoke((Action)delegate
                {
                    this.SetCurrentValue(TxtProperty, "123");
                });
            };
        }

        public string Txt
        {
            get { return (string)this.GetValue(TxtProperty); }

            set { this.SetValue(TxtProperty, value); }
        }

        private static void OnTxtChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            Console.WriteLine("Changed: '{0}' -> '{1}'", e.OldValue, e.NewValue);
        }
    }

    public class MyViewModel : INotifyPropertyChanged
    {
        private string str;

        public string Str
        {
            get { return this.str; }
            set
            {
                if (this.str != value)
                {
                    this.str = value; this.OnPropertyChanged("Str");
                }
            }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null && propertyName != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

XAML:

<Window x:Class="SO18779291.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SO18779291"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <local:MyCustomControl Txt="{Binding Str, Mode=OneWayToSource}"/>
        <Button x:Name="setNewContext">New Context</Button>
        <TextBlock Text="{Binding Str, Mode=OneWay}"/>
    </StackPanel>
</Window>
Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • OnInitalized gets triggered only once when the control is completely initalized but what if I changed DataContext at runtime? It would again destroy the value of the dependecy property in control. Like I mentioned in my question I did a bunch of test and I also tried your approach with OnInitalized event but as soon the DataContext changes the Binding sets the value of the dependency property back to its default (which is null). That shouldnt happen though because Binding.Mode is OneWayToSource. – ninja hedgehog Sep 13 '13 at 08:53
  • `DataContextChanged += (s, e) => this.SetCurrentValue(TxtProperty, "123");` – Kent Boogaart Sep 13 '13 at 08:56
  • I tried that too and its not working. Try it out yourself. The value stay null. Lets not keep guessing and handling every possible case. I would like to find a good solution or at least reason why is this happening when using OneWayToSource Binding – ninja hedgehog Sep 13 '13 at 09:05
  • @ninja. I did try it - it was not a guess. It works. What version of WPF are you using? – Kent Boogaart Sep 13 '13 at 09:12
  • I am using 4.0 and on DataContextChanged I set current value of the Txt dependency property. The Binding does transfer the value from target to source but right after that the Binding again transfers the null value from target to source. So both are null. For me its not working and I have tried it out too. I am sitting right now in front of that wpf project. – ninja hedgehog Sep 13 '13 at 09:16
  • Try change datacontext by adding a button to the stackpanel and when clicking on it you run following code this.DataContext = new MyViewModel(); See my question I edited the code there. – ninja hedgehog Sep 13 '13 at 09:29
  • @ninja: updated my answer. Your point about adding a button was exactly what I missed. – Kent Boogaart Sep 13 '13 at 09:37
  • To sum up you solved this by using Dispatcher.BeginInvoke. But why is not wpf forwarding the value in first place? Does somebody know why is this happening? Its is a wpf bug? – ninja hedgehog Sep 13 '13 at 09:46
  • btw, I would say this is a limitation of WPF. If the current value of a one-way-to-source binding isn't explicitly set, it doesn't push it to the source, even if the source itself changes. EDIT: yes, I'd say a bug. Will it ever be fixed? Fat chance I'm afraid. – Kent Boogaart Sep 13 '13 at 09:47
2

Binding will work with locally set value only after initialization of target property.

The dependency property gets set to null and I lose the value I initalized in constructor.Why is that happening?

Since InitializeComponent() is missing in your UserControl ctor, and you may set Txt either before or after it, let's consider both cases with the presumption that Txt is initialized inside InitializeComponent(). Here initialization of Txt means it gets assingned the value declared in XAML. If Txt is set locally beforehand, the binding from XAML would replace this value. And if it is set afterwards, the binding would take into account this value whenever it gets its chance to be evaluated, which is the case for both TwoWay and OneWayToSource bindings. (Trigger of binding evaluation will be explained later.)

To prove my theory, I did a test with three elements each with a different mode of binding.

<TextBox Text="{Binding TwoWayStr,Mode=TwoWay}"></TextBox>
<local:UserControl1 Txt="{Binding OneWayToSourceStr, Mode=OneWay}" />
<Button Content="{Binding OneWayStr,Mode=OneWay}"></Button>   

However the result shows that local value is ignored in both cases. Because unlike other elements, by the time InitializeComponent() exits and Initialized event fires, properties of UserControl haven't been initialized yet, including Txt.

  1. Initialization
    • (Begin) Enters InitializeComponent() in Window ctor
    • Text initialized and TwoWay binding attempt to attach binding source
    • TextBox Initialized
    • UserControl Initialized
    • Txt initialized and OneWayToSource binding attempt to attach binding source
    • Content initialized and OneWay binding attempt to attach binding source
    • Button Initialized
    • Window Initialized
    • (End) Exits InitializeComponent() in Window ctor
  2. Loading/Rendering
    • (Begin) Exits Window ctor
    • TwoWay binding attempt to attach binding source if unattached
    • OneWayToSource binding attempt to attach binding source if unattached
    • OneWay binding attempt to attach binding source if unattached
    • Window Loaded
    • TextBox Loaded
    • UserControl Loaded
    • Button Loaded
    • (End) All elements Loaded
  3. Post-loading
    • (Begin) All elements Loaded
    • TwoWay binding attempt to attach binding source if unattached
    • OneWayToSource binding attempt to attach binding source if unattached
    • OneWay binding initial attempt to attach binding source if unattached
    • (End) Window displayed

This special behavior of UserControl that properties are initialized afterwards is discussed in this question. If you use the method provided there, the calling of OnInitialized override along with the firing of Initialized event will be delayed until after all properties are initialized. And if you call BindingOperations.GetBindingExpression(this, MyCustomControl.TxtProperty) in OnInitialized override or in the handler of Initialized, the return value will no longer be null.

At this point, it will be safe to assign the local value. But the binding evaluation will not be triggered immediately to transfer the value, because the binding source(DataContext) is still unavailable, notice the DataContext is not set until after Window initialization. In fact if you check Status property of the returned binding expression, the value is Unattached.

After entering Loading stage, the second attempt to attach binding source will seize the DataContext, then an evaluation will be triggered by this first attachment of binding source where value of Txt (in this case "123") will be transfered to source property Str via setter. And the status of this biniding expression now changes to Active which represents a resolved state of binding source.

If you weren't to use the method mentioned in that question, you can move the local value assignment after InitializeComponent() of Window, into Intialized handler of Window or Loaded handler of Window/UserControl, the result will be the same. Except if it were set in Loaded, local assignment will trigger an immediate evaluation, since binding source is already attached. And the one triggered by first attachment will be instead transferring default value of Txt.

OneWayToSource binding evaluation triggered by change of binding source will pass default value to source property.

What if I changed DataContext at runtime? It would again destroy the value of the dependecy property in control.

From the previous segment, we've already seen two types of triggers for binding evaluation of OneWayToSource binding, one is target property change(if UpdateSourceTrigger of binding is PropertyChanged, which is often default), and the other is first attachment of binding source.

It seems that from the discussion in the accepted answer that you have a second question about why default value is used instead of the "current" value of Txt in the evaluation triggered by binding source changes. It turns out this is the designed behavior of this third type of evaluaiton trigger, which is also confirmed by the 2nd and 3rd answer to this question. By the way, I'm testing this in .Net 4.5, there is a change of evaluation process of OneWayToSource from 4.0 by removing the getter call after setter, but this does not change the "default value" behavior.

As a side note, for both TwoWay and OneWay binding, the evaluation triggered by first attachment and change of binding source behaves exactly the same by calling getter.

Extra: OneWayToSource binding will ignore changes on all levels of path

Another strange behavior of OneWayToSource binding that is perhaps pertaining to the topic is that although it is expected that changes of target property is not listened to, if the binding path contains more than one level, which means the target property is nested, changes to all levels up from the target property is also ignored. For example, if the binding is declared like this Text={Binding ChildViewModel.Str, Mode=OneWayToSource}, a change to the ChildViewModel property will not trigger a binding evaluation, in fact, if you trigger an evaluation by changing Text, Str setter is called on the previous ChildViewModel instance. This behavior makes OneWayToSource deviate more from the other two modes.

P.S.: I know this is an old post. But since these behaviors are still not well documented, I thought it might be helpful to anyone also trying to understand what happens.

ricecakebear
  • 301
  • 4
  • 15