Window forms data annotation
Tags: codenewbie, dotnetcore, csharp
Published: 2023-04-28 21:04:59

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](https://github.com/karenpayneoregon/data-anntations-win-forms) 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](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-8.0) 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.

```csharp
[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;
    }
}
```

Usage in a class on a date property.

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

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dmfy4hlcnv161u4n1rc2.png)

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](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.errorprovider?view=windowsdesktop-7.0) component when an input is not valid.


![form with validation errors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yx3k1981pcnkcr1jl55g.png)

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

- The [Display](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute?view=net-8.0) attribute is not used for Windows Forms, its included for use in a web project and does no harm being here.
-  The [Required](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute?view=net-8.0) 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.

```csharp
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));
    }

}
```

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


![Base library classes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/resorc6kg54x1l88myk6.png)

## 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.

```csharp
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);
        }
    }
}
```

Calling the above method on a Customer object.

```csharp
var (success, container) = IsValidEntity(customer);
```

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

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

}
```

- 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.

```csharp
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);
}
```
In a button click event to trigger validation we first clear all controls of ErrorProvider text.

```csharp
foreach (var control in _controls)
{
    errorProvider1.SetError(control.Value,"");
}
```

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

```csharp
foreach (var item in container)
{
    if (_controls.TryGetValue(item.Name, out var control))
    {
        errorProvider1.SetError(control, item.Description);
    }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zy7xsk2r3qfy36vvjua9.png)

```csharp
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; }

}
```
Simple usage

```csharp
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");
    }
}
```
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.

```csharp
/// <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;
    }
}
```


## How to use in your project

:bulb: 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#](https://dev.to/karenpayneoregon/fluentvalidation-tips-c-3olf)

## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/data-anntations-win-forms)