web-dev-qa-db-ja.com

ユーザーを怒らせることなく、TextBoxにバインドされた10進数をフォーマットするにはどうすればよいですか?

WPFのデータバインディングを使用して、TextBoxで書式設定された10進数を表示しようとしています。

目標

目標1:コードで小数プロパティを設定するとき、TextBoxに小数第2位を表示します。

目標2:ユーザーがTextBoxと対話する(入力する)とき、腹を立てないでください。

目標3:バインディングはPropertyChangedのソースを更新する必要があります。

試み

試行1:フォーマットなし

ここでは、ほぼゼロから始めています。

<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" />

目標1に違反。SomeDecimal = 4.5は、TextBoxに「4.50000」を表示します。

試行2:バインディングでStringFormatを使用します。

<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged, StringFormat=F2}" />

目標2に違反しています。SomeDecimalが2.5で、TextBoxに「2.50」が表示されているとします。すべてを選択し、「13.5」と入力すると、フォーマッターが小数とゼロを「便利に」挿入するため、TextBoxに「13.5.00」が表示されます。

試行3:Extended WPF ToolkitのMaskedTextBoxを使用します。

http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox

ドキュメントを正しく読んでいると仮定すると、マスク## 0.00は「2つのオプションの数字と、それに続く必要な数字、小数点、さらに2つの必要な数字」を意味します。このTextBoxは999.99 "ですが、それでいいとしましょう。

<xctk:MaskedTextBox Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" Mask="##0.00" />

目標2に違反します。TextBoxは___.__で始まり、選択して5.75と入力すると575.__になります。 5.75を取得する唯一の方法は、TextBoxを選択して<space><space>5.75と入力することです。

試み4:Extended WPF ToolkitのDecimalUpDownスピナーを使用します。

http://wpftoolkit.codeplex.com/wikipage?title=DecimalUpDown

<xctk:DecimalUpDown Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" FormatString="F2" />

目標3に違反します。DecimalUpDownは、UpdateSourceTrigger = PropertyChangedを喜んで無視します。 Extended WPF Toolkit Codeplexページのコーディネーターの1人が http://wpftoolkit.codeplex.com/discussions/352551/ で修正されたControlTemplateを提案しています。これは目標3を満たしますが、目標2に違反し、試行2と同じ動作を示します。

試行5:スタイルデータトリガーを使用し、ユーザーが編集していない場合にのみフォーマットを使用します。

TextBoxでStringFormatを使用してdoubleにバインド

これで3つの目標がすべて満たされたとしても、それは使いたくありません。 (A)テキストボックスはそれぞれ1行ではなく12行になり、アプリケーションには多数のテキストボックスが含まれます。 (B)すべてのテキストボックスには、Margin、Height、その他を設定するグローバルStaticResourceを指すStyle属性が既にあります。 (C)以下のコードがバインディングパスを2回設定することに気付いたかもしれません。これはDRYプリンシパルに違反しています。

<TextBox>
    <TextBox.Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Text" Value="{Binding Path=SomeDecimal, StringFormat=F2}" />
            <Style.Triggers>
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="Text" Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

これらすべての不快なことはさておき...

目標2に違反する。最初に、「13.50」を表示するTextBoxをクリックすると、突然「13.5000」が表示されますが、これは予想外です。第二に、空のTextBoxで開始し、「13.50」と入力すると、TextBoxには「1350」が含まれます。理由は説明できませんが、カーソルがTextBoxの文字列の右端にある場合、ピリオドキーを押しても小数が挿入されません。 TextBoxに長さ> 0の文字列が含まれ、カーソルを文字列の右端以外の任意の場所に再配置した場合、小数点を挿入できます。

試行6:自分でやる

TextBoxをサブクラス化するか、添付プロパティを作成し、自分で書式設定コードを記述することにより、痛みと苦しみの喜びに乗り出します。それは文字列操作でいっぱいであり、かなりの抜け毛を引き起こします。


上記の目標をすべて満たす、テキストボックスにバインドされた小数をフォーマットするための提案はありますか?

52
epalm

ViewModelレベルで解決してください。そのこと:

public class FormattedDecimalViewModel : INotifyPropertyChanged
    {
        private readonly string _format;

        public FormattedDecimalViewModel()
            : this("F2")
        {

        }

        public FormattedDecimalViewModel(string format)
        {
            _format = format;
        }

        private string _someDecimalAsString;
        // String value that will be displayed on the view.
        // Bind this property to your control
        public string SomeDecimalAsString
        {
            get
            {
                return _someDecimalAsString;
            }
            set
            {
                _someDecimalAsString = value;
                RaisePropertyChanged("SomeDecimalAsString");
                RaisePropertyChanged("SomeDecimal");
            }
        }

        // Converts user input to decimal or initializes view model
        public decimal SomeDecimal
        {
            get
            {
                return decimal.Parse(_someDecimalAsString);
            }
            set
            {
                SomeDecimalAsString = value.ToString(_format);
            }
        }

        // Applies format forcibly
        public void ApplyFormat()
        {
            SomeDecimalAsString = SomeDecimal.ToString(_format);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

[〜#〜] sample [〜#〜]

Xaml:

<TextBox x:Name="tb" Text="{Binding Path=SomeDecimalAsString, UpdateSourceTrigger=PropertyChanged}" />

コードビハインド:

public MainWindow()
{
    InitializeComponent();
    FormattedDecimalViewModel formattedDecimalViewModel = new FormattedDecimalViewModel { SomeDecimal = (decimal)2.50 };
    tb.LostFocus += (s, e) => formattedDecimalViewModel.ApplyFormat(); // when user finishes to type, will apply formatting
    DataContext = formattedDecimalViewModel;
}
5

StringFormat={}{0:0.00}を使用するときにユーザーカーソルを小数点の後に移動する次のカスタム動作を作成しました。

目標2に違反しています。SomeDecimalが2.5で、TextBoxに「2.50」が表示されているとします。すべてを選択して「13.5」と入力すると、TextBoxに「13.5.00」が表示されます。これは、フォーマッタが「小切手」で小数点とゼロを挿入するためです。

ユーザーがを押したときにユーザーカーソルを小数点以下に移動するカスタム動作を使用して、これを回避しました。キー:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace GUI.Helpers.Behaviors
{
    public class DecimalPlaceHotkeyBehavior : Behavior<TextBox>
    {
        #region Methods
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.PreviewKeyDown -= AssociatedObject_PreviewKeyDown;
        }

        protected override Freezable CreateInstanceCore()
        {
            return new DecimalPlaceHotkeyBehavior();
        }
        #endregion

        #region Event Methods
        private void AssociatedObject_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            if (e.Key == System.Windows.Input.Key.OemPeriod || e.Key == System.Windows.Input.Key.Decimal)
            {
                var periodIndex = AssociatedObject.Text.IndexOf('.');
                if (periodIndex != -1)
                {
                    AssociatedObject.CaretIndex = (periodIndex + 1);
                    e.Handled = true;
                }
            }
        }
        #endregion

        #region Initialization
        public DecimalPlaceHotkeyBehavior()
            : base()
        {
        }
        #endregion
    }
}

次のように使用します。

<TextBox xmlns:Behaviors="clr-namespace:GUI.Helpers.Behaviors" 
         xmlns:i="clr-namespace:System.Windows.Interactivity;Assembly=System.Windows.Interactivity" 
         Text="{Binding Value, UpdateSourceTrigger=PropertyChanged, StringFormat={}{0:0.00}}">
        <i:Interaction.Behaviors>
            <Behaviors:DecimalPlaceHotkeyBehavior></Behaviors:DecimalPlaceHotkeyBehavior>
        </i:Interaction.Behaviors>
</TextBox>
2

WPF Extended Tookit Masked TextBoxを試して、入力マスクを実装します。 http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox

例:

<toolkit:MaskedTextBox Mask="(000) 000-0000" Value="(555) 123-4567" 
IncludeLiterals="True" />
1
Xcalibur37