TwoWay binding trap on Windows Store apps

Imagine having a TextBox bound to a viewmodel’s property in a TwoWay mode. In between a converter that does possibly an asymmetric conversion – in this case the text is uppercased (it should do for this demo).

Like this:

<StackPanel>
    <StackPanel.Resources>
        <local:UppercaseConverter x:Key="cnv" />
    </StackPanel.Resources>
    <TextBox Text="{Binding Text, Mode=TwoWay, Converter={StaticResource cnv}}" Margin="0,60,0,0" />
    <TextBlock Text="{Binding Text, Mode=OneWay}" />
    <Button Content="Test" Click="Button_Click" />
</StackPanel>

Below is an additional TextBlock for feedback and a button that assigns a value to the property bound.Here is the WPF converter (Windows Store version has a slightly different method signature)

public class UppercaseConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return null;
        else
            return ((string)value).ToUpper();
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value;
    }
}

and the viewmodel with a single property and INPC implementation:

public class Tubo : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string text;
    public string Text
    {
        get
        {
            return text;
        }
        set
        {
            if (text != value)
            {
                text = value;
                OnPropertyChanged("Text");
            }
        }
    }

    protected virtual void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, 
                new  PropertyChangedEventArgs(name));
    }
}

Finally the code behind the XAML page:

public partial class MainWindow : Window
{
    private Tubo source;
    public MainWindow()
    {
        InitializeComponent();
        source = new Tubo();
        DataContext = source;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        source.Text = "Tubo";
    }
}
Everything is pretty much straightforward. The instance of Tubo is created and assigned as DataContext. On button click a value is assigned to Tubo’s Text property. Binding picks up the PropertyChanged notifications and displays the UppercaseConverter converted value in TextBox and a raw value in TextBlock.

Now the question. What will be the two values displayed in a WPF application and in a Windows Store application after pressing the Test button? Can you guess?

I’d expect TUBO (uppercased) in TextBox and tubo in TextBlock since there is no conversion applied and user didn’t change anything.

And that’s the case with the WPF application. But the surprise result comes from Windows Store application. It shows both values as uppercased. It gets even worse, this behavior isn’t consistent, more on this later.

What actually happens is that Windows Store app (who invented that name anyway) TwoWay binding does both directions on value change: it picks the modified value from viewmodel and immediately stores it back into viewmodel. So without any user intervention the uppercased value is stored in viewmodel. Nasty I’d say.

But it gets worse. This behavior is only for visible UIElements. In other words, if the TextBox isn’t visible (Collapsed) at property change time it will act like expected, just pick the value from viewmodel but it won’t write it back to viewmodel.

The combination of both might result in edge case bugs one would really not expect and might not be caught easily.

Luckily there is a workaround. The source updating has to be done manually by

  1. Setting Binding’s UpdateSourceTrigger=Explicit
  2. Implementing own logic when to update the source (this might be tricky)

Here is how one does update the source in an overriden TextBox (better to create a Behavior though).

BindingExpression be = GetBindingExpression(TextProperty);
if (be != null)
{
    be.UpdateSource();
}