Search code examples
c#wpfxamlstring-formatting

c# wpf xaml using StringFormat without or with CUSTOM error message


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

Solution

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