17

I'm trying to bind TextBox to double property of some object with UpdateSourceTrigger=PropertyChanged. The goal is to immediately during editing validate entered value to be in allowed range (and display an error if not). I want to implement validation on Model level, i.e. via IDataErrorInfo.

All works great when I bind to int property, but if property is double then a frustrating editing behavior appears: after erasing last significant digit in fractional part of number - the decimal separator is automatically erased (with all possible fractional zeroes). For example, after erasing digit '3' from number '12.03' the text is changed to '12' instead of '12.0'.

Please, help.

Here is the sample code:

MainWindow.xaml:

<Window x:Class="BindWithValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="80" Width="200" WindowStartupLocation="CenterOwner">

  <StackPanel>
    <TextBox Width="100" Margin="10" Text="{Binding DoubleField, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
      <TextBox.Style>
        <Style TargetType="TextBox">
          <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
              <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </TextBox.Style>
    </TextBox>
  </StackPanel>
</Window>

MainWindow.xaml.cs:

namespace BindWithValidation
{
  public partial class MainWindow : Window
  {
    private UISimpleData _uiData = new UISimpleData();

    public MainWindow()
    {
      InitializeComponent();
      DataContext = _uiData;
    }
  }
}

UISimpleData.cs:

namespace BindWithValidation
{
  public class UISimpleData : INotifyPropertyChanged, IDataErrorInfo
  {
    private double _doubleField = 12.03;

    public double DoubleField
    {
      get
      {
        return _doubleField;
      }
      set
      {
        if (_doubleField == value)
          return;

        _doubleField = value;
        RaisePropertyChanged("DoubleField");
      }
    }

    public string this[string propertyName]
    {
      get
      {
        string validationResult = null;
        switch (propertyName)
        {
          case "DoubleField":
          {
            if (DoubleField < 2 || DoubleField > 5)
              validationResult = "DoubleField is out of range";
            break;
          }

          default:
            throw new ApplicationException("Unknown Property being validated on UIData");
        }

        return validationResult;
      }
    }

    public string Error { get { return "not implemented"; } }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string property)
    {
      if ( PropertyChanged != null )
        PropertyChanged(this, new PropertyChangedEventArgs(property)); 
    }
  }
}
tmdavison
  • 64,360
  • 12
  • 187
  • 165
arudoy
  • 561
  • 1
  • 6
  • 8
  • I'd imagine this is to do with formatting - since 12 is equivalent to 12.00, have you tried using StringFormat on binding? – Charleh Jun 27 '12 at 09:38
  • Yes, I've tried, but don't like how editing works with it. StringFormat is good for presenting, but during editing I would like to avoid it. – arudoy Jun 27 '12 at 13:27

6 Answers6

11

The behavior of binding float values to a textbox has been changed from .NET 4 to 4.5. With .NET 4.5 it is no longer possible to enter a separator character (comma or dot) with ‘UpdateSourceTrigger = PropertyChanged’ by default.

Microsoft says, this (is) intended

If you still want to use ‘UpdateSourceTrigger = PropertyChanged’, you can force the .NET 4 behavior in your .NET 4.5 application by adding the following line of code to the constructor of your App.xaml.cs:

public App()  
{
    System.Windows.FrameworkCompatibilityPreferences
               .KeepTextBoxDisplaySynchronizedWithTextProperty = false;   
}

(Sebastian Lux - Copied verbatim from here)

Benjol
  • 63,995
  • 54
  • 186
  • 268
10

I realize I'm a little late to the party but I found a (I think) rather clean solution to this problem.

A clever converter that remembers the last string converted to double and returns that if it exists should do everything you want.

Note that when the user changes the contents of the textbox, ConvertBack will store the string the user input, parse the string for a double, and pass that value to the view model. Immediately after, Convert is called to display the newly changed value. At this point, the stored string is not null and will be returned.

If the application instead of the user causes the double to change only Convert is called. This means that the cached string will be null and a standard ToString() will be called on the double.

In this way, the user avoids strange surprises when modifying the contents of the textbox but the application can still trigger a change.

public class DoubleToPersistantStringConverter : IValueConverter
{
    private string lastConvertBackString;

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is double)) return null;

        var stringValue = lastConvertBackString ?? value.ToString();
        lastConvertBackString = null;

        return stringValue;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is string)) return null;

        double result;
        if (double.TryParse((string)value, out result))
        {
            lastConvertBackString = (string)value;
            return result;
        }

        return null;
    }
}
Jas Laferriere
  • 804
  • 10
  • 12
  • 2
    This is a good solution. However, it will mess things up when calling NotifyPropertyChanged at the same time on more than one TextBoxes using this converter. I suggest passing the property as a parameter for the converter and replacing `lastConvertBackString` by a `Dictionary`. In that way you can remember last string for each property. – Octan Jul 07 '16 at 09:13
4

Tried formatting the value with decimal places?

It may be weird though since you will then always have N decimal places.

<TextBox.Text>
    <Binding Path="DoubleField" StringFormat="{}{0:0.00}" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True"/>
</TextBox.Text>

If having fixed decimal places is not good enough, you may have to write a converter that treats the value as a string and converts it back to a double.

Marino Šimić
  • 7,318
  • 1
  • 31
  • 61
  • 1
    Yes, I've tried. With enabled StringFormat editing works also not very good. For example, with your StringFormat erasing decimal separator with delete key from '12.00' leads to '1200.00' with cursor after '12'. So, it is impossible to erase whole fractional part which could be quite surprising for user. – arudoy Jun 27 '12 at 10:42
  • I've also tried to write my own converter. Conversion from string to double works ok, but there is a problem with back conversion: how could converter know that it should insert decimal separator into string representation of value 12? User could type '12' or '12.' or even '12.0' and these all have value 12 which is passed to converter – arudoy Jun 27 '12 at 10:47
  • This is exactly why the default behavior is doing it how it does. It will be difficult to guess what the visible value should be if the UI has no "state" - maybe a custom control could do the work :/ – Marino Šimić Jun 27 '12 at 10:52
  • May be you have some idea how to implement such control? – arudoy Jun 27 '12 at 11:41
4

The problem is that you are updating your property every time the value changes. When you change 12.03 to 12.0 it is rounded to 12.

You can see changes by providing delay by changing the TextBox in xaml like this

<TextBox Width="100" Margin="10" Text="{Binding DoubleField, UpdateSourceTrigger=PropertyChanged,Delay=500, ValidatesOnDataErrors=True}">

but delay will notify and set the property after the delay time in mili sec. Better use StringFormat like this

<TextBox Width="100" Margin="10" Text="{Binding DoubleField, UpdateSourceTrigger=PropertyChanged,StringFormat=N2, ValidatesOnDataErrors=True}">
Sabyasachi Mishra
  • 1,677
  • 2
  • 31
  • 49
2

I have run into the same problem, and have found a quite simple solution: use a custom validator, which does not return "valid" when the Text ends in "." or "0":

double val = 0;
string tmp = value.ToString();

if (tmp.EndsWith(",") || tmp.EndsWith("0") || tmp.EndsWith("."))
{
    return new ValidationResult(false, "Enter another digit, or delete the last one.");
}
else
{
    return ValidationResult.ValidResult;
}
Digifaktur
  • 247
  • 8
  • 18
1

Try using StringFormat on your binding:

<TextBox Width="100" Margin="10" Text="{Binding DoubleField, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, StringFormat='0.0'}"> 

Not sure if that string format is even right since I've not done one for a while but it's just an example

Charleh
  • 13,749
  • 3
  • 37
  • 57
  • Thanks for your suggestion, but StringFormat is not convenient (see my comment above). If fact, I'm going to apply StringFormat after TextBox will loose focus, but while it has focus I would like to avoid StringFormat – arudoy Jun 27 '12 at 10:50