.NET: Defensive data programming (Part 4) Data Annotation
Introduction
Validating data in windows forms application is generally done by determining if the information entered by a user is the proper type e.g. price of a product is type decimal, the first and last name of a person entered are not empty strings. In a conventional application, a developer will have logic in a button click event to validate all information entered on a form which has worked for many, yet this logic is locked into a single form/window. For ASP.NET MVC Data Annotations are used to validate against a model (a class) using built-in attributes to validate members in a model/class along with the ability to override built-in attributes and create custom attributes. In this article, exploration will be done to show how to use built-in and custom Data Annotation Attributes in windows forms applications.
.NET: Defensive data programming (part 1)
.NET: Defensive data programming (Part 2)
.NET: Defensive data programming (Part 3)
ASP.NET MVC Data Annotation
Using these attributes provide methods to display meaningful text for displaying field names and error text as shown below. Note the red text is the text to display when a rule for validation fails which is done for you via the validator.
The view where on the first line PersonModel is the class container for validating against. Pressing the submit button will activate the validation.
@model TextBox_Validation_MVC.Models.PersonModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<style type="text/css">
body {
font-family: Arial;
font-size: 10pt;
}
.error {
color: red;
}
</style>
</head>
<body>
<div class="container-fluid">
@using (@Html.BeginForm("Index", "Home", FormMethod.Post))
{
<h2>Simple data validation</h2>
<h3>Defensive programming part 3</h3>
<table>
<tr>
<td>
@Html.Label("First name",
new {@class = "float-right form-control-sm"})
</td>
<td>
@Html.TextBoxFor(pm => pm.FirstName,
new {@class = "form-control-sm"})
</td>
<td>
@Html.ValidationMessageFor(pm => pm.FirstName,
"",
new {@class = "error"})
</td>
</tr>
<tr>
<td>
@Html.Label("Last name",
new {@class = "float-right form-control-sm"})
</td>
<td>
@Html.TextBoxFor(pm => pm.LastName,
new {@class = "form-control-sm"})
</td>
<td>
@Html.ValidationMessageFor(m => m.LastName,
"",
new {@class = "error"})
</td>
</tr>
<tr>
<td>
@Html.Label("Ssn",
new {@class = "float-right form-control-sm"})
</td>
<td>
@Html.TextBoxFor(pm => pm.Ssn,
new {@class = "form-control-sm"})
</td>
<td>
@Html.ValidationMessageFor(m => m.Ssn,
"", new {@class = "error"})
</td>
</tr>
<tr>
<td></td>
<td><input type="submit" class="btn btn-primary" value="Submit" /></td>
<td></td>
</tr>
</table>
}
</div>
</body>
@Scripts.Render("~/bundles/jquery")
</html>
Windows Forms Data Annotation
When working with data annotations in windows forms functionality for displaying field names as done in ASP.NET MVC is not possible without a good deal of coding which is beyond the scope of this article, similarly for displaying error messages as done in ASP.NET MVC unless a ErrorProvider component is implemented to display error messages by cycling through each error message and set the error text for each control (TextBox, ComboBox etc). Here the focus will be on displaying error messages in a dialog.
Using the login shown above here is the Windows form version.
Step 1, add a reference to System.ComponentModel.DataAnnotations to the project. Add the following three classes to the project.
EntityValidationResult
EntityValidator
ValidationHelper
Step 2, add a class to represent data which is to be collected in a windows form.
Imports System.ComponentModel.DataAnnotations
Namespace Classes
Public Class Taxpayer
<RegularExpression("^\d{9}|\d{3}-\d{2}-\d{4}$",
ErrorMessage:="Invalid Social Security Number")>
<Required(ErrorMessage:="Contact {0} is required"),
DataType(DataType.Text)>
Public Property SSN() As String
<Required(ErrorMessage:="{0} is required"),
DataType(DataType.Text)>
Public Property FirstName() As String
<Required(ErrorMessage:="{0} is required"),
DataType(DataType.Text)>
Public Property LastName() As String
End Class
End Namespace
For the property SSN, the attribute RegularExpression is the rule to validate there is a good SSN which is a generic rule as for a full validation of an SSN there can be many rules such as.
A Social security number cannot
- Contain all zeroes in any specific group (ie 000-##-####, ###-00-####, or ###-##-0000)
- Begin with ’666′.
- Begin with any value from ’900-999′
- Be ’078-05-1120′ (due to the Woolworth’s Wallet Fiasco)
- Be ’219-09-9999′ (appeared in an advertisement for the Social Security Administration)
To be able to run all of these rules a special validator is in order which will be gone over later.
Step 3, create a new form, SocialSecurityForm.vb with
- TextBox for FirstName
- TextBox for LastName
- TextBox for SSN
- Button for validation
To validate entries the following code is used in the validate button click event. In the code shown below the import, statements point to classes in the project (which is included in the source code for this article).
Imports BasicClassValidation.Classes
Imports BasicClassValidation.LanguageExtensions
Imports BasicClassValidation.Validators
Namespace Forms
Public Class SocialSecurityForm
Private Sub LogInButton_Click(sender As Object, e As EventArgs) _
Handles LogInButton.Click
'
' Note that many people like to use SSN format 111-11-1111 so we remove dashes.
' Alternately a mask may be used on the TextBox.
'
Dim taxpayer As New Taxpayer With
{
.FirstName = FirstNameTextBox.Text,
.LastName = LastNameTextBox.Text,
.SSN = SsnTextBox.Text.Replace("-", "")
}
Dim validationResult As EntityValidationResult =
ValidationHelper.ValidateEntity(taxpayer)
If validationResult.HasError Then
MessageBox.Show(validationResult.ErrorMessageList())
Else
DialogResult = DialogResult.OK
End If
End Sub
End Class
End Namespace
Notes:
For SSN validation
<RegularExpression("^\d{9}|\d{3}-\d{2}-\d{4}$",
ErrorMessage:="Invalid Social Security Number")>
<Required(ErrorMessage:="Contact {0} is required"),
DataType(DataType.Text)>
Public Property SSN() As String
RegularExpression is the rule to validate while ErrorMessage is the text to display which data entered does not match the rule and DataType(DataType.Text) is the type for the property. If the field name can be shown and be meaningful to the user the following can be used where {0} at runtime is replaced with the field name, in this case SSN,
ErrorMessage:="Invalid {0}")
For fields such as
<Required(ErrorMessage:="{0} is required"),
DataType(DataType.Text)>
Public Property FirstName() As String
The error message will be FirstName is required. This may not be pleasing to some. Included in the source code is the following language extension method.
Public Module StringExtensions
<Runtime.CompilerServices.Extension>
Public Function SplitCamelCase(sender As String) As String
Return Regex.Replace(
Regex.Replace(sender,
"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), "(\p{Ll})(\P{Ll})", "$1 $2")
End Function
End Module
The language extension is used to take field names such as FirstName and split them per capital letters so FirstName turns into First Name. Still in the social security form.
Dim validationResult As EntityValidationResult =
ValidationHelper.ValidateEntity(taxpayer)
If validationResult.HasError Then
MessageBox.Show(validationResult.ErrorMessageList())
Else
DialogResult = DialogResult.OK
End If
ErrorMessageList is a language extension method which iterates through the validator errors, collects each error, splits fields like FirstName and removes any double spaces from the error message and places the error messages into a StringBuilder then returns a string to present in a MessageBox.
Imports System.ComponentModel.DataAnnotations
Imports System.Text
Imports System.Text.RegularExpressions
Imports BasicClassValidation.Validators
Namespace LanguageExtensions
Public Module ValidatorExtensions
''' <summary>
''' Separates tokens with a space e.g. ContactName becomes Contact Name
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
<Runtime.CompilerServices.Extension>
Public Function SanitizedErrorMessage(sender As ValidationResult) As String
Return Regex.Replace(sender.ErrorMessage.SplitCamelCase(), " {2,}", " ")
End Function
''' <summary>
''' Place all validation messages into a string with each validation message on one line
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
<Runtime.CompilerServices.Extension>
Public Function ErrorMessageList(sender As EntityValidationResult) As String
Dim sb As New StringBuilder
sb.AppendLine("Validation issues")
For Each errorItem As ValidationResult In sender.Errors
sb.AppendLine(errorItem.SanitizedErrorMessage)
Next
Return sb.ToString()
End Function
End Module
End Namespace
Compare attribute
Using the compare attribute provides an easy method to confirm two properties have the same value. A good example is when a new user is registering and must confirm their passwords.
<Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
<StringLength(12, MinimumLength:=6)>
Public Property Password() As String
<Compare("Password", ErrorMessage:="Passwords do not match, please try again")>
<StringLength(12, MinimumLength:=6)>
Public Property PasswordConfirmation() As String
The complete code sample for the above is found in this form. Note that the same validator EntityValidationResult used in the Taxpayer example above is used in this code sample, this keeps everything consistent no matter which class is being validated.
Imports BasicClassValidation.Classes
Imports BasicClassValidation.LanguageExtensions
Imports BasicClassValidation.Validators
Namespace Forms
Public Class LoginForm
Private Sub LogInButton_Click(sender As Object, e As EventArgs) _
Handles LogInButton.Click
Dim login As New CustomerLogin With
{
.Name = UserNameTextBox.Text,
.Password = PasswordTextBox.Text,
.PasswordConfirmation = PasswordConfirmTextBox.Text,
.EntryDate = EntryDateDateTimePicker.Value
}
Dim validationResult As EntityValidationResult = ValidationHelper.ValidateEntity(login)
If validationResult.HasError Then
MessageBox.Show(validationResult.ErrorMessageList())
Else
DialogResult = DialogResult.OK
End If
End Sub
End Class
End Namespace
If the validation passes the form (shown modal) is closed where the calling form can get data from the TaxPayer (in a real app the instance of CustomerLogin would be a public read only property of the form).
StringLength attribute
This attribute provides the ability to specify the max and min length of a string. For instance, a field FirstName in a database table can only store 50 characters, the following will check the value passed to see if the length exceeds this length. For the min this prevents a user entering a space. The full rule is
- Required indicates the field can not be empty.
- MinimumLength indicates at least three characters are required.
- 50 is the max length.
<Required(ErrorMessage:="{0} is Required"),
StringLength(50, MinimumLength:=3)>
Public Property FirstName() As String
Besides StringLength, MaxLength(X) can be used to specify a max length of a string.
Range attribute
Use this attribute to constrain the range a property can fall into.
Public Class Product
<Range(10, 1000, ErrorMessage:="Value for {0} must be between {1} and {2}.")>
Public Weight As Object
<Range(300, 3000)>
Public ListPrice As Object
<Range(GetType(DateTime), "1/2/2019", "3/4/2019",
ErrorMessage:="Value for {0} must be between {1} and {2}")>
Public SellEndDate As Object
End Class
Although the range attribute handles numerics great there may be times when a date rule is needed that just does not fit into the range e.g. a property value can not be a weekend date. In this case a custom class is required.
Namespace Rules
''' <summary>
''' Checks for if a date falls on a weekend.
''' </summary>
Public Class CustomerWeekendValidation
Public Shared Function WeekendValidate(senderDate As Date) As ValidationResult
Return If(senderDate.DayOfWeek = DayOfWeek.Saturday OrElse
senderDate.DayOfWeek = DayOfWeek.Sunday,
New ValidationResult("The weekend days are not valid"),
ValidationResult.Success)
End Function
End Class
End Namespace
Sample implementation specifying the rule using CustomValidation class. See this in use in the following form.
<CustomValidation(GetType(CustomerWeekendValidation),
NameOf(CustomerWeekendValidation.WeekendValidate))>
Public Property EntryDate() As Date
Dealing with Enumerations
One of the most difficult items to validate on are Enum types. In short for validating Enum types using the following as an example
Namespace Enumerations
Public Enum BookCategory
SpaceTravel
Adventure
Romance
Sports
Automobile
End Enum
End Namespace
Change to the following where there is a zero member.
Namespace Enumerations
Public Enum BookCategory
[Select] = 1
SpaceTravel = 2
Adventure = 3
Romance = 4
Sports = 5
Automobile = 6
End Enum
End Namespace
Besides numbering members an additional member has been added to provide visual confirmation that a selection is required. The following demonstrates this in use where a book class shown below uses a custom rule, RequiredEnumAttruibute which checks first if a value has been set then if not empty validates it's a valid enum..
Namespace Classes
Public Class Book
<Required(ErrorMessage:="{0} is required")>
Public Property Title() As String
<Required(ErrorMessage:="{0} is required")>
Public Property ISBN() As String
<RequiredEnum(ErrorMessage:="{0} is required.")>
Public Property Category() As BookCategory
<ListHasElements(ErrorMessage:="{0} must contain at lease one note")>
Public Property NotesList() As List(Of String)
End Class
End Namespace
Here a form is setup to collect information on a book where categories are displayed in a ComboBox. Pressing the validate button runs the same validation as in prior examples with the difference of converting the Category selection from string to Enum then determining if a valid selection has been made where "Select" is not valid.
Namespace Forms
Public Class ListAndEnmForm
Private Sub ListAndEnmForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
CategoryComboBox.Items.AddRange([Enum].GetNames(GetType(BookCategory)))
CategoryComboBox.SelectedIndex = 0
End Sub
Private Sub ValidateButton_Click(sender As Object, e As EventArgs) Handles ValidateButton.Click
Dim category = DirectCast([Enum].Parse(GetType(BookCategory), CategoryComboBox.Text), BookCategory)
Dim book = New Book() With
{
.Title = BookTitleTextBox.Text,
.ISBN = IsbnTextBox.Text,
.Category = If(category = BookCategory.Select, Nothing, category),
.NotesList = TextBox1.Lines.ToList()
}
Dim validationResult = ValidationHelper.ValidateEntity(book)
If validationResult.HasError Then
MessageBox.Show(validationResult.ErrorMessageList())
Else
DialogResult = DialogResult.OK
End If
End Sub
End Class
End Namespace
List properties
Not all properties of a class a numeric, string, date or enum, there are cases where a property may be a list. The following custom class which inherits from the class ValidationAttribute which is the base class for creating custom validation rules.
Namespace Rules
Public Class ListHasElements
Inherits ValidationAttribute
Public Overrides Function IsValid(sender As Object) As Boolean
If sender Is Nothing Then
Return False
End If
If IsList(sender) Then
Dim result = CType(sender, IEnumerable).Cast(Of Object)().ToList()
Return result.Any()
Else
Return False
End If
End Function
End Class
End Namespace
Support method
Namespace HelperModules
Public Module ObjectHelper
Public Function IsList(sender As Object) As Boolean
If sender Is Nothing Then
Return False
End If
Return TypeOf sender Is IList AndAlso
sender.GetType().IsGenericType AndAlso
sender.GetType().GetGenericTypeDefinition().IsAssignableFrom(GetType(List(Of )))
End Function
End Module
End Namespace
IsList function determines if sender (of type object) is of type list and can represent List(Of. If so checks using the extension method Any to see if the list has any items. Validation passes if the sender is assigned, is a list and has at least one item.
Implemented in a class property NoteList of List of String.
Namespace Classes
Public Class Book
<Required(ErrorMessage:="{0} is required")>
Public Property Title() As String
<Required(ErrorMessage:="{0} is required")>
Public Property ISBN() As String
<RequiredEnum(ErrorMessage:="{0} is required.")>
Public Property Category() As BookCategory
<ListHasElements(ErrorMessage:="{0} must contain at lease one note")>
Public Property NotesList() As List(Of String)
End Class
End Namespace
Although this article is VB.NET here is a simplified version done in C#.
namespace DataValidatorLibrary.CommonRules
{
public class ListHasElements : ValidationAttribute
{
public override bool IsValid(object sender)
{
if (sender == null)
{
return false;
}
if (sender.IsList())
{
var result = ((IEnumerable)sender).Cast<object>().ToList();
return result.Any();
}
else
{
return false;
}
}
}
}
Using a language extension (where VB.NET can not have extension methods against type Object)
namespace DataValidatorLibrary.LanguageExtensions
{
public static class ObjectExtensions
{
public static bool IsList(this object sender)
{
if (sender == null) return false;
return sender is IList &&
sender.GetType().IsGenericType &&
sender.GetType().GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>));
}
}
}
Phone number validation
Although there is a standard attribute for phone type, this can be another property that may need special consideration. As with the last example, a custom class can be created to apply special rules as per below.
Imports System.ComponentModel.DataAnnotations
Namespace Rules
''' <summary>
''' Provides custom rule for phone number rather than using [Phone]
''' </summary>
Public Class CheckPhoneValidationAttribute
Inherits ValidationAttribute
Public Overrides Function IsValid(value As Object) As Boolean
If value Is Nothing Then
Return False
End If
Dim convertedValue As String = value.ToString()
Return (Not String.IsNullOrWhiteSpace(convertedValue)) AndAlso
IsDigitsOnly(convertedValue) AndAlso convertedValue.Length <= 10
End Function
Private Function IsDigitsOnly(str As String) As Boolean
For Each c In str
If c < "0"c OrElse c > "9"c Then
Return False
End If
Next
Return True
End Function
End Class
End Namespace
In C#
using System.ComponentModel.DataAnnotations;
namespace DataValidatorLibrary.CommonRules
{
/// <summary>
/// Provides custom rule for phone number rather than using [Phone]
/// </summary>
public class CheckPhoneValidationAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
/*
* VS2017 or higher
*/
bool IsDigitsOnly(string str)
{
foreach (var c in str)
{
if (c < '0' || c > '9')
return false;
}
return true;
}
if (value == null)
{
return false;
}
string convertedValue = value.ToString();
return !string.IsNullOrWhiteSpace(convertedValue) &&
IsDigitsOnly(convertedValue) &&
convertedValue.Length <= 10;
}
}
}
ErrorProvider implementation
So far all error messages have been displayed in a MessageBox, another option is to place an ErrorProvider on the form that requires validation. In the code block which follows ErrorMessageList has been used to show validation error messages in a MessageBox while ErrorItemList creates a list of ErrorContainer which stores the property name in PropertyName member of the property in the class which is being validated and ErrorMessage member contains the error message for the validation of the control.
Namespace LanguageExtensions
Public Module ValidatorExtensions
''' <summary>
''' Separates tokens with a space e.g. ContactName
''' becomes Contact Name
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
<Runtime.CompilerServices.Extension>
Public Function SanitizedErrorMessage(sender As ValidationResult) As String
Return Regex.Replace(sender.ErrorMessage.SplitCamelCase(), " {2,}", " ")
End Function
''' <summary>
''' Place all validation messages into a string with
''' each validation message on one line
''' </summary>
''' <param name="sender"></param>
''' <returns></returns>
<Runtime.CompilerServices.Extension>
Public Function ErrorMessageList(sender As EntityValidationResult) As String
Dim sb As New StringBuilder
sb.AppendLine("Validation issues")
For Each errorItem As ValidationResult In sender.Errors
sb.AppendLine(errorItem.SanitizedErrorMessage)
Next
Return sb.ToString()
End Function
<Runtime.CompilerServices.Extension>
Public Function ErrorItemList(sender As EntityValidationResult) As List(Of ErrorContainer)
Dim itemList As New List(Of ErrorContainer)
For Each errorItem As ValidationResult In sender.Errors
itemList.Add(New ErrorContainer() With
{
.PropertyName = errorItem.MemberNames.FirstOrDefault(),
.ErrorMessage = errorItem.SanitizedErrorMessage
})
Next
Return itemList
End Function
End Module
End Namespace
In the button used to validate data entered, the following line clears each TextBox error message using a language extension method.
Descendants(Of TextBox)().ToList().
ForEach(Sub(ctr) ErrorProvider1.SetError(ctr, ""))
Next an instance of the Contact Class is populated (as done on prior examples)
Dim contact = New Contact() With {
.FirstName = FirstNameTextBox.Text,
.LastName = LastNameTextBox.Text,
.PersonalEmail = PersonalEmailTextBox.Text,
.BusinessEmail = BusinessEmailTextBox.Text,
.Phone = PhoneTextBox.Text
}
Next the validator is executed
Dim validationResult = ValidationHelper.ValidateEntity(contact)
This is followed by
- Obtaining a list of error which contain property name and error message for each control that failed validation.
- Obtain all TextBox controls into a list using a language extension method.
- Iterate the error list from step 1, find the control associated with the error.
- Set the error message for each control using the ErrorProvider.
If there are no failures on validation add a new Contact to the BindingSource which is then displayed in a DataGridView.
If validationResult.HasError Then
Dim errorItemList As List(Of ErrorContainer) = validationResult.ErrorItemList()
Dim controlsToValidate = Descendants(Of TextBox)().
Where(Function(c) c.Tag IsNot Nothing).
Select(Function(ctr) New With {.Control = ctr, .Tag = CStr(ctr.Tag)})
'
' Iterate errors, set error text via the ErrorProvider component
'
For Each ec As ErrorContainer In errorItemList
Dim current = controlsToValidate.FirstOrDefault(Function(item) item.Tag = ec.PropertyName)
If current IsNot Nothing Then
ErrorProvider1.SetError(
panel1.Controls.Find($"{ec.PropertyName}TextBox", False)(0),
ec.ErrorMessage)
End If
Next
Else
_bsContacts.Add(contact)
If _dataGridViewSizeDone Then
Return
End If
dataGridView1.ExpandColumns()
_dataGridViewSizeDone = True
End If
Unusual rules
There can be special situations that just don't fit into the standard rules, for instance, a string field is limited to a word count rather than a length. Here a custom validation class can be created and used
Imports System.ComponentModel.DataAnnotations
Namespace Rules
Public Class MaxWordAttributes
Inherits ValidationAttribute
Private ReadOnly mMaxWords As Integer
Public Sub New(maxWords As Integer)
MyBase.New("{0} has to many words.")
mMaxWords = maxWords
End Sub
Protected Overrides Function IsValid(
value As Object, validationContext As ValidationContext) As ValidationResult
If value Is Nothing Then
Return ValidationResult.Success
End If
Dim textValue = value.ToString()
If textValue.Split(" "c).Length <= mMaxWords Then
Return ValidationResult.Success
End If
Dim errorMessage = FormatErrorMessage(validationContext.DisplayName)
Return New ValidationResult(errorMessage)
End Function
End Class
End Namespace
Usage where only eight words are permitted.
<MaxWordAttributes(8)>
Public Property Words() As String
How to implement in a project
- The the project Validator library from the source code repository and add it to your Visual Studio solution.
- In your project add a reference to the validation library.
- Use the appropriate attributes on properties of your class which need validation e.g. <Required(ErrorMessage:="Your error message")> or custom attribute e.g. BirthDateValidation.
- In a form create a class instance from data inputted by a user.
- Pass the class instance to the validator as per this example.
For a very easy to follow implementation for the above see the following sample project in the GitHub repository.
Summary
In this article methods have been presented to validate information entered by users in Windows Form applications using Data Annotations which is usually done in web applications. What has not been shown is working with conventional DataSet, DataTable containers yet these methods presented may integrate into code interacting with DataSet and or DataTable containers but validating with a class then if all data validates add data in the class into a DataTable. Both basic and advance methods have been shown although they don’t cover all possible situations the foundation is here for developers to expand upon by examining and running various code samples provided in the accompanying source code.
When moving to ASP.NET the validator classes are not required, only your custom validation rules as validators are within ASP.NET MVC.
See also
References
Adding Validation to the model
How to Validate Forms with ASP.NET MVC Data Annotations
Source code
https://github.com/karenpayneoregon/ClassValidationVisualBasic