Его действительно раздражает, что TextBox
не обеспечивает PreviewTextChanged
события и все должны изобрести колесо каждый раз, чтобы эмулировать его. Недавно я решил точно такую же проблему и даже опубликовал свое решение на github как WpfEx project (посмотрите на TextBoxBehavior.cs и TextBoxDoubleValidator.cs).
Ответ Адама Света очень хорош, но мы должны рассмотреть несколько других угловых случаев.
- Выбранный текст.
Во время обработки результирующего текста в нашем обработчике событий textBox_PreviewTextInput
мы должны учитывать, что пользователь может выбрать текст в текстовом поле, а новый вход заменит его. Поэтому мы должны использовать что-то вроде:
private static void PreviewTextInputForDouble(object sender,
TextCompositionEventArgs e)
{
// e.Text contains only new text and we should create full text manually
var textBox = (TextBox)sender;
string fullText;
// If text box contains selected text we should replace it with e.Text
if (textBox.SelectionLength > 0)
{
fullText = textBox.Text.Replace(textBox.SelectedText, e.Text);
}
else
{
// And only otherwise we should insert e.Text at caret position
fullText = textBox.Text.Insert(textBox.CaretIndex, e.Text);
}
// Now we should validate our fullText, but not with
// Double.TryParse. We should use more complicated validation logic.
bool isTextValid = TextBoxDoubleValidator.IsValid(fullText);
// Interrupting this event if fullText is invalid
e.Handled = !isTextValid;
}
И мы должны использовать ту же логику, когда будем обрабатывать событие OnPaste.
- Проверка текста
Мы не можем использовать простой Double.TryParse, поскольку пользователь может ввести '+'. для типа '+.1' ('+.1' - абсолютно допустимая строка для double), поэтому наш метод проверки должен возвращать true в '+.' или '-.' (я даже создал отдельный класс под названием TextBoxDoubleValidator
и набор модульных тестов, потому что эта логика так важна).
Перед копаться в реализации позволяет взглянуть на набор модульных тестов, которые будут охватывать все случаи угловые для метода проверки:
[TestCase("", Result = true)]
[TestCase(".", Result = true)]
[TestCase("-.", Result = true)]
[TestCase("-.1", Result = true)]
[TestCase("+", Result = true)]
[TestCase("-", Result = true)]
[TestCase(".0", Result = true)]
[TestCase("1.0", Result = true)]
[TestCase("+1.0", Result = true)]
[TestCase("-1.0", Result = true)]
[TestCase("001.0", Result = true)]
[TestCase(" ", Result = false)]
[TestCase("..", Result = false)]
[TestCase("..1", Result = false)]
[TestCase("1+0", Result = false)]
[TestCase("1.a", Result = false)]
[TestCase("1..1", Result = false)]
[TestCase("a11", Result = false)]
[SetCulture("en-US")]
public bool TestIsTextValid(string text)
{
bool isValid = TextBoxDoubleValidator.IsValid(text);
Console.WriteLine("'{0}' is {1}", text, isValid ? "valid" : "not valid");
return isValid;
}
Обратите внимание, что я использую SetCulture ("EN-US") атрибут , потому что десятичный разделитель «локально-специфический».
Я думаю, что я покрываю все угловые шкалы этими тестами, но с помощью этого инструмента в ваших руках вы можете легко «эмулировать» пользователя вменять и проверять (и повторно использовать) любые случаи, которые вы хотите. И теперь давайте взглянем на метод TextBoxDoubleValidator.IsValid
:
/// <summary>
/// Helper class that validates text box input for double values.
/// </summary>
internal static class TextBoxDoubleValidator
{
private static readonly ThreadLocal<NumberFormatInfo> _numbersFormat = new ThreadLocal<NumberFormatInfo>(
() => Thread.CurrentThread.CurrentCulture.NumberFormat);
/// <summary>
/// Returns true if input <param name="text"/> is accepted by IsDouble text box.
/// </summary>
public static bool IsValid(string text)
{
// First corner case: null or empty string is a valid text in our case
if (text.IsNullOrEmpty())
return true;
// '.', '+', '-', '+.' or '-.' - are invalid doubles, but we should accept them
// because user can continue typeing correct value (like .1, +1, -0.12, +.1, -.2)
if (text == _numbersFormat.Value.NumberDecimalSeparator ||
text == _numbersFormat.Value.NegativeSign ||
text == _numbersFormat.Value.PositiveSign ||
text == _numbersFormat.Value.NegativeSign + _numbersFormat.Value.NumberDecimalSeparator ||
text == _numbersFormat.Value.PositiveSign + _numbersFormat.Value.NumberDecimalSeparator)
return true;
// Now, lets check, whether text is a valid double
bool isValidDouble = StringEx.IsDouble(text);
// If text is a valid double - we're done
if (isValidDouble)
return true;
// Text could be invalid, but we still could accept such input.
// For example, we should accepted "1.", because after that user will type 1.12
// But we should not accept "..1"
int separatorCount = CountOccurances(text, _numbersFormat.Value.NumberDecimalSeparator);
// If text is not double and we don't have separator in this text
// or if we have more than one separator in this text, than text is invalid
if (separatorCount != 1)
return false;
// Lets remove first separator from our input text
string textWithoutNumbersSeparator = RemoveFirstOccurrance(text, _numbersFormat.Value.NumberDecimalSeparator);
// Second corner case:
// '.' is also valid text, because .1 is a valid double value and user may try to type this value
if (textWithoutNumbersSeparator.IsNullOrEmpty())
return true;
// Now, textWithoutNumbersSeparator should be valid if text contains only one
// numberic separator
bool isModifiedTextValid = StringEx.IsDouble(textWithoutNumbersSeparator);
return isModifiedTextValid;
}
/// <summary>
/// Returns number of occurances of value in text
/// </summary>
private static int CountOccurances(string text, string value)
{
string[] subStrings = text.Split(new[] { value }, StringSplitOptions.None);
return subStrings.Length - 1;
}
/// <summary>
/// Removes first occurance of valud from text.
/// </summary>
private static string RemoveFirstOccurrance(string text, string value)
{
if (string.IsNullOrEmpty(text))
return String.Empty;
if (string.IsNullOrEmpty(value))
return text;
int idx = text.IndexOf(value, StringComparison.InvariantCulture);
if (idx == -1)
return text;
return text.Remove(idx, value.Length);
}
}
** всегда ** сильное слово –
@jberger: Что заставляет вас говорить это? Разве это не достаточно простое требование? –
Что должно произойти, когда пользователь вводит недопустимый символ, а затем оставляет текстовое поле? –