Numeric TextBox - przy użyciu Double.TryParse


Wiem, że to odwieczne pytanie z wieloma odpowiedziami, ale nie znalazłem żadnej dobrej, wiarygodnej odpowiedzi.
Wymagane jest pole tekstowe, które zawsze będzie zawierało ciąg, w którym Double.TryParse zwróci wartość true.
Większość implementacji, które widziałem, nie chroni przed danymi wejściowymi, takimi jak „10.45.8”. To jest problem.
Preferowany sposób, aby zrobić to całkowicie ze zdarzeniami takimi jak TextInput i KeyDown (dla spacji). Problem z nimi polega na tym, że dość trudno jest uzyskać ciąg reprezentujący nowy tekst, zanim został zmieniony (lub stary tekst po zmianie). Problem z TextChanged polega na tym, że uniemożliwia pobranie starego tekstu.
Gdybyś mógł w jakiś sposób uzyskać nowy tekst, zanim się zmieni, byłoby to najbardziej pomocne, ponieważ możesz przetestować go z Double.TryParse. Jednak może być lepsze rozwiązanie.
Jak najlepiej to zrobić?
Najlepszą odpowiedzią na to pytanie jest taka, która zawiera kilka podejść i porównuje je.
Zaproszony:
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:


Podejście 1

Użyj kombinacji zdarzeń
TextChanged
i
KeyDown
dla
TextBox
. W
KeyDown
możesz zapisać bieżący tekst w polu tekstowym, a następnie ustawić
Double.TryParse
w zdarzeniu
TextChanged
. Jeśli wprowadzony tekst jest nieprawidłowy, powrócisz do starej wartości tekstowej. Będzie to wyglądać tak:
private int oldIndex = 0;
private string oldText = String.Empty;private void textBox1_TextChanged(object sender, TextChangedEventArgs e)
{
double val;
if (!Double.TryParse(textBox1.Text, out val))
{
textBox1.TextChanged -= textBox1_TextChanged;
textBox1.Text = oldText;
textBox1.CaretIndex = oldIndex;
textBox1.TextChanged += textBox1_TextChanged;
}
}private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
oldIndex = textBox1.CaretIndex;
oldText = textBox1.Text;
}

CaratIndex
jest przydatny, aby nie denerwować użytkownika na śmierć, przesuwając kursor na pierwszą pozycję w przypadku nieudanego testu. Jednak ta metoda nie przechwytuje naciśnięcia klawisza SpaceBar. Umożliwi to wprowadzenie następującego tekstu: „1234,56
„Ponadto wstawianie tekstu nie będzie prawidłowo sprawdzane. Poza tym nie lubię bawić się programami obsługi zdarzeń podczas aktualizowania tekstu”.

Podejście 2

Takie podejście powinno odpowiadać Twoim potrzebom.
Użyj modułów obsługi zdarzeń
PreviewKeyDown
i
PreviewTextInput
. Obserwując te zdarzenia i odpowiednio je obsługując, nie musisz się martwić o powrót do poprzedniej wartości tekstowej w polu tekstowym.
PreviewKeyDown
może służyć do obserwowania i ignorowania naciśnięcia klawisza spacji, podczas gdy < code> PreviewTextInput może służyć do weryfikacji nowej wartości pola tekstowego przed jej przypisaniem.
private void textBox1_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
e.Handled = true;
}
}private void textBox1_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
//Create a string combining the text to be entered with what is already there.
//Being careful of new text positioning here, though it isn't truly necessary for validation of number format.
int cursorPos = textBox1.CaretIndex;
string nextText;
if (cursorPos > 0)
{
nextText = textBox1.Text.Substring(0, cursorPos) + e.Text + textBox1.Text.Substring(cursorPos);
}
else
{
nextText = textBox1.Text + e.Text;
}
double testVal;
if (!Double.TryParse(nextText, out testVal))
{
e.Handled = true;
}
}

To podejście lepiej sprawdza się w wychwytywaniu nieprawidłowych danych wejściowych, zanim trafią one w pole tekstowe. Jednak ustawienie zdarzenia na
Handled
może spowodować kłopoty, jak sądzę, w zależności od pozostałych miejsc docelowych na liście routingu wiadomości. Ostatnią częścią, która nie jest tutaj obsługiwana, jest możliwość wstawienia przez użytkownika nieprawidłowych danych wejściowych do pola tekstowego. Można to zrobić, dodając ten kod, który jest zbudowany w oparciu o pliki

Wklej zdarzenia w WPF TextBox
https://coderoad.ru/3061475/
.
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
double testVal;
bool ok = false; var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.Text, true);
if (isText)
{
var text = e.SourceDataObject.GetData(DataFormats.Text) as string;
if (Double.TryParse(text, out testVal))
{
ok = true;
}
} if (!ok)
{
e.CancelCommand();
}
}

Dodaj tę procedurę obsługi z tym kodem po wywołaniu
InitializeComponent
:
DataObject.AddPastingHandler(textBox1, new DataObjectPastingEventHandler(OnPaste));
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

To naprawdę denerwujące, że
TextBox
nie udostępnia zdarzenia
PreviewTextChanged
i każdy musi za każdym razem wymyślać koło, aby je naśladować. Niedawno rozwiązałem dokładnie ten sam problem, a nawet opublikowałem moje rozwiązanie w

projekt github jako WpfEx
https://github.com/SergeyTeplyakov/WpfEx
(patrzeć na

TextBoxBehavior.cs
https://github.com/SergeyTeply ... or.cs
i

TextBoxDoubleValidator.cs
https://github.com/SergeyTeply ... or.cs
).
Odpowiedź Adama S. jest bardzo dobra, ale musimy wziąć pod uwagę również kilka innych przypadków narożnych.
  • Wybrany tekst.

Kopiując wynikowy tekst do naszego modułu obsługi zdarzeń
textBox_PreviewTextInput
, musimy wziąć pod uwagę, że użytkownik może zaznaczyć tekst w polu tekstowym, a nowe dane wejściowe go zastąpią. Dlatego musimy użyć czegoś takiego:
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;
}

I musimy użyć tej samej logiki, kiedy obsługujemy zdarzenie OnPaste.
  • Sprawdzenie poprawności tekstu

Nie możemy użyć prostego Double.TryParse, ponieważ użytkownik może wpisać „+”. Aby wprowadzić „+.1” („+.1” jest absolutnie poprawnym ciągiem znaków podwójnych), więc nasza metoda walidacji musi zwracać wartość true dla łańcuchów ” +. 'lub' -. '(Stworzyłem nawet oddzielną klasę o nazwie
TextBoxDoubleValidator
i zestaw testów jednostkowych, ponieważ ta logika jest tak ważna).
Zanim zagłębimy się w implementację, przyjrzyjmy się zestawowi testów jednostkowych, które obejmą wszystkie narożne przypadki dla metody walidacji:
[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;
}

Zauważ, że używam atrybutu SetCulture („en-US”), ponieważ separator dziesiętny jest „lokalny”.
Myślę, że te testy obejmują wszystkie przypadki narożne, ale dzięki temu narzędziu na wyciągnięcie ręki możesz łatwo „emulować” przypisanie użytkownikowi i przetestować (i ponownie wykorzystać) dowolne przypadki. Teraz przyjrzyjmy się metodzie
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);
}}
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Bardziej komentarz niż odpowiedź, ale ...
Byłbym ostrożny w sprawdzaniu poprawności danych wejściowych po każdym naciśnięciu klawisza, ponieważ może to mieć niezamierzone konsekwencje i denerwować użytkownika końcowego.
Na przykład, pamiętam, że denerwowała mnie kontrolka DatePicker, która nie zezwala na daty w przyszłości i została zainicjowana na dzisiejszą datę. Przeprowadził walidację po wpisaniu dnia, miesiąca lub roku, więc nie można było wprowadzić miesiąca/dnia późniejszego niż data bieżąca bez uprzedniej zmiany roku.
W przypadku podwójnych możesz mieć podobny problem, na przykład sugerowana przez Ciebie kontrola uniemożliwi użytkownikowi wprowadzenie idealnie prawidłowych wartości „-1”, „.12”, „1e + 5”:
- - invalid
-1 - valid. - invalid.1 - valid1 - valid
1e - invalid
1e+ - invalid
1e+5 - valid

Zalecałbym sprawdzenie jak zwykle, kiedy użytkownik opuszcza pole tekstowe, lub jawnie sprawdza, klikając przycisk.

Aby odpowiedzieć na pytania, Zaloguj się lub Zarejestruj się