⛓️

WPFでTextBoxにdoubleをBindingする

2021/12/28に公開1

Qiita を退会するにあたって記事の移植をおこないます。
元は2020年02月15日に投稿しました。
https://qiita.com/naminodarie/items/131a534bfe1a5bca1e5f


WPFでTextBoxにdoubleをBindingすると

  • 文字の末尾に小数点を入力できない
  • 1.002の2を消すと1.00ではなく1になる

などの動作になります。

これはBindingしている値をTextBoxにも即反映させるという動作になっているからです。

1.002の末尾の2を消した際の動作は下記のようになります。

  1. Text1.00に更新
  2. Binding Sourceのdoubleが(double)1に更新
  3. Text1に更新

雑な解決

起動時に

App.xaml.cs
System.Windows.FrameworkCompatibilityPreferences
                          .KeepTextBoxDisplaySynchronizedWithTextProperty = false;

と設定すれば解決です。
Bindingしている値をTextBoxにも即反映させないようにします。

.NET Framework 4.5以前はこれがデフォルトです。

解決

Textプロパティを更新する前に別のプロパティでチェックを行うことで期待する動作を実現します。

Textは設定せずにDoubleTextのみを使います。

<myControl:DoubleTextBox DoubleText="{Binding Double1, UpdateSourceTrigger=PropertyChanged}" />
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

public class DoubleTextBox : TextBox
{
    public string DoubleText
    {
        get => (string)GetValue(DoubleTextProperty);
        set => SetValue(DoubleTextProperty, value);
    }
    public static readonly DependencyProperty DoubleTextProperty =
          DependencyProperty.Register(
              nameof(DoubleText),
              typeof(string),
              typeof(DoubleTextBox),
              new FrameworkPropertyMetadata(
                  string.Empty,
                  FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal,
                  new PropertyChangedCallback(OnDoubleTextChanged),
                  null,
                  true,
                  UpdateSourceTrigger.LostFocus));

    private static void OnDoubleTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBox textBox)
        {
            var currentText = textBox.Text;
            var newText = (string)e.NewValue;
            if (currentText == newText)
                return;
            if (
                double.TryParse(currentText, out var currentDouble) &&
                double.TryParse(newText, out var newDouble) &&
                currentDouble == newDouble
                )
                return;

            textBox.Text = newText;
        }
    }

    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        base.OnTextChanged(e);
        this.DoubleText = this.Text;
    }
}

動作の解説

これによって、1.002の末尾の2を消した際の動作は下記のようになります。

  1. Text1.00に更新
  2. DoubleText1.00に更新
  3. Binding Sourceのdoubleが(double)1に更新
  4. DoubleText1に更新
  5. 11.00はともにdouble値として等しいのでTextは更新されない

Textが更新された場合の動作

キーの入力などでTextが更新されたときはその値がそのままDoubleTextに反映されます。
Binding Sourceへも同様に反映されます。

DoubleText(あるいはBinding Source)が更新された場合の動作

基本的にはTextに反映されます。
ただし、DoubleTextTextをそれぞれdoubleに変換して同じ値になる場合はTextを更新しません。

GitHubで編集を提案

Discussion

KZRNMKZRNM

string とバインドしつつ変換をかけるのでも良さそう。

<TextBox Text="{Binding Double2Text,ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}" />
public class MainViewModel : INotifyPropertyChanged
{
    public double _Double2 = 0.1;
    public double Double2
    {
        get => _Double2;
        set
        {
            if (SetProperty(ref _Double2, value))
            {
                if (SetProperty(ref _Double2Text, value.ToString(), nameof(Double2Text)))
                {

                }
            }
        }
    }
    public string _Double2Text = "0.1";
    public string Double2Text
    {
        get => _Double2Text;
        set
        {
            double.Parse(value);
            if (SetProperty(ref _Double2Text, value))
            {
                if (SetProperty(ref _Double2, double.Parse(value), nameof(Double2)))
                {

                }
            }
        }
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(storage, value))
        {
            storage = value;
            RaisePropertyChanged(propertyName);
            return true;
        }
        return false;
    }
    #endregion INotifyPropertyChanged
}