In my TextBox I have;
Text="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, StringFormat={}{0:N2}}"
The binding data is decimal while the textbox formatting should be like 1,000.00
.
My only concern here is that when the TextBox is empty, or if I deleted the value, then the border of the textbox gets red and I get the error message of Value * could not be converted
just below the textbox, this causes by using StringFormat
.
Now, I really don't care if the value is empty because in my database, the default value is zero and I do accept an empty value, and also the textbox accepts only digits.
What I want to know is how can I disable this validation but still be able to use the StringFormat? Second, just in case in the future, I wanted to use the same behavior, how can I change the default error message to something else?
EDIT: As suggested, I tried using a binding converter and DataTrigger to apply the StringFormat, but I still got the error message.
Using Binding Converter
//AmountFormatter.class
public class AmountFormatter : IValueConverter{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture){
decimal amount = (decimal.TryParse(value.ToString(), out decimal n)) ? n : 0; // if it failed to convert into decimal, meaning wrong value, then set default value as 0.
return (amount>0)?string.Format(culture, "{0:N2}", amount):null; //return null, empty if value is less than 1. Maybe the user wants to type new value, so leave the textbox empty.
}
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
return null;
}
}
//xaml layout
<DataTemplate>
<DockPanel LastChildFill="True">
<TextBox x:Name="TextBoxAmount" Text="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource AmountFormatter}}" />
</DockPanel>
</DataTemplate>
//App.xaml
<converters:DecimalOnly x:Key="AmountFormatter" />
Using DataTrigger to apply StringFormat
//AmountFormatter.class
public class AmountFormatter : IValueConverter{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture){
decimal amount = (decimal.TryParse(value.ToString(), out decimal n)) ? n : 0; // if it failed to convert into decimal, meaning wrong value, then set default value as 0.
return (amount>0) //return true or false if amount is greather than zero. Maybe the user wants to type new value, so leave the textbox empty.
}
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
return null;
}
}
//xaml layout
<DataTemplate>
<DockPanel LastChildFill="True">
<TextBox x:Name="TextBoxAmount" />
</DockPanel>
<DataTemplate.Triggers>
<!-- StringFormat when value is greather than 0 -->
<DataTrigger Binding="{Binding Path=Amount, Converter={StaticResource AmountFormatter}}" Value="true">
<Setter TargetName="TextBoxAmount" Property="Text" Value="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, StringFormat={}{0:N2}}" />
</DataTrigger>
<!-- Otherwise, no StringFormat -->
<DataTrigger Binding="{Binding Path=Amount, Converter={StaticResource AmountFormatter}}" Value="false">
<Setter TargetName="TextBoxAmount" Property="Text" Value="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
//App.xaml
<converters:DecimalOnly x:Key="AmountFormatter" />
CodePudding user response:
Does this solve your problem?
Converter:
using System;
using System.Globalization;
using System.Windows.Data;
internal class DecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return "";
return ((Decimal)value).ToString("0.00");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if ((value == null) || (value.ToString() == ""))
return Binding.DoNothing;
return Decimal.Parse(value.ToString());
}
catch
{
return null;
}
}
}
Usage:
<TextBox>
<TextBox.Text>
<Binding Path="Amount" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" StringFormat="{}{0:N2}">
<Binding.Converter>
<local:DecimalConverter />
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
CodePudding user response:
It turns out that the validation error message is not caused by using the StringFormat
.
The data type of Amount
in my ViewModel
which is bound into the TextBox
is Decimal
.
What happens when there is no value in the TextBox
is that, the UpdateSourceTrigger=PropertyChanged
is being triggered. But since it's empty, it cannot convert the value into the format I wanted. As result, the border of my TextBox turns to red, and an error message below it saying value * cannot be converted
. I don't know if this is a fact but I think it's a kind of feature when binding data.
My solution is from the snippet code of Mark Feldman.
First, in my ViewModel, change the data type of Amount
from Decimal
into String
.
/** MyViewModel Class **/
public class MyViewModel{
...
public string? Amount { get; set; } //instead of decimal, I used string. This is the one that is causing the validation error/message when no value in the textbox.
}
Next is to create an IValueConverter
class which I named AmountFormatter.class
. This will be the class that will handle the formatting when typing.
/** AmountFormatter.class **/
// (1) convert the value of the textbox which is string into decimal.
// //a succesfull convert, meaning the value is valid and in correct format.
// (2) format the decimal into {0:N2}
public class AmountFormatter : IValueConverter{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture){
decimal amount = (decimal.TryParse(value.ToString(), out decimal n)) ? n : 0; // if it failed to convert into decimal, meaning wrong value, then set default value as 0.
return (amount>0)?string.Format(culture, "{0:N2}", amount):""; //return "" (empty string) if value is less than 1. Maybe the user wants to type new value, so leave the textbox empty.
}
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
if ((value==null) || (string.IsNullOrEmpty(value.ToString()))) return ""; // returning empty string will also trigger the `updatesourcetrigger`.
return (decimal.TryParse(value.ToString(), out decimal n)) ? n : "";
}
}
Then declared the AmountFormatter.class
into my App.xaml
.
<Application
x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:MyApp.Converters"
xmlns:local="clr-namespace:MyApp"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
....
</ResourceDictionary.MergedDictionaries>
...
<converters:AmountFormatter x:Key="AmountFormatter" />
</ResourceDictionary>
</Application.Resources>
</Application>
After that in my xaml layout
which contains my TextBox, bind the Amount
and the AmountFormatter
converter;
/** xaml layout **/
<DataTemplate>
<DockPanel LastChildFill="True">
<TextBox
x:Name="TextBoxAmount"
DataObject.Pasting="TextBoxAmount_Paste"
PreviewKeyDown="TextBoxAmount_PreviewKeyDown"
PreviewTextInput="TextBoxAmount_PreviewTextInput"
Text="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource AmountFormatter}}" />
</DockPanel>
</DataTemplate>
And viola!
Miscellaneus;
The Textbox
is for currency, I wanted to format it as the user types on the TextBox
. In order to make sure that the users type the correct input, I limit the textbox to positive numbers only, no spacing is allowed, single decimal point, and two decimal places only. I also don't allow copy-paste unless the data is in correct format.
In my xaml layout class
, I have the following;
/** xaml layout class **/
//handle the pasting event, if the copied data failed to convert into decimal, then the format is invalid and do not allow to paste it.
private void TextBoxAmount_Paste(object sender, DataObjectPastingEventArgs e){
if (e.DataObject.GetDataPresent(typeof(String))){
if (!decimal.TryParse((String)e.DataObject.GetData(typeof(String)), out _)) e.CancelCommand();
} else {
e.CancelCommand();
}
}
//prevent from using space
private void TextBoxAmount_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e){
e.Handled = (e.Key == Key.Space); //do not allow spacing
}
//accept only numbers, single decimal, and two decimal places.
private void TextBoxAmount_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e){
TextBox tb = (TextBox)sender;
char ch = e.Text[0];
if (!Char.IsDigit(ch) && (ch!='.')) e.Handled = true;
if ((ch == '.') && tb.Text.IndexOf('.') > -1) e.Handled = true;
}
I hope there is no bug in this method.