Share via


WPF/Entity Framework Core simple data validation (C#)

Introduction

Using the following article, WPF/Entity Framework Core primer (C#) learn how to data annotations to validate entities that are marked as modified, removed and added. In addition, synchronous operations become asynchronous operations to ensure the user interface remains responsive while reading and saving data to the database. 

Synchronous to asynchronous

Moving to asynchronous read of data allows a user interface to remain responsive and in most cases is simple to implement. For instance in part one of this series employee data loaded as follows.

var employeeCollection = new  ObservableCollection<Employees>(Context.Employees.AsQueryable());
EmployeeGrid.ItemsSource = employeeCollection;

To read data asynchronously change the event signature from

protected override  void OnContentRendered(EventArgs e)

To include async.

protected override  async void  OnContentRendered(EventArgs e)

Finally change the call above to which now reads data asynchronously.

var employeeCollection = new  ObservableCollection<Employees>();
 
await Task.Run(async () =>
{
    employeeCollection = new  ObservableCollection<Employees>(
        await Context.Employees.ToListAsync());
});

For saving changes, change the click event signature from

private void  SaveChangesButton_Click(object sender, RoutedEventArgs e)

To

private async void SaveChangesButton_Click(object sender, RoutedEventArgs e)

Then replace Context.SaveChanges() to

await Task.Run(async () =>
{
    await Context.SaveChangesAsync();
});

User interface concerns

If the data take a while to load it would not be good if the save button was pressed or an attempt to perform a search. Handling the save issue set IsEnabled to false.

<Button x:Name="SaveButton"
    Grid.Column="1"
    HorizontalAlignment="Left"
    Content="Save" 
    Width="70" IsEnabled="False"
    Margin="159,3,0,2" Height="27" Grid.ColumnSpan="2"
    Click="SaveChangesButton_Click"/>

Once data has been loaded enable the button.

SaveButton.IsEnabled = true;

Validating changes

Validation can be done in several places, for this article either by implementing IDataErrorInfo Interface or data annotations. In reality both can be used and extended past what is presented here.

DataGrid expose errors

In the last version of Employee class this was the signature.

public partial  class Employees : INotifyPropertyChanged

To propagate validation issues to the user interface add IDataErrorInfo to the class.

public partial  class Employees : INotifyPropertyChanged, IDataErrorInfo

Which requires the following members. this[string columnName] will receive a property name as a string which is interrogated in a switch inspecting for empty values and if found are returned in this case to the window hosting the DataGrid.  

public string  Error
{
    get => throw new  NotImplementedException();
}
public string  this[string columnName]
{
    get
    {
        string errorMessage = null;
        switch (columnName)
        {
            case "FirstName":
                if (String.IsNullOrWhiteSpace(FirstName))
                {
                    errorMessage = "First Name is required.";
                }
                break;
            case "LastName":
                if (String.IsNullOrWhiteSpace(LastName))
                {
                    errorMessage = "Last Name is required.";
                }
                break;
        }
        return errorMessage;
    }
}

Modify the FirstName and LastName columns in the DataGrid where Binding.ValidatesOnDataErrors property gets the error message from the code above.

<DataGridTextColumn
    Header="First"
    Binding="{Binding FirstName, ValidatesOnDataErrors=True}"
    Width="*"  >
</DataGridTextColumn>
 
<DataGridTextColumn
    Header="Last"
    Binding="{Binding LastName, ValidatesOnDataErrors=True}"
    Width="*"  />

Build, run the application and when the data appears select a row and edit first or last name and empty the contents followed by leaving the cell. A red rectangle surrounds the cell and no other editing is permitted. To the user they have no idea what is wrong. 

Adding a visual clue is in order which can be done using DataGrid.RowValidationErrorTemplate as shown in the following Microsoft documentation.

<DataGrid.RowValidationErrorTemplate>
  <ControlTemplate>
    <Grid Margin="0,-2,0,-2"
      ToolTip="{Binding RelativeSource={RelativeSource
      FindAncestor, AncestorType={x:Type DataGridRow}},
      Path=(Validation.Errors)[0].ErrorContent}">
      <Ellipse StrokeThickness="0" Fill="Red"
        Width="{TemplateBinding FontSize}"
        Height="{TemplateBinding FontSize}" />
      <TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
        FontWeight="Bold" Foreground="White"
        HorizontalAlignment="Center"  />
    </Grid>
  </ControlTemplate>
</DataGrid.RowValidationErrorTemplate>

Since in the source code for this project styles are placed into App.xaml the above style can be placed here too with a few modifications.

<Setter Property="RowValidationErrorTemplate">
    <Setter.Value>
        <ControlTemplate>
            <Grid
                Margin="0,-2,0,-2"
                ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}, 
                Path=(Validation.Errors)[0].ErrorContent}">
 
                <Ellipse
                    StrokeThickness="0"
                    Fill="Red"
                    Width="{TemplateBinding FontSize}"
                    Height="{TemplateBinding FontSize}" />
 
                <TextBlock
                    Text="!"
                    FontSize="{TemplateBinding FontSize}"
                    FontWeight="Bold"
                    Foreground="White" 
                    HorizontalAlignment="Center"  />
            </Grid>
        </ControlTemplate>
 
    </Setter.Value>
</Setter>

Finally place the following style into App.xml to complete the formatting.

<Style x:Key="ErrorStyle" TargetType="{x:Type TextBox}">
    <Setter Property="Padding" Value="-2"/>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="Background" Value="Red"/>
            <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},  
                Path=(Validation.Errors)[0].ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Example when a cell is empty.

Entity Framework and data annotations

Another method is to validate information is by annotating properties in the class, in this case employees. Note [Required] indicates a required property, if not checked when performing saves back to the database an exception will be thrown. StringLength indicates the max length the column accepts in the back end table, if the value was in this case more than 20 characters the database will thrown a runtime exception.

[Required]
[StringLength(20, ErrorMessage = "Description Max Length is 20")]
public string  FirstName
{
    get => _firstName;
    set
    {
        if (value == _firstName) return;
        _firstName = value;
        OnPropertyChanged();
    }
}
[Required]
public string  LastName
{
    get => _lastName;
    set
    {
        if (value == _lastName) return;
        _lastName = value;
        OnPropertyChanged();
    }
}

See the following for other attributes available from Microsoft or create your own as provided in the following project included in the article's source code. One example is a custom password validator.

public class  PasswordCheck : ValidationAttribute
{
    public override  bool IsValid(object value)
    {
        var validPassword = false;
        var reason = string.Empty;
        string password = (value == null) ? string.Empty : value.ToString();
 
        if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
        {
            reason = "new password must be at least 6 characters long. ";
        }
        else
        {
            Regex pattern = new  Regex("((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})");
            if (!(pattern.IsMatch(password)))
            {
                reason += "Your new password must contain at least 1 symbol character and number.";
            }
            else
            {
                validPassword = true;
            }
        }
 
        if (validPassword)
        {
            return true;
        }
        else
        {
            return false;
        }
 
    }
 
}

To validate prior to saving changes work with the Entity Framework ChangeTracker, look at deleted, modified and added.

IEnumerable<EntityEntry> modified = Context.ChangeTracker.Entries().Where(entry =>
    entry.State == EntityState.Deleted ||
    entry.State == EntityState.Modified ||
    entry.State == EntityState.Added);

Iterate the entities.

foreach (var entityEntry in modified)
{
    var employee = (Employees) entityEntry.Entity;
 
    EntityValidationResult validationResult = ValidationHelper.ValidateEntity(employee);
    if (validationResult.HasError)
    {
        InspectEntities(entityEntry);
        builderMessages.AppendLine($"{employee.EmployeeId} - {validationResult.ErrorMessageList()}");
    }
}

If there are validation issues present them to the user. If the code did not have the validation via IDataErrorInfo it's possible to have several validation issues while having IDataErrorInfo implemented only one issue at a time.

Validating entities is done by first having rules from database tables column rules which range from a column is required, not nullable,  maximum and minimum string lengths for strings, valid dates, date ranges, numeric ranges, is a value a valid email address are basic rules. Next properties in models are annotated which applies column rules to properties.

Validation class project

Included with the source code is a class project which can be used in any Windows form or WPF project to perform validation using Data Annotations. Consider keeping custom rules in this project so they are reusable in other projects.

Inspecting values

While developing an application and working with validation during iterating entities from the DbContext ChangeTracker (as per above) the following method offers a look into changed items for both original and current values of properties.

private void  InspectEntities(EntityEntry entityEntry)
{
    foreach (var property in entityEntry.Metadata.GetProperties())
    {
        var originalValue = entityEntry.Property(property.Name).OriginalValue;
        var currentValue = entityEntry.Property(property.Name).CurrentValue;
        if (originalValue != null || currentValue != null)
        {
            if (!currentValue.Equals(originalValue))
            {
                Console.WriteLine($"{property.Name}: Original: '{originalValue}', Current: '{currentValue}'");
            }
        }
        else
        {
            // TODO handle nulls
        }
 
    }
}

Developing validation tip

Isolate data operations from the user interface either with unit testing or simply create a method to work through testing validation of objects which is the next best thing to unit testing while unit testing allows a developer to test and re-test code rather than keeping a test method in production code.

Example, adding a new entity.

Try 

  • Passing nothing for first and last name.
  • Pass an invalid email address format
  • etc.
private void  AddHardCodedEmployee()
{
    // create new employee
    var employee = new  Employees()
    {
        FirstName = "Jim",
        LastName = "Lewis",
        Email = "jlewis@comcast.net",
        HireDate = new  DateTime(2012, 3, 14),
        JobId = 4,
        Salary = 100000,
        DepartmentId = 9
    };
 
    EntityValidationResult validationResult = ValidationHelper.ValidateEntity(employee);
    if (validationResult.HasError)
    {
        var errors = validationResult.ErrorMessageList();
        MessageBox(errors,"Validation errors");
        return;
    }
    else
    {
        // add and set state for change tracker
        Context.Entry(employee).State = EntityState.Added;
        // add employee to the grid
        var test = EmployeeGrid.ItemsSource;
        ((ObservableCollection<Employees>)EmployeeGrid.ItemsSource).Add(employee);
        MessageBox("Added");
    }
}

Summary

In this part of the series, basic validation been presented along with how to style a DataGrid to present validation issues to the user.  Changing from synchronous to asynchronous data operations to keep the user interface responsive.

Where to go from here is to first get familiar with what has been presented. If more is needed to consider looking at MVVM pattern.

See also

Source code

Clone or download source from the following GitHub repository which is a different branch then the first article. Once opened in Visual Studio run the data script, in solution explorer run restore NuGet packages.