DEV Community

Cover image for Window forms data annotation
Karen Payne
Karen Payne

Posted on

Window forms data annotation

Validation is the first and most important step in securing an application. It prevents the application from processing unwanted inputs that may produce unpredictable results. Couple validation with properly transmitting data to a data source.

When validating data should there be instant feedback? This is subjective, instant feedback will be better when there are many inputs so this would be better than waiting to submit their input. In the case of instant feedback there needs to be events triggered to perform validation while on submit there is a central generic method to perform validation.

Experience developers

Go to the source code and run the code

Requires

Microsoft Visual Studio 2022 or later.

Data Source

No databases have been used here, this simply means code provided focuses on validating data. Suppose Entity Framework Core is being used, the code provides works with Entity Framework Core. What about DataSet/DataTable, first a DataTable must be converted to a strong type class list or a DataRow converted to a class instance

Data Annotations

Data Annotations are nothing but a set of attributes which can be used to configure your model classes to validate the input data entered by users. It provides a set of .NET attributes that can be applied to data object class properties. These attributes offer a very declarative way to apply validation rules directly to a model.

Note
A validation rule may be a property can not be empty, have a min and or max value etc.

Data Annotations are a general purpose mechanism which can be used to feed metadata to the framework. Framework drives validation from the metadata, and uses the metadata while building the HTML to display and edit models. Well, you can manually validate the view model is ASP.NET Core and Windows Forms too, but using data annotation makes you validation logic reusable and also saves time. In addition, it also minimizes the complexity in the action methods. The idea is to add constraints metadata to the properties in the view model (or in Windows Forms), which can be later picked up by the default binder in the model-binding process.

Besides what the .NET Framework provides a developer can create their own, for instance, a rule for disallowing a date to be on a weekend.

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class WeekendDateNotPermittedAttribute : ValidationAttribute
{
    /// <summary>
    ///  Override of <see cref="ValidationAttribute.IsValid(object)" />
    /// </summary>
    public override bool IsValid(object senderDate)
    {
        DateTime date = Convert.ToDateTime(senderDate);
        return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage in a class on a date property.

[WeekendDateNotPermitted]
[Display(Prompt = "Next appointment date")]
public DateTime AppointmentDate { get; set; }
Enter fullscreen mode Exit fullscreen mode

Data annotations are easy to use in ASP.NET Core and Razor pages which display appropriate error messages and sometimes image .

web page with invalid entries

Although Windows forms can also use data annotations its not easy for a novice developer to figure out how too.

Using data annotation in windows forms

In this case the goal is to display a error icon using an ErrorProvider component when an input is not valid.

form with validation errors

First a class/model is needed for storing data, in this case the following class Customers.

  • The Display attribute is not used for Windows Forms, its included for use in a web project and does no harm being here.
  • The Required attribute specifies that a data field value is required.
  • ValidCountryNameAttribute is included in the source code
  • WeekendDateNotPermitted is included in the source code
  • SocialSecurityAttribute is included in the source code

INotifyPropertyChanged is optional, this interface provides change notification to the user interface while many developers get this with a DataTable we will not be using DataTable containers. Get use to using classes and when moving to web consider Entity Framework Core.

public class Customer : INotifyPropertyChanged
{
    private ValidatingFormProject.Models.Country _country;
    private DateTime _birthDate;

    [Key]
    public int Id { get; set; }

    [Required(ErrorMessage = "{0} is required"), DataType(DataType.Text)]
    [StringLength(12,MinimumLength = 3,  ErrorMessage = "{0} {2} min {1} max and not empty")]
    [Display(Name = "First name")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "{0} is required"), DataType(DataType.Text)]
    [MaxLength(12, ErrorMessage = "The {0} can not have more than {1} characters")]
    [Display(Name = "Last name")]
    public string LastName { get; set; }

    [Required(ErrorMessage = "The email address is required")]
    [EmailAddress(ErrorMessage = "Invalid Email Address")]
    [Display(Name = "Personal email address")]
    public string Email { get; set; }

    [Display(Name = "Credit limit")]
    public decimal CreditLimit { get; set; }

    [CreditCard]
    [Required]
    [Display(Name = "Credit card number")]
    public string CreditCardNumber { get; set; }

    [Display(Name = "Current discount")]
    public int Discount { get; set; }

    public bool HasDiscount { get; set; }

    [Display(Name = "Street")]
    public string Address { get; set; }

    [ValidPostalCode]
    [Display(Name = "Zip code")]
    public string PostalCode { get; set; }

    [WeekendDateNotPermitted]
    [Display(Prompt = "Next appointment date")]
    public DateTime AppointmentDate { get; set; }

    [Required]
    public Country Country
    {
        get => _country;
        set
        {
            _country = value;
            OnPropertyChanged();
        }
    }
    [ValidCountryName(ErrorMessage = "Country name is required")]
    public string CountryName => Country.CountryName;

    [ValidPin]
    [Display(Prompt = "Security pin")]
    public string Pin { get; set; }

    [SocialSecurityAttribute]
    [Display(Prompt = "SSN")]
    public string SocialSecurity { get; set; }

    [YearRange(maximumYear: 2022, MinimumYear = 1932)]
    [Display(Prompt = "Birth date")]
    public DateTime BirthDate
    {
        get => _birthDate;
        set
        {
            _birthDate = value;
            OnPropertyChanged();
        }
    }

    [ListMustContainFewerThan(5)]
    public List<string> NotesList { get; set; }

    public override string ToString() => $"{FirstName} {LastName}";


    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}
Enter fullscreen mode Exit fullscreen mode

Validation code

All validation methods are located in a class project so they can be used in other projects.

Base library classes

Key methods

IsValidEntity accepts a instantiated object which has data annotations lie in the Customer class/model above and validates each property which is annotated.

public class ValidationHelper
{
    /// <summary>
    /// Validate entity against validation rules
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="entity"></param>
    /// <returns></returns>
    public static EntityValidationResult ValidateEntity<T>(T entity) where T : class 
        => (new EntityValidator<T>()).Validate(entity);

    /// <summary>
    /// Assert entity is valid against data annotated properties
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="entity"></param>
    /// <returns></returns>
    public static (bool success, List<ErrorContainer> container) IsValidEntity<T>(T entity) where T : class
    {
        List<ErrorContainer> list = new();

        var result = ValidateEntity(entity);

        if (result.IsNotValid)
        {
            if (result.Errors.Count == 1)
            {

                list.Add(new ErrorContainer()
                {
                    Name = result.Errors.FirstOrDefault()!.MemberNames.FirstOrDefault(),
                    Description = result.Errors.FirstOrDefault()!.ErrorMessage
                });

                return (false, list);
            }
            else
            {
                foreach (ValidationResult resultError in result.Errors)
                {
                    list.Add(new ErrorContainer()
                    {
                        Name = resultError.MemberNames.FirstOrDefault(),
                        Description = resultError.ErrorMessage
                    });
                }

                return (false,list);
            }

        }
        else
        {
            return (true, null);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling the above method on a Customer object.

var (success, container) = IsValidEntity(customer);
Enter fullscreen mode Exit fullscreen mode

If success is false than container has a list of issues of type ErrorContainer.

public class ErrorContainer
{
    public string Name { get; set; }
    public string Description { get; set; }
    public override string ToString() => Name;

}
Enter fullscreen mode Exit fullscreen mode
  • Name is the property name
  • Description is the error message from a property

To properly associate a ErrorContainer item with a Form control we set the Tag property for each control in the form designer.

A form level variable is used to store controls with a Tag property set.

private readonly Dictionary<string, Control> _controls = new();
.
.
.
IEnumerable<Control> items = this.Descendants<Control>()
    .Where(x => x.Tag is not null);

foreach (Control item in items)
{
    _controls.Add(item.Tag!.ToString()!, item);
}
Enter fullscreen mode Exit fullscreen mode

In a button click event to trigger validation we first clear all controls of ErrorProvider text.

foreach (var control in _controls)
{
    errorProvider1.SetError(control.Value,"");
}
Enter fullscreen mode Exit fullscreen mode

Than when there are issue with validation, find the control in the dictionary and set the error text.

foreach (var item in container)
{
    if (_controls.TryGetValue(item.Name, out var control))
    {
        errorProvider1.SetError(control, item.Description);
    }
}
Enter fullscreen mode Exit fullscreen mode

That is all that is needed to provide validation.

Explore the custom classes for validation in the source code which can be used to create your own.

A good example is the class LoginCompare which provides a method to have the user enter a password in a TextBox and in another TextBox confirm the password. There is an example that does not mask either passwords on purpose and if the passwords do not match we tell the user it's invalid, you can surely give them more help.

password form

public class LoginCompare
{

    [Required(ErrorMessage = "{0} is required"), DataType(DataType.Text)]
    [MaxLength(12, ErrorMessage = "The {0} can not have more than {1} characters")]
    public string Name { get; set; }

    [Required(ErrorMessage = "{0} is required"), DataType(DataType.Text)]
    [StringLength(12, MinimumLength = 6)]
    public string Password { get; set; }
    [Compare("Password", ErrorMessage = "Passwords do not match, please try again")]
    [StringLength(12, MinimumLength = 6)]
    public string PasswordConfirmation { get; set; }

}
Enter fullscreen mode Exit fullscreen mode

Simple usage

using BaseDataValidatorLibrary.CommonRules;
using static BaseDataValidatorLibrary.Helpers.ValidationHelper;

namespace ValidatingFormProject;
public partial class LoginForm : Form
{
    public LoginForm()
    {
        InitializeComponent();
    }

    private void ValidateButton_Click(object sender, EventArgs e)
    {
        LoginCompare loginCompare = new LoginCompare()
        {
            Name = NameTextBox.Text,
            Password = PasswordTextBox.Text,
            PasswordConfirmation = PasswordConfirmTextBox.Text,
        };
        var (success, container) = IsValidEntity(loginCompare);
        MessageBox.Show(success ? "Ok" : "Invalid");
    }
}
Enter fullscreen mode Exit fullscreen mode

Another example, for a property that accepts a postal code, the rule is that only specific postal codes are valid.

In the example below the valid postal codes are in code but they could come from a data source.

/// <summary>
/// Made up rules to validate a postal code
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ValidPostalCodeAttribute : ValidationAttribute
{
    /// <summary>
    ///  Override of <see cref="ValidationAttribute.IsValid(object)" />
    /// </summary>
    public override bool IsValid(object postalCode)
    {
        if (postalCode is null)
        {
            return false;
        }

        var value = postalCode.ToString();

        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        List<string> list = new() { "97301", "97223", "97209", "97146", "97374", "97734" };
        var result = list.FirstOrDefault(item => item == value);
        return result is not null;
    }
}
Enter fullscreen mode Exit fullscreen mode

How to use in your project

πŸ’‘ First, rather than copy code, take time to understand the supplied code.

Add the project BaseDataValidatorLibrary to your Visual Studio solution and add it as a reference to your Windows Form project.

See also

FluentValidation tips C#

Source code

Clone the following GitHub repository

Top comments (0)