Management and Validation of Data: the User Interface

This is the third and last article of an article series about the management and validation of data in a graphical user interface. Within this article series I want to introduce a data class which can manage a single value, a data collection based on this data class and an example how to use these classes within a WPF user interface.

 
Example application

I want to show the data management and validation by using a graphical user interface implanted with WPF. It should be possible to type in the data of a person: first name, last name and age. A validation check should be done to check the user input. Each single parameter should be checked and the validation errors should be shown. Furthermore the user interface should contain a summary field with all validation errors.

The demo application is implemented with WPF. To keep it simple the data management is implanted in the code behind file. Of course, in a real application, you should separate the visualization code from the logical code for example by using the MVVM pattern. But in this article series we don’t want to learn the MVVM pattern, we want to learn something about data management and validation. Therefore the code of the user interface should be as simple as possible.

 
Validation in WPF

There are several possibilities to implement data validation in WPF. I want to use the interface IDataErrorInfo in combination with the ValidatesOnDataErrors property. This interface provides the functionality to bind custom error information to the user interface.

The interface IDataErrorInfo contains the following properties:

  • string Error { get; }
    • gets an error message indicating what is wrong with this object
    • so this can be used to validate the whole object
  • string this[string columnName] { get; }
    • gets the error message for the property with the given name
    • so this can be used to get validation information about each single property

 
Data class

As described, we want to manage the data for a person with first name, last name and age. To implement the data class we will use the PropertyCollection class, developed in the previous articles. Therefore we can create a PersonData class which uses the PropertyCollection class as base.

namespace UserInterface
{
    public class PersonData : PropertyCollection
    {
    }       
}

 

Our PersonData will provide the needed properties and will contain all validation definitions. So it is a simple wrapper class which does not contain additional functionality but will allow as an easier data management. Therefore the PersonData class should contain three properties to set and get the name and age fields. The following code shows the according implementation.

using System;
using System.Collections.ObjectModel;

namespace UserInterface
{
    public class PersonData : PropertyCollection
    {   
        #region Properties

        public string FirstName
        {
            get { return GetProperty<string>("FirstName").Value; }
            set { GetProperty<string>("FirstName").Value = value; }
        }

        public string LastName
        {
            get { return GetProperty<string>("LastName").Value; }
            set { GetProperty<string>("LastName").Value = value; }
        }

        public int Age
        {
            get { return GetProperty<int>("Age").Value; }
            set { GetProperty<int>("Age").Value = value; }
        }

        #endregion
    }
}

 

To demonstrate the validation functions, we will define the following conditions: the first name and last name should not be empty and the age of the person must be at least 18. These conditions should be set in the constructor of the data calls. The following code shows a possible implementation.

using System;
using System.Collections.ObjectModel;

namespace UserInterface
{
    public class PersonData : PropertyCollection
    {
        #region ctor

        public PersonData()
        {
            Property<string> firstName;
            Property<string> lastName;
            Property<int> age;

            //---init member and set validators---
            //first name
            Func<string, ObservableCollection<string>> firstNameValidator = delegate(string value)
            {
                if (string.IsNullOrEmpty(value))
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("First name should not be empty");

                    return errors;
                }

                return null;
            };

            firstName = new Property<string>(firstNameValidator);

            //last name
            Func<string, ObservableCollection<string>> lastNameValidator = delegate(string value)
            {
                if (string.IsNullOrEmpty(value))
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("Last name should not be empty");

                    return errors;
                }

                return null;
            };

            lastName = new Property<string>(lastNameValidator);

            //age
            Func<int, ObservableCollection<string>> ageValidator = delegate(int value)
            {
                if (value < 18)
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("Age must be 18 or higher");

                    return errors;
                }

                return null;
            };

            age = new Property<int>(ageValidator);

            //---init collection---
            Add("FirstName", firstName);
            Add("LastName", lastName);
            Add("Age", age);
        }

        #endregion
    }
}

 

That’s all we have to implement in the data class. All other functionality will come into from the base class. The full source code of the data class is shown at the end of this article.

 
User Interface visualization

The user interface is implemented with XAML. It contains three text box elements for input of the person properties. Furthermore it contains a Save button to commit the input and a Reset button to reset all data to the values of the last submit.

You can find the full source code of the XAML file at the end of the article. Now we only want to discuss the parts important for the data validation.

The property validation is done by implementing the interface IDataErrorInfo which we will see later on in the code behind class. The code behind class will then offer a property with error information. So each user control can be bound to this error information. To do so we have to set the binding property ValidatesOnDataErrors to true. The following code will show an according example for a TextBox element. ValidatesOnDataErrors is set to true and the property itself will be identified by the code behind class by the property name set in the Path value.

<TextBox Name="TextBoxFirstName" Style="{StaticResource textBoxInError}"
         Text="{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}">
</TextBox>

 

As you can see I have defined a style for the TextBox element. Of course, if there is a validation error, you want to display it. According to your operating system there should already be a standard visualization. For example there can be a red border if a validation error occurs. Additional to this standard visualization I want to show the validation error within a tool tip. Therefore I have created the following style which will set the according tool tip.

<Style x:Key="textBoxInError" TargetType="TextBox">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip"
                Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                Path=(Validation.Errors)[0].ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

 

Furthermore the user interface contains a TextBlock for the error summery to show all errors. The content of this element will be set in the data validation functions of the code behind file.

 
User Interface data management

The data management is implemented in the code behind file. As mentioned before I recommend you to use the MVVM pattern in your real application. But for the demo application we will implement a quick and dirty solution by using the code behind file.

The following source code shows the according code behind file. The class of the main window should implement the IDataErrorInfo interface needed for the binding of the data validation results. Within our view class we will use the previously PersonData class to store the data. So I have created an according member variable and set some demo data in the constructor.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region ctor

        public MainWindow()
        {
            InitializeComponent();

            _person = new PersonData() { FirstName = "John", LastName = "Doe", Age = 28 };
            _person.AcceptChanges();
        }

        #endregion

        #region Member

        private PersonData _person;

        #endregion  
    }
}

 

The view class contains some properties which are bound to the text controls. The following code shows these properties. As you can see, an update of a property will call the functions UpdateButtons and UpdateErrorSummary which I want to describe as next.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region Properties

        public string FirstName
        {
            get
            {
                return _person.FirstName;
            }
            set
            {
                _person.FirstName = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        public string LastName
        {
            get
            {
                return _person.LastName;
            }
            set
            {
                _person.LastName = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        public int Age
        {
            get
            {
                return _person.Age;
            }
            set
            {
                _person.Age = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        #endregion
    }
}

 

As described above, the user interface contains a Save and a Reset button. These buttons should be enabled if the user has done some changes on the data. So the following function is implemented which updates the IsEnabled property by using the IsChanged function of the PersonData class.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region Content Management

        private void UpdateButtons()
        {
            ButtonSave.IsEnabled = _person.IsChanged;
            ButtonReset.IsEnabled = _person.IsChanged;
        }

        #endregion
    }
}

 

The second function called by the setter of the properties is UpdateErrorSummary. This function is used to set the content of the error summery control. So it sets the text of the TextBlock element according to the validation errors. This is implemented by joining all single errors.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region Validation

        private void UpdateErrorSummary()
        {
            string errors = null;

            if (_person.IsValid == false)
            {
                errors = string.Join(Environment.NewLine, _person.ValidationErrors);
            }

            TextBlockAllErrors.Text = errors;
        }

        #endregion
    }
}

 

The validation for each single control is done by implementing the IDataErrorInfo interface. As described it contains two properties. We will use the second one, which will get the validation errors of a single element. So for the first property, which we don’t use, we can always return an empty string. For the data validation of each property we can directly work with the name of the property as we have used this name as key for the PropertyCollection member. So we can get the according property from the collection by using the name and ask for the validation errors. As this is a list we will join all errors to return them in a single string which is used in the user interface for the error tool tip.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region Validation

        public string Error
        {
            get
            {
                return "";
            }
        }

        public string this[string name]
        {
            get
            {
                string result = null;

                if (_person.ContainsKey(name))
                {
                    if (_person[name].IsValid == false)
                    {
                        result = string.Join(" ", _person[name].ValidationErrors);
                    }
                }

                return result;
            }
        }

        #endregion
    }
}

 

Last but not least I have implemented some event receivers. As the user interface is loaded, I have to set the DataContext. Furthermore the two buttons have click events. If the Save button is pressed, we will commit all changes. If the Reset button pressed we will reset all changes by using the according function of our data class. And of course, as we have used a quick and dirty implementation instead of the MVVM pattern, we have to call the update functions for the buttons and the error summary in all the different events.

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region Events

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = this;

            UpdateButtons();
            UpdateErrorSummary();
        }

        private void ButtonSave_Click(object sender, RoutedEventArgs e)
        {
            _person.AcceptChanges();
            UpdateButtons();
            UpdateErrorSummary();
        }

        private void ButtonReset_Click(object sender, RoutedEventArgs e)
        {
            _person.RevertChanges();

            TextBoxFirstName.Text = _person.FirstName;
            TextBoxLastName.Text = _person.LastName;
            TextBoxAge.Text = _person.Age.ToString();
            
            UpdateButtons();
            UpdateErrorSummary();
        }

        #endregion
    }
}

 
Final implementation

So far, we have seen all the implemented single features. Now it’s time to show the complete final classes. Following you will find the source code of the data class the user interface and the code behind for the user interface.

PersonData

using System;
using System.Collections.ObjectModel;

namespace UserInterface
{
    public class PersonData : PropertyCollection
    {
        #region ctor

        public PersonData()
        {
            Property<string> firstName;
            Property<string> lastName;
            Property<int> age;

            //---init member and set validators---
            //first name
            Func<string, ObservableCollection<string>> firstNameValidator = delegate(string value)
            {
                if (string.IsNullOrEmpty(value))
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("First name should not be empty");

                    return errors;
                }

                return null;
            };

            firstName = new Property<string>(firstNameValidator);

            //last name
            Func<string, ObservableCollection<string>> lastNameValidator = delegate(string value)
            {
                if (string.IsNullOrEmpty(value))
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("Last name should not be empty");

                    return errors;
                }

                return null;
            };

            lastName = new Property<string>(lastNameValidator);

            //age
            Func<int, ObservableCollection<string>> ageValidator = delegate(int value)
            {
                if (value < 18)
                {
                    ObservableCollection<string> errors = new ObservableCollection<string>();
                    errors.Add("Age must be 18 or higher");

                    return errors;
                }

                return null;
            };

            age = new Property<int>(ageValidator);

            //---init collection---
            Add("FirstName", firstName);
            Add("LastName", lastName);
            Add("Age", age);
        }

        #endregion

        #region Properties

        public string FirstName
        {
            get { return GetProperty<string>("FirstName").Value; }
            set { GetProperty<string>("FirstName").Value = value; }
        }

        public string LastName
        {
            get { return GetProperty<string>("LastName").Value; }
            set { GetProperty<string>("LastName").Value = value; }
        }

        public int Age
        {
            get { return GetProperty<int>("Age").Value; }
            set { GetProperty<int>("Age").Value = value; }
        }

        #endregion
    }
}

 

User Interface xaml

<Window x:Class="UserInterface.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        Loaded="Page_Loaded">
    
    <Window.Resources>
        <!--The tool tip for the TextBox to display the validation error message.-->
        <Style x:Key="textBoxInError" TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions> 

        <Label Grid.Column="0" Grid.Row="0">First Name</Label>
        <Label Grid.Column="0" Grid.Row="1">Last Name</Label>
        <Label Grid.Column="0" Grid.Row="2">Age</Label>

        <TextBox Grid.Column="1" Grid.Row="0" Name="TextBoxFirstName" Style="{StaticResource textBoxInError}"
                   Text="{Binding Path=FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}">
        </TextBox>

        <TextBox Grid.Column="1" Grid.Row="1" Name="TextBoxLastName" Style="{StaticResource textBoxInError}"
                   Text="{Binding Path=LastName, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}">
        </TextBox>

        <TextBox Grid.Column="1" Grid.Row="2" Name="TextBoxAge" Style="{StaticResource textBoxInError}"
                   Text="{Binding Path=Age, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}">
        </TextBox>

        <StackPanel Orientation="Horizontal" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="3">
            <Button MinWidth="80" Margin="5" Name="ButtonSave" Click="ButtonSave_Click">Save</Button>
            <Button MinWidth="80" Margin="5" Name="ButtonReset" Click="ButtonReset_Click">Reset</Button>
        </StackPanel>

        <TextBlock Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="4" Name="TextBlockAllErrors" Foreground="Red">
        </TextBlock>

    </Grid>
</Window>

 

User Interface code behind

using System;
using System.Windows;
using System.ComponentModel;

namespace UserInterface
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IDataErrorInfo
    {
        #region ctor

        public MainWindow()
        {
            InitializeComponent();

            _person = new PersonData() { FirstName = "John", LastName = "Doe", Age = 28 };
            _person.AcceptChanges();
        }

        #endregion

        #region Properties

        public string FirstName
        {
            get
            {
                return _person.FirstName;
            }
            set
            {
                _person.FirstName = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        public string LastName
        {
            get
            {
                return _person.LastName;
            }
            set
            {
                _person.LastName = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        public int Age
        {
            get
            {
                return _person.Age;
            }
            set
            {
                _person.Age = value;
                UpdateButtons();
                UpdateErrorSummary();
            }
        }

        #endregion

        #region Events

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = this;

            UpdateButtons();
            UpdateErrorSummary();
        }

        private void ButtonSave_Click(object sender, RoutedEventArgs e)
        {
            _person.AcceptChanges();
            UpdateButtons();
            UpdateErrorSummary();
        }

        private void ButtonReset_Click(object sender, RoutedEventArgs e)
        {
            _person.RevertChanges();

            TextBoxFirstName.Text = _person.FirstName;
            TextBoxLastName.Text = _person.LastName;
            TextBoxAge.Text = _person.Age.ToString();
            
            UpdateButtons();
            UpdateErrorSummary();
        }

        #endregion

        #region Content Management

        private void UpdateButtons()
        {
            ButtonSave.IsEnabled = _person.IsChanged;
            ButtonReset.IsEnabled = _person.IsChanged;
        }

        #endregion

        #region Validation

        public string Error
        {
            get
            {
                return "";
            }
        }

        public string this[string name]
        {
            get
            {
                string result = null;

                if (_person.ContainsKey(name))
                {
                    if (_person[name].IsValid == false)
                    {
                        result = string.Join(" ", _person[name].ValidationErrors);
                    }
                }

                return result;
            }
        }

        private void UpdateErrorSummary()
        {
            string errors = null;

            if (_person.IsValid == false)
            {
                errors = string.Join(Environment.NewLine, _person.ValidationErrors);
            }

            TextBlockAllErrors.Text = errors;
        }

        #endregion

        #region Member

        private PersonData _person;

        #endregion  
    }
}
Werbung
Dieser Beitrag wurde unter .NET, C# veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s