[.net] BindingMode.OneWayToSource resetting the target value?

Started by
3 comments, last by itachi 14 years, 10 months ago
I had a strange bug in my code and found that BindingMode.OneWayToSource doesn't quite work the way I expected. Maybe someone can help me understand what is happening. I have 2 DependencyObjects that will have their properties bound, and a converter to convert between the two:

class TestObjectBool : DependencyObject {
    public static readonly DependencyProperty BoolProperty = DependencyProperty.Register(
        "Bool", typeof(bool), typeof(TestObjectBool)
    );
    public bool Bool {
        get { return (bool)this.GetValue(BoolProperty); }
        set { this.SetValue(BoolProperty, value); }
    }
}

class TestObjectString : DependencyObject {
    public static readonly DependencyProperty StringProperty = DependencyProperty.Register(
        "String", typeof(string), typeof(TestObjectString)//, new UIPropertyMetadata("nope")
    );
    public string String {
        get { return (string)this.GetValue(StringProperty); }
        set { this.SetValue(StringProperty, value); }
    }
}

class Converter : IValueConverter {
    object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        return (bool)value ? "yep" : "nope";
    }
    object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        return value.ToString() == "yep";
    }
}

When I bind the Bool and String properties of these objects using the converter, it seems that the value of the target property is reset when binding starts:

[TestMethod]
public void OneWayToSourceTest() {

    TestObjectBool o1 = new TestObjectBool();
    TestObjectString o2 = new TestObjectString() { String = "nope" };

    Binding b = new Binding("Bool") {
        Source = o1,
        Mode = BindingMode.OneWayToSource,
        Converter = new Converter()
    };
    BindingOperations.SetBinding(o2, TestObjectString.StringProperty, b); //<- Converter throws exception because o2.String is null
			
    o2.String = "yep";
    Assert.IsTrue(o1.Bool);
}

When I set BindingMode to TwoWay, everything works as expected. Shouldn't OneWayToSource just work the same way here? Also, when I set a default value on StringProperty everything works as expected. Unfortunately in my real code setting a default value doesn't make any sense. Why is o2.String set to null when I set the Binding?
Advertisement
I'm still in the process of learning WPF myself, so I was curious and decided to play around with this. Here's what I think is happening:

Before the binding is set, the String dependency property just has a simple backing field for the o2 instance. Setting a binding, however, changes it from a backing field to a BindingExpression which links it to the binding source. If you try BindingOperations.GetBindingExpression() before and after setting the binding, you'll notice it's null before, and an instance after. Doing this appears to clobber any value that may have been there already. When you think about it, this makes perfect sense for every type of binding except OneWayToSource.

Even for OneWayToSource bindings though, it still somewhat makes sense when you realize that bindings are usually set immediately in the constructor, either through XAML or in code. This usually happens before the property is ever used at all, so in most situations it's probably never noticed.
Quote:When I set BindingMode to TwoWay, everything works as expected.

I'm not sure that it does. If you set a value changed callback on the String property, then use a TwoWay binding, you'll notice that it immediately pulls in the value from its binding source (the Bool property in this case). Since the default value for bools is false, the converter will yield "nope" which is the same value you're setting on String right off the bet. If you instead set string to "yep" and try the two way binding, it still comes out as "nope" after the binding is set.

I'm not sure that there's anything you can do about this except either setting your bindings before using the property, or caching the value, setting the binding, then setting the value back again.
Quote:
When you think about it, this makes perfect sense for every type of binding except OneWayToSource.

Even for OneWayToSource bindings though, it still somewhat makes sense when you realize that bindings are usually set immediately in the constructor, either through XAML or in code. This usually happens before the property is ever used at all, so in most situations it's probably never noticed.


I agree, for binding in XAML it probably makes no real difference and for "regular" bindings it makes sense, too. In my actual code the bindings in both directions are created and destroyed dynamically, and no XAML is involved. My real converter can handle null values but they do mean something inside the code, so in this case I receive two values (first null, then the real value) when only one is expected.

Quote:
Quote:
When I set BindingMode to TwoWay, everything works as expected.

I'm not sure that it does. If you set a value changed callback on the String property, then use a TwoWay binding, you'll notice that it immediately pulls in the value from its binding source (the Bool property in this case). Since the default value for bools is false, the converter will yield "nope" which is the same value you're setting on String right off the bet. If you instead set string to "yep" and try the two way binding, it still comes out as "nope" after the binding is set.


You're right, TwoWay doesn't quite do what I need it to in this case. The target is immediately updated from the source and that's exactly what you'd expect. My expectation for OneWayToSource was that it would work exactly the same as if I were creating it as a OneWay Binding with source and target reversed, but that doesn't seem to be the case.

I looked into BindingOperation.SetBinding in Reflector and it's a little too much code for me to go through right now, but it definitely calls SetValue on the target (which in my case is really the source).

Anyway, the issue is easy enough to work around (for now I will just reverse source and target and use OneWay in the cases where I need OneWayToSource).

Thanks for your time, your insights gave me a different perspective. Maybe I'll take another look at it later, but now I really need to get some work done... :)
Coincidentally enough, I've just been confronted with this in my app less than a day after investigating. Unfortunately, in my case, I absolutely have to use OneWayToSource, since the source is not a dependency object. Here's a clevar hack that I just came up with to get around it for my scenario; it may or may not be useful to you:
class SuppressInitialBindingValidationRule : ValidationRule{	bool _IsFirstUpdate;	public SuppressInitialBindingValidationRule()	{		_IsFirstUpdate = true;	}	public override ValidationResult Validate(object value, CultureInfo cultureInfo)	{		if (_IsFirstUpdate)		{			_IsFirstUpdate = false;			return new ValidationResult(false, "Initial binding ignored.");		}		else		{			return new ValidationResult(true, null);		}	}}

Instantiate one of these and add it as a validation rule to the binding. It will force the initial push that occurs when you first set the binding to fail and the value will not reach the source. The caveat is that it will also trip the error template for the control, but that can probably be dealt with as well somehow. In my case, I'll probably be setting the value to itself again immediately, which will reset the template. Perhaps there's another, cleaner way...

EDIT:
Using Validation.ClearInvalid on the binding expression after setting the binding clears the validation error and prevents the error template from kicking in.

[Edited by - JPatrick on May 29, 2009 2:18:27 PM]
Very nice! Reversing source and target turned out to be the best solution for my needs because it kept my converters simple (I only use the Convert methods and know there's a bug if ConvertBack is ever called) but it's really good to know there's a simple solution.

This topic is closed to new replies.

Advertisement