Input Validation

Validation of user input is a pretty essential component of almost any application. Ideally, it is a quick and unobtrusive way to give the user feedback and help guide them through the application.

Instead of preventing the user from entering invalid data, I’ve found actually allowing invalid states to give a much better user experience. Don’t preventing the user from moving the cursor out of a text box with an invalid input, just prevent the user from doing the higher-level OK/Save command. If a user is entering contact information into an address book, you shouldn’t lock the the cursor in the phone number field if it’s invalid. You should instead prevent them from hitting “OK” or “Save”. This is a less annoying behavior (“Why can’t I move my cursor? This program is broken!”). Example I’ve created a simple example that shows how to do input validation in an MVVM-way using IDataErrorInfo, an ErrorTemplate, and a RelayCommand. This code assumes you have a ViewModelBase that implements INotifyPropertyChanged and RelayCommand. Both are included in most MVVM toolkits and are very easy to write. View Model: This code is unfortunately extremely verbose. I’ve been investigating ways to reduce the amount of code that MVVM and things like properties and validation take. Some simple code generation/rewriting combined with naming conventions should really eliminate almost all of this code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public class ContactViewModel : ViewModelBase, IDataErrorInfo {
    private string m_FirstName, m_LastName;
    private string m_PhoneNumber;
    private readonly RelayCommand m_SaveCommand;
    public ContactViewModel() {
        m_SaveCommand = new RelayCommand(SaveCommand_Execute, SaveCommand_CanExecute);
    }
    public ICommand SaveCommand { get { return m_SaveCommand; } }
    private void SaveCommand_Execute() {
        //Save to database or something here
    }
    private bool SaveCommand_CanExecute() {
        return IsFirstNameValid && IsLastNameValid && IsPhoneNumberValid;
    }
    public string FirstName {
        get { return m_FirstName; }
        set {
            if (m_FirstName != value) {
                m_FirstName = value;
                OnPropertyChanged("FirstName");
            }
        }
    }
    public bool IsFirstNameValid {
        get {
            return !string.IsNullOrWhiteSpace(FirstName);
        }
    }
    public string LastName {
        get { return m_LastName; }
        set {
            if (m_LastName != value) {
                m_LastName = value;
                OnPropertyChanged("LastName");
            }
        }
    }
    public bool IsLastNameValid {
        get {
            return !string.IsNullOrWhiteSpace(LastName);
        }
    }
    public string PhoneNumber {
        get { return m_PhoneNumber; }
        set {
            if (m_PhoneNumber != value) {
                m_PhoneNumber = value;
                OnPropertyChanged("PhoneNumber");
            }
        }
    }
    public bool IsPhoneNumberValid {
        get {
            return !string.IsNullOrWhiteSpace(PhoneNumber) && Regex.IsMatch(PhoneNumber, @"^[0-9() -]{3,}$");
        }
    }
    public string Error {
        get {
            if (!IsFirstNameValid) {
                return this["FirstName"];
            }
            if (!IsLastNameValid) {
                return this["LastName"];
            }
            if (!IsPhoneNumberValid) {
                return this["PhoneNumber"];
            }
            return string.Empty;
        }
    }
    public string this[string columnName] {
        get {
            if (columnName == "FirstName") {
                if (!IsFirstNameValid)
                    return "First Name must not be empty.";
                else
                    return string.Empty;
            }
            if (columnName == "LastName") {
                if (!IsLastNameValid)
                    return "Last Name must not be empty.";
                else
                    return string.Empty;
            }
            if (columnName == "PhoneNumber") {
                if (!IsPhoneNumberValid)
                    return "Phone Number contains invalid characters.";
                else
                    return string.Empty;
            }
            return string.Empty;
        }
    }
}
Design-time View Model:public class DesignTimeContactViewModel : ContactViewModel {
    public DesignTimeContactViewModel() {
        FirstName = "John";
        LastName = "Smith";
        PhoneNumber = "(555) 123-4567asdf";
    }
}

View in XAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 <UserControl
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:InputValidationExample="clr-namespace:InputValidationExample"
        mc:Ignorable="d"
        d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
    <ControlTemplate x:Key="InvalidInputTemplate" TargetType="Control">
        <Border BorderThickness="2" BorderBrush="Red">
            <AdornedElementPlaceholder />
        </Border>
    </ControlTemplate>
</UserControl.Resources>
<Grid d:DataContext="{d:DesignInstance InputValidationExample:DesignTimeContactViewModel, IsDesignTimeCreatable=True}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Label Content="First Name:" Grid.Row="0" Grid.Column="0" />
    <TextBox Text="{Binding FirstName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="0" Grid.Column="1"
             Validation.ErrorTemplate="{StaticResource InvalidInputTemplate}"/>
    <Label Content="Last Name:" Grid.Row="1" Grid.Column="0" />
    <TextBox Text="{Binding LastName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="1" Grid.Column="1"
             Validation.ErrorTemplate="{StaticResource InvalidInputTemplate}" />
    <Label Content="Phone Number:" Grid.Row="2" Grid.Column="0" />
    <TextBox Text="{Binding PhoneNumber, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Grid.Row="2" Grid.Column="1"
             Validation.ErrorTemplate="{StaticResource InvalidInputTemplate}"/>
    <Button Content="Save" Command="{Binding SaveCommand}" Grid.Row="3" Grid.ColumnSpan="2"
            HorizontalAlignment="Right" Margin="8" />
</Grid>