WPF TextBox Binding с форматированием

Я только что обновил наше приложение wpf с 3.5sp1 до 4.0.

В приведенном ниже коде мы используем привязку текстового поля к базовой модели представления. Текстовое поле доступно для редактирования.

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

В 3.5sp1 форматирование произойдет только изначально. Поэтому, когда текстовое поле было загружено и привязано к значению 4000, форматирование изменит его на 4000. Если пользователь отредактировал это значение, форматирование не произойдет.

В 4.0 форматирование происходит по мере изменения значения (т.е. когда пользователь вводит новое значение). Хотя теоретически это звучит нормально, на самом деле это катастрофа. Курсор повсюду. Его непригодность.

Теперь мы можем изменить UpdateSourceTrigger на "LostFocus", но это создает новые проблемы, когда данные не проверяются в определенных сценариях.

Есть ли способ вернуть прежнее поведение 3.5sp1?

Обновление 1

Использование Converter по-прежнему реализует одно и то же поведение:

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;
    }
}

и измененный XAML:

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

Обновление 2

Подобно этому соединить статью.

Ответ 1

В качестве обновления я взял предложение Jonathans и переустановил Binding, чтобы использовать LostFocus вместо PropertyChanged (где это необходимо - то есть везде, где был указан StringFormat).

Как сказал Джонатан, в некоторых случаях вам нужно инициировать привязку обновления/проверки вручную с помощью этого подхода.

Если у кого-то есть лучший подход, я бы очень хотел его увидеть.

Ответ 2

Меня не устраивало решение LostFocus, поэтому я решил закодировать метод, который вручную корректно перемещает каретку. Я поместил его в код за файлом и добавив его в событие TextChanged в TextBox, чтобы он запускался каждый раз при изменении текста.

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;
}

Просто добавьте это в событие TextChanged следующим образом:

MyTextBox.TextChanged += moveCaret;

Я не уверен на 100%, но похоже, что он хорошо себя ведет, хотя он не обрабатывает удаление разделителя тысяч.

EDIT: Я понял, как обращаться с тысячным разделителем. Я сделал другой метод в коде за файлом и поместил его в событие PreviewKeyDown в TextBox. Этот метод проверяет, получает ли TextBox входное значение "Backspace of Delete", и просто игнорирует его и перемещает каретку вместо.

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;
            }
        }
    }
}     

Обратите внимание на специальный случай для тысячного разделителя в первом char в TextBox, где он удаляется вместо пропущенного. В тысячах разделителей в идеале никогда не должно быть там, но форматирование номера n0 не обрабатывает случай, когда вы удаляете первые числа перед разделителем первой тысячи.

Ответ 3

Вы можете попытаться удалить StringFormat={0:#,##0;(#,##0)} и записать конвертер для создания.