In the last example from the previous section, we data bound the entire Validation.Errors collection to a tooltip in the error template for our TextBox control. We also data bound our own Errors collection from our base class to the ItemsControl element above the form fields.
Our Errors collection can display all of the errors for all of the properties in each data Model. However, the Validation.Errors collection has access to UI-based validation errors that never make it back to the View Models. Take a look at the following example:

The UI-based validation error says Value '0t' could not be converted, and that explains why the View Models never see this error. The type of value expected in the data bound property is decimal, but an unconvertible value has been entered. Therefore, the input value cannot be converted to a valid decimal number and so, the data bound value is never updated.
However, the Validation.Errors collection is a UI element, and each data bound control has its own collection, and so we have no simple way to access them all from our View Model classes. Furthermore, the ValidationError class is in the System.Windows.Controls UI assembly, so we don't want to add a reference of that to our ViewModels project.
Instead of trying to control the UI-based validation errors from the View Models, we can alternatively extend controls, or define Attached Properties that restrict the ability of the users to enter invalid data in the first place, thereby avoiding the need for UI-based validation. Let's take a look at one way in which we can modify a standard TextBox control, so that it will only accept numerical input, using our TextBoxProperties class:
using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace CompanyName.ApplicationName.Views.Attached { public class TextBoxProperties : DependencyObject { #region IsNumericOnly public static readonly DependencyProperty IsNumericOnlyProperty = DependencyProperty.RegisterAttached("IsNumericOnly", typeof(bool), typeof(TextBoxProperties), new UIPropertyMetadata(default(bool), OnIsNumericOnlyChanged)); public static bool GetIsNumericOnly(DependencyObject dependencyObject) { return (bool)dependencyObject.GetValue(IsNumericOnlyProperty); } public static void SetIsNumericOnly(DependencyObject dependencyObject, bool value) { dependencyObject.SetValue(IsNumericOnlyProperty, value); } private static void OnIsNumericOnlyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { TextBox textBox = (TextBox)dependencyObject; bool newIsNumericOnlyValue = (bool)e.NewValue; if (newIsNumericOnlyValue) { textBox.PreviewTextInput += TextBox_PreviewTextInput; textBox.PreviewKeyDown += TextBox_PreviewKeyDown; DataObject.AddPastingHandler(textBox, TextBox_Pasting); } else { textBox.PreviewTextInput -= TextBox_PreviewTextInput; textBox.PreviewKeyDown -= TextBox_PreviewKeyDown; DataObject.RemovePastingHandler(textBox, TextBox_Pasting); } } private static void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { string text = GetFullText((TextBox)sender, e.Text); e.Handled = !IsTextValid(text); } private static void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) { TextBox textBox = (TextBox)sender; if (textBox.Text.Length == 1 &&
(e.Key == Key.Delete || e.Key == Key.Back))
{
textBox.Text = "0";
textBox.CaretIndex = 1;
e.Handled = true;
}
else if (textBox.Text == "0") textBox.Clear();
else e.Handled = e.Key == Key.Space; } private static void TextBox_Pasting(object sender, DataObjectPastingEventArgs e) { if (e.DataObject.GetDataPresent(typeof(string))) { string text = GetFullText((TextBox)sender, (string)e.DataObject.GetData(typeof(string))); if (!IsTextValid(text)) e.CancelCommand(); } else e.CancelCommand(); } private static string GetFullText(TextBox textBox, string input) { return textBox.SelectedText.Length > 0 ? string.Concat(textBox.Text.Substring(0, textBox.SelectionStart), input, textBox.Text.Substring(textBox.SelectionStart + textBox.SelectedText.Length)) : textBox.Text.Insert(textBox. SelectionStart, input); } private static bool IsTextValid(string text) { return Regex.Match(text, @"^\d*\.?\d*$").Success; } #endregion ... } }
Excluding the other, existing members from our TextBoxProperties class, we first declare the IsNumericOnly Attached Property and its related getter and setter methods and attach the OnIsNumericOnlyChanged handler.
In the OnIsNumericOnlyChanged method, we first cast the dependencyObject input parameter to a TextBox element and then cast the NewValue property of the DependencyPropertyChangedEventArgs class to the bool newIsNumericOnlyValue variable.
If the newIsNumericOnlyValue variable is true, we attach our event handlers for the PreviewTextInput, PreviewKeyDown, and DataObject.Pasting events. If the newIsNumericOnlyValue variable is false, we detach the handlers.
We need to handle all of these events in order to create a TextBox control that can only enter numerical values. The UIElement.PreviewTextInput event is raised when a TextBox element receives a text input from any device, the Keyboard.PreviewKeyDown event occurs specifically when a keyboard key is pressed, and the DataObject.Pasting event is raised when we paste from the clipboard.
The TextCompositionEventArgs object in the TextBox_PreviewTextInput handler method only provides us with the last typed character through its Text property, along with TextComposition details. At the stage that this tunneling event is called, the Text property of the relevant TextBox control is not yet aware of this latest character.
Therefore, in order to correctly validate the whole entered text value, we need to combine the existing value with this new character. We do that in the GetFullText method and pass the returned value to the IsTextValid method.
We then set the inverted return value of the IsTextValid method to the Handled property of the TextCompositionEventArgs input parameter. Note that we invert this bool value, because setting the Handled property to true will stop the event from being routed any further and result in the latest character not being accepted. Therefore, we do this when the input value is invalid.
Next, we see the TextBox_PreviewKeyDown event handler method, and in it, we again start by casting the sender input parameter to a TextBox instance. We specifically need to handle this event, because the PreviewTextInput event does not get raised when the Space bar, Delete, or Backspace keys on the keyboard are pressed.
Therefore, we stop the event being routed any further by setting the Handled property of the KeyEventArgs input parameter to true if the pressed key is the Space bar key, or if the length of the entered text is a single character and the Delete or Backspace key is pressed; this stops the user from deleting the last character from the TextBox control, which would result in a UI-based validation error.
However, if the user was trying to delete the last character because it was incorrect and they wanted to replace it with a different value, this could be awkward. Therefore, in this situation, we replace the last character with a zero and place the caret position after it, which then enables the user to type a different value. Note our extra condition that clears the text if the input is 0, so that it will be replaced with the typed character.
In the TextBox_Pasting handler method, we check whether the DataObject property that is accessed from the DataObjectPastingEventArgs input parameter has any string data available, and call its CancelCommand method to cancel the paste operation if not.
If string data is present, we cast the sender input parameter to a TextBox instance and then pass the data from the DataObject property to the GetFullText method to reconstruct the whole entered string. We pass the reconstructed text to the IsTextValid method and, if it is invalid, then we call the CancelCommand method to cancel the paste operation.
Next is the GetFullText method, where the entered text from the TextBox element is reconstructed. In this method, if any text is selected in the TextBox control, we rebuild the string by concatenating the portion of text before the selection with the newly entered or pasted text and the portion of text after the selection. Otherwise, we use the Insert method of the String class, along with the TextBox control's SelectionStart property, to insert the new character into the appropriate place in the string.
At the end of the class, we see the IsTextValid method, which simply returns the Success property value of the Regex.Match method. The regular expression that we validate with is as follows:
@"^\d*\.?\d*$"
The ampersand (@)marks the string as a Verbatim String Literal, which is useful when using characters that normally need to be escaped, the caret (^) signifies the start of the input line, \d* indicates that we can have zero or more numerical digits, \.? specifies that zero or one periods are then valid, \d* again indicates that we can then have zero or more numerical digits, and finally, $ signifies the end of the input line.
When attached to an ordinary TextBox control, we can now only enter numeric values, but both integer and decimal values are allowed. Note that this particular implementation does not accept the minus sign, as we don’t want to allow negative prices, but that could be easily changed. Using our earlier ProductNotifyViewExtended example, we can attach our new property like this:
xmlns:Attached="clr-namespace:CompanyName.ApplicationName.Views.Attached" ... <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Products.CurrentItem.Price, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True, Delay=250}" Style="{StaticResource FieldStyle}" Attached:TextBoxProperties.IsNumericOnly="True" />