Связывание с двойным полем с проверкой

Я пытаюсь привязать свойство TextBox к double некоторого объекта с помощью UpdateSourceTrigger=PropertyChanged. Цель состоит в том, чтобы сразу во время редактирования подтвердить введенное значение в разрешенном диапазоне (и отобразить ошибку, если нет). Я хочу выполнить проверку на уровне модели, то есть через IDataErrorInfo.

Все работает отлично, когда я привязываюсь к свойству int, но если свойство двойное, возникает неприятное редактирование: после стирания последней значащей цифры в дробной части числа - десятичный разделитель автоматически стирается (со всеми возможными дробными нулями). Например, после стирания цифры "3" из номера "12.03" текст изменяется на "12" вместо "12.0".

Пожалуйста, помогите.

Вот пример кода:

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)); 
    }
  }
}

Ответ 1

Я понимаю, что немного опаздываю на вечеринку, но я нашел (я думаю) довольно чистое решение этой проблемы.

Умный конвертер, который запоминает последнюю строку, преобразованную в double, и возвращает ее, если она существует, должна делать все, что вам нужно.

Обратите внимание, что когда пользователь меняет содержимое текстового поля, ConvertBack будет хранить строку, которую вводит пользователь, анализирует строку для double и передает это значение модели представления. Сразу же после этого Convert вызывается для отображения вновь измененного значения. На этом этапе сохраненная строка не будет нулевой и будет возвращена.

Если приложение вместо пользователя вызывает двойное изменение, вызывается только Convert. Это означает, что кэшированная строка будет нулевой, и в Double будет вызываться стандартный ToString().

Таким образом, пользователь избегает странных неожиданностей при изменении содержимого текстового поля, но приложение все еще может вызвать изменение.

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;
    }
}

Ответ 2

Пробовал форматирование значения с десятичными знаками?

Это может быть странно, но поскольку у вас всегда будет N знаков после запятой.

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

Если фиксированные десятичные разряды недостаточно хороши, вам может потребоваться написать конвертер, который обрабатывает значение в виде строки и преобразует его обратно в double.

Ответ 3

Поведение значений привязки привязки к текстовому полю было изменено от .NET 4 до 4.5. С .NET 4.5 больше невозможно ввести разделитель (запятая или точка) с помощью "UpdateSourceTrigger = PropertyChanged по умолчанию.

Microsoft говорит, что это (есть) предназначено

Если вы все еще хотите использовать 'UpdateSourceTrigger = PropertyChanged, вы может повлиять на поведение .NET 4 в вашем приложении .NET 4.5, добавив следующую строку кода для конструктора вашего App.xaml.cs:

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

(Себастьян Люкс - Скопировано дословно из здесь)

Ответ 4

Проблема в том, что вы обновляете свою собственность каждый раз, когда значение изменяется. Когда вы меняете 12.03 на 12.0, округляется до 12.

Вы можете увидеть изменения, предоставив delay, изменив TextBox в xaml следующим образом

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

но delay будет уведомлять и устанавливать свойство после времени задержки в миллисекундах. Лучше использовать StringFormat как этот

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

Ответ 5

Я столкнулся с той же проблемой и нашел довольно простое решение: используйте специальный валидатор, который не возвращает "valid", когда текст заканчивается на ".". или "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;
}

Ответ 6

Попробуйте использовать StringFormat для привязки:

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

Не уверен, что этот строковый формат даже прав, поскольку я не проработал какое-то время, но это просто пример