Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I format a decimal bound to TextBox without angering my users?

I'm trying to display a formatted decimal in a TextBox using data binding in WPF.

Goals

Goal 1: When setting a decimal property in code, display 2 decimal places in the TextBox.

Goal 2: When a user interacts with (types in) the TextBox, don't piss him/her off.

Goal 3: Bindings must update source on PropertyChanged.

Attempts

Attempt 1: No formatting.

Here we're starting nearly from scratch.

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

Violates Goal 1. SomeDecimal = 4.5 will show "4.50000" in the TextBox.

Attempt 2: Use StringFormat in the Binding.

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

Violates Goal 2. Say SomeDecimal is 2.5, and the TextBox is displaying "2.50". If we select all and type "13.5" we end up with "13.5.00" in the TextBox because the formatter "helpfully" inserts decimals and zeros.

Attempt 3: use Extended WPF Toolkit's MaskedTextBox.

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

Assuming I'm reading the documentation correctly, the mask ##0.00 means "two optional digits, followed by a required digit, a decimal point, and two more required digits. This forces me to say "the largest possible number that can go into this TextBox is 999.99" but let's say I'm ok with that.

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

Violates Goal 2. The TextBox starts with ___.__ and selecting it and typing 5.75 yields 575.__. The only way to get 5.75 is to select the TextBox and type <space><space>5.75.

Attempt 4: use Extended WPF Toolkit's DecimalUpDown spinner.

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

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

Violates Goal 3. DecimalUpDown happily ignores UpdateSourceTrigger=PropertyChanged. One of the Coordinators on the Extended WPF Toolkit Codeplex page suggests a modified ControlTemplate at http://wpftoolkit.codeplex.com/discussions/352551/. This satisfies Goal 3, but violates Goal 2, exhibiting the same behavior as in Attempt 2.

Attempt 5: Using style datatriggers, only use formatting if user is not editing.

Binding to a double with StringFormat on a TextBox

Even if this one satisfied all three goals, I wouldn't want to use it. (A) Textboxes become 12 lines each instead of 1, and my application contains many, many textboxes. (B) All my textboxes already have a Style attribute which points to a global StaticResource which sets Margin, Height, and other things. (C) You may have noticed the code below sets the binding Path twice, which violates the DRY principal.

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

All these uncomfortable things aside...

Violates Goal 2. First, clicking on a TextBox which displays "13.50" suddenly makes it display "13.5000", which is unexpected. Second, if starting with a blank TextBox, and I type "13.50", the TextBox will contain "1350". I can't explain why, but pressing the period key doesn't insert decimals if the cursor is at the right end of the string in the TextBox. If the TextBox contains a string with length > 0, and I reposition the cursor to anywhere except the right end of the string, I can then insert decimal points.

Attempt 6: Do it myself.

I'm about to embark on a jouney of pain and suffering, by either subclassing TextBox, or creating an attached property, and writing the formatting code myself. It will be full of string manipulation, and cause substantial hairloss.


Does anyone have any suggestions for formatting decimals bound to textboxes that satisfies all the above goals?

like image 487
epalm Avatar asked Jul 13 '12 20:07

epalm


People also ask

How do I change decimal format?

To convert a percentage to a decimal, divide by 100. So 25% is 25/100, or 0.25. To convert a decimal to a percentage, multiply by 100 (just move the decimal point 2 places to the right). For example, 0.065 = 6.5% and 3.75 = 375%.

How do I get 6 decimal places in C#?

Use N6 as the numeric format string.

What is the default value of maxFractionDigits while using the decimal pipe?

Default is 0. maxFractionDigits : The maximum number of digits after the decimal point. Default is 3.

What is decimal pipe in angular?

The DecimalPipe is used to format a value/ number as per the required decimal digits and locale information. The name of the pipe is number.


1 Answers

Try to resolve that on ViewModel level. That it:

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}" /> 

Code behind:

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; } 
like image 135
Vladimir Dorokhov Avatar answered Sep 21 '22 12:09

Vladimir Dorokhov