I'm trying to display a formatted decimal in a TextBox using data binding in WPF.
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.
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?
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%.
Use N6 as the numeric format string.
Default is 0. maxFractionDigits : The maximum number of digits after the decimal point. Default is 3.
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.
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; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With