Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding to double field with validation

Tags:

binding

wpf

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)); 
    }
  }
}
like image 251
arudoy Avatar asked Jun 27 '12 09:06

arudoy


4 Answers

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)

like image 118
Benjol Avatar answered Oct 17 '22 10:10

Benjol


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;
    }
}
like image 20
Jas Laferriere Avatar answered Oct 17 '22 12:10

Jas Laferriere


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.

like image 4
Marino Šimić Avatar answered Oct 17 '22 11:10

Marino Šimić


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}">
like image 4
Sabyasachi Mishra Avatar answered Oct 17 '22 12:10

Sabyasachi Mishra