ozczecho
ozczecho

Reputation: 8849

WPF TextBox Binding with Formatting

I have just upgraded our wpf application from 3.5sp1 to 4.0.

The code below we use to bind the textbox to the underlying view model. The textbox is editable.

    <TextBox HorizontalContentAlignment="Right"
Text="{Binding Path=Price,   StringFormat={0:#,##0;(#,##0)},  Mode=TwoWay,  ValidatesOnDataErrors=True,  UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"/>

In 3.5sp1 the formatting would only occur initially. So when the textbox was loaded and bound to value 4000, the formatting would change it to 4,000. If user edited this value no formatting would occur.

In 4.0 the formatting occurs as the value changes (ie while user enters in new value). While in theory this sounds OK, in reality its a disaster. The cursor is all over the place. Its unusable.

Now, we could change the UpdateSourceTrigger to "LostFocus" but that introduces new problems with data not being validated in certain scenarios.

Is there a way to get the old 3.5sp1 behaviour back?

Update 1

Using Converter still procudes same behaviour:

public class DecimalConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null)
            return ((decimal)value).ToString("#,##0;(#,##0)");

        return string.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}

and the modified XAML:

<TextBox Text="{Binding Path=Price, Converter={StaticResource DecimalConverter}, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}"/>

Update 2

Similar to this connect article.

Upvotes: 12

Views: 19615

Answers (3)

S&#225;mal Rasmussen
S&#225;mal Rasmussen

Reputation: 3495

I wasn't satisfied with the LostFocus solution, so I decided to code a method that manually moves the caret correctly. I've put it in the code behind file and by adding it to the TextChanged event on the TextBox it get it to run every time the text changes.

void moveCaret(object sender, TextChangedEventArgs args)
{
    TextBox tb = (TextBox) sender;
    if (args.Changes.Any())
    {
        var first = args.Changes.First();
        int offset = 1;
        if(first.AddedLength > 0)
        {
            if (tb.Text.Length > 4 && tb.Text.Length % 4 == 1)
                offset = 2;
            tb.CaretIndex = first.Offset + offset;
        }
        else
        {
            if (tb.CaretIndex > 0)
            {
                offset = 0;
                if (tb.Text.Length > 2 && (tb.Text.Length + 2) % 4 == 1)
                    offset = -1;
                tb.CaretIndex = first.Offset + offset;
            }
        } 
    }
    args.Handled = true;
}

Just add this to the TextChanged event like so:

MyTextBox.TextChanged += moveCaret;

I'm not 100% sure, but this seems to behave well, though it doesn't handle deleting of the thousand separator.

EDIT: I figured out how to handle the thousand separator. I made another method in the code behind file, and put it on the PreviewKeyDown event on the TextBox. This method checks if the TextBox is receiving a Backspace of Delete button input, and just ignores it and moves the caret in stead.

private void handleThousandSeparator(object sender, KeyEventArgs e)
{
    var textBox = sender as TextBox;
    if (e.Key == Key.Back)
    {
        if (textBox.CaretIndex > 0)
        {
            if (textBox.Text[textBox.CaretIndex - 1] +"" == System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator)
            {
                if (textBox.Text[0] + "" == System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator)
                    return;
                textBox.CaretIndex = textBox.CaretIndex - 1;
                e.Handled = true;
            }
        }
    }
    if (e.Key == Key.Delete)
    {
        if (textBox.CaretIndex < textBox.Text.Length)
        {
            if (textBox.Text[textBox.CaretIndex] + "" == System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator)
            {
                if (textBox.Text[0] + "" == System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator)
                    return;
                textBox.CaretIndex = textBox.CaretIndex + 1;
                e.Handled = true;
            }
        }
    }
}     

Notice the special case for a thousand separator at the first char in the TextBox, where it is deleted in stead of skipped. A thousand separator should ideally never get to be there, but the n0 number formatter doesn't handle the case where you delete the first numbers before the first thousand separator.

Upvotes: 0

ozczecho
ozczecho

Reputation: 8849

As an update I took Jonathans suggestion and rejigged the Binding to use LostFocus instead of PropertyChanged (where appropriate - ie wherever StringFormat was also specified).

As Jonathan said, in some cases you have to trigger binding refresh / validation manually taking this approach.

If anyone has better approach, I would love to see it.

Upvotes: 2

Arsen Mkrtchyan
Arsen Mkrtchyan

Reputation: 50712

You can try to remove StringFormat={0:#,##0;(#,##0)} and write converter to do formating.

Upvotes: 0

Related Questions