简体   繁体   中英

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

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.

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM