dev hedgehog
dev hedgehog

Reputation: 8791

Binding PropertyChanged Dilemma

Oh lord is monday again and I feel like all my knowledge about wpf has been deleted just like that.

I thought when Binding in Mode PropertyChanged that the Source will be only updated when the Target property was changed and not all the time.

Here is an example where Binding keeps updating the Source even though the Target property hasn't been changed. Why?

Btw, I am in .NET 4.0

  <StackPanel>
    <TextBox x:Name="tbx1" Text="{Binding Txt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    <Button Content="Change Text" Click="OnClick" />
  </StackPanel>

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

    private void OnClick(object sender, RoutedEventArgs e)
    {
        tbx1.Text = "hello";
    }
}

public class ViewModel 
{

    private string txt;

    public string Txt
    {
        get { return txt; }
        set { txt = value; Console.WriteLine("Txt Setter Called!");}
    }
}

Everytime I click on Button the setter of Txt is being called. Why? The value was not changed.

GetHashCode() method returns same results.

What am I missing??? :-)

Upvotes: 0

Views: 137

Answers (4)

nmclean
nmclean

Reputation: 7724

As you expect, there is no actual "property change" happening, which can be confirmed with:

using System.ComponentModel;

var descriptor = DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox));

descriptor.AddValueChanged(tbx1, (s, e) => Console.WriteLine("tbx1 changed"));

"tbx1 changed" will only appear once.

If the source is a dependency property, it doesn't change either. Try adding another textbox and using it as the source instead of the viewmodel:

<TextBox Name="tbx2" />
<TextBox Name="tbx1" Text="{Binding ElementName=tbx2, Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

Handler:

descriptor.AddValueChanged(tbx2, (s, e) => Console.WriteLine("tbx2 changed"));

Again, only one change.

So yes, the trigger is not accurately named. A reason for this behavior may be to ensure that a property set always triggers a binding converter, because a ConvertBack could return a different value even with the same input.

In any event, both target and source need to take some responsibility in determining what a "change" is. After all, if it's a true two-way binding, then we should be allowed to implement OnClick this way with exactly the same effect:

tbx1.DataContext.Txt = "hello";

So just make sure your property setters always check for an actual change before proceeding (as dependency properties do).

Upvotes: 1

Peter Hansen
Peter Hansen

Reputation: 8907

Well, that's the way it's supposed to work, but the name might be a bit misleading.

The source value is being updated when the target property has been set, not necessarily when the value has changed, as the name suggests.

You can observe the same behavior by using fx. a CheckBox. Setting the IsChecked property to true over and over will also trigger a source update even though the target value does not change.

So the binding system does not compare the actual values before triggering an update, it just cares about whether the target property was set or not.

Your example extended with a CheckBox:

XAML:

<StackPanel>
    <TextBox x:Name="tbx1" Text="{Binding Txt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    <CheckBox x:Name="chk1" IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <Button Content="Change Text" Click="OnClick" />
</StackPanel>

Code-behind:

public partial class MainWindow
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new ViewModel();
    }

    private void OnClick(object sender, RoutedEventArgs e)
    {
        tbx1.Text = "hello";
        chk1.IsChecked = true;
    }
}

public class ViewModel
{
    private string txt;
    public string Txt
    {
        get { return txt; }
        set { txt = value; Console.WriteLine("Txt Setter Called!"); }
    }

    private bool isChecked;
    public bool IsChecked
    {
        get { return isChecked; }
        set { isChecked = value; Console.WriteLine("IsChecked Setter Called!"); }
    }
}

Upvotes: 2

apc
apc

Reputation: 5566

You could change you setter to check if the new value is different from the old.

 public string Txt
{
    get { return txt; }
    set { 
          if (txt == value) return;
          txt = value;               
          Console.WriteLine("Txt Setter Called!");
        }
}

Upvotes: 0

Sankarann
Sankarann

Reputation: 2665

Change your BindingMode to OneWay or OneWayToSource.. your problem will get solve...

<TextBox x:Name="tbx1" Text="{Binding Txt, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"/>

Upvotes: 0

Related Questions