Как я могу форматировать десятичную привязку к TextBox, не зная моих пользователей?
Я пытаюсь отобразить отформатированное десятичное число в TextBox, используя привязку данных в WPF.
Цели
Цель 1: при установке десятичного значения в коде отобразите 2 десятичных знака в текстовом поле.
Цель 2: Когда пользователь взаимодействует с (вводит) в TextBox, не срывает его/ее.
Цель 3: привязки должны обновлять источник в PropertyChanged.
Попытки
Попытка 1: Без форматирования.
Здесь мы начинаем почти с нуля.
<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" />
Нарушает цель 1. SomeDecimal = 4.5
отобразит "4.50000" в текстовом поле.
Попытка 2: Использовать StringFormat в привязке.
<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged, StringFormat=F2}" />
Нарушает цель 2. Скажите SomeDecimal значение 2.5, а TextBox отображает "2.50". Если мы выберем все и наберем "13.5", мы получим "13.5.00" в TextBox, потому что форматирование "полезно" вставляет десятичные знаки и нули.
Попытка 3: использовать расширенный набор инструментов WPF MaskedTextBox.
http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox
Предполагая, что я правильно читаю документацию, маска ## 0.00 означает "две необязательные цифры, за которой следует требуемая цифра, десятичная точка и еще две требуемые цифры". Это заставляет меня сказать "максимально возможное число, может войти в этот 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: использовать расширенный WPF Toolkit DecimalUpDown spinner.
http://wpftoolkit.codeplex.com/wikipage?title=DecimalUpDown
<xctk:DecimalUpDown Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" FormatString="F2" />
Нарушает цель 3. DecimalUpDown с радостью игнорирует UpdateSourceTrigger = PropertyChanged. Один из координаторов на странице расширенного WPF Toolkit Codeplex предлагает модифицированный контрольный шаблон в http://wpftoolkit.codeplex.com/discussions/352551/. Это удовлетворяет цели 3, но нарушает цель 2, проявляя то же поведение, что и в попытке 2.
Попытка 5: использование стилей datatriggers, используйте только форматирование, если пользователь не редактирует.
Связывание с двойным с StringFormat в TextBox
Даже если бы этот был удовлетворен всем трем целям, я бы не хотел его использовать. (A) Текстовые поля становятся 12 строк каждый вместо 1, а мое приложение содержит много и много текстовых полей. (B) Все мои текстовые поля уже имеют атрибут Style, который указывает на глобальный StaticResource, который устанавливает Margin, Height и другие вещи. (C) Возможно, вы заметили, что приведенный ниже код устанавливает путь привязки дважды, что нарушает принципал 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. Сначала щелчок на TextBox, который отображает "13.50", внезапно отображает "13.5000", что неожиданно. Во-вторых, если начать с пустого TextBox, и я набираю "13.50", TextBox будет содержать "1350". Я не могу объяснить, почему, но нажатие клавиши периода не вставляет десятичные знаки, если курсор находится в правом конце строки в TextBox. Если TextBox содержит строку с длиной > 0, и я перемещаю курсор в любую точку, кроме правого конца строки, тогда я могу вставить десятичные точки.
Попытка 6: сделайте это сама.
Я собираюсь приступить к jouney боли и страдания, либо подклассифицируя TextBox, либо создав прикрепленное свойство, и сам написав код форматирования. Он будет наполнен струнными манипуляциями и вызовет существенную потерю волос.
Есть ли у кого-нибудь предложения по форматированию десятичных знаков, привязанных к текстовым полям, которые удовлетворяют всем вышеперечисленным целям?
Ответы
Ответ 1
Попробуйте разрешить это на уровне 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));
}
}
ОБРАЗЕЦ
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;
}
Ответ 2
Я создал следующее пользовательское поведение для перемещения курсора пользователя после десятичной точки при использовании StringFormat={}{0:0.00}
, что вынуждает десятичное место присутствовать, однако это может вызвать следующую проблему:
Нарушает цель 2. Скажите SomeDecimal значение 2.5, а TextBox отображает "2.50". Если мы выберем все и наберем "13.5", мы получим "13.5.00" в TextBox, потому что форматирование "полезно" вставляет десятичные знаки и нули.
Я взломал это с помощью пользовательского поведения, которое будет перемещать курсор пользователя после десятичной точки, когда они нажимают кнопку. Ключ:
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>
Ответ 3
Попробуйте WPF Extended Tookit Masked TextBox для реализации входной маски:
http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox
Пример:
<toolkit:MaskedTextBox Mask="(000) 000-0000" Value="(555) 123-4567"
IncludeLiterals="True" />