On The Dot...
Talking about all things .NET related

Silverlight 4 DataGrid - Bulk Validation Using INotifyDataErrorInfo

Saturday, October 29, 2011 2:23 AM

Level: Beginner

In working with the Silverlight 4 DataGrid control, I found that documentation and online examples tend to focus on validation either at the cell level, or row level. However, there could be a case where validation is required after all rows have been entered, and comparisons between rows may need to be made.

One example I can think of is the case where a list of products, including SKU information, is entered by the user. The SKU code is not derived automatically, but must be unique per product. Client-side validation will need to ensure that duplicate values are not entered for this field. This type of validation rules out using the built-in annotations, which validates data at the row level.

Also, there may be a case where screen real estate may be an issue. I recently needed to host the DataGrid in a popup/dialog style window. Therefore, displaying the errors as a validation summary within the DataGrid was not feasible. Once a few errors are raised, the validation summary takes up the space available for the DataGrid.

To work around these limitations, I decided to create a class that represented the collection of products, as an ObservableCollection. The Products class implemented the INotifyDataErrorInfo interface, instead of the Product class (which would be done if using row level validation). Furthermore, I added public methods within the Products collection class to make it easy to trigger the validation from the Silverlight page, and access the collection of errors.

For this example, the validation is triggered when a button is clicked in the main page of the Silverlight control. If multiple errors are discovered, related messages are added to the errors collection within the Products class, and a child window loads with the errors displayed in a ListBox. As well, the rows that contain the errors are highlighted in red, as shown below.



I’ll walk through a simple, quick-and-dirty, example to demonstrate how I implemented this. For this example, you will need to ensure you have the following installed on your machine: Visual Studio 2010, .NET Framework 4, and the Silverlight 4 SDK.

Launch Visual Studio 2010 and create a new Silverlight application (File | New | Project..)


First we’ll create the objects we need for this example.

To do this, right-click on the Project in the Solution Explorer, and click Add | New Item…

In the dialog, select Class.


Change the name of the class to Product.cs, then click the Add button to create the Product class.

Include the following using directives at the top of the Product class:

using System;

using System.Linq;

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.ComponentModel;

using System.Windows;

 

Ensure the Product class implements the INotifyPropertyChanged interface and add the following members:

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string propertyName)

{

if (null != PropertyChanged)

{

this.IsDirty = (!this.IsIntialLoad);

PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

}

}

public void RaisePropertyChanged(string propertyName)

{

if (null != Application.Current.RootVisual)

{

Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>

OnPropertyChanged(propertyName));

}

}

#endregion

Add a few properties to the class. In this example, I added SKU, Name, and Description, as follows:

private string _sku;

public string SKU

{

get { return _sku; }

       set

       {

       if (_sku != value)

       {

                     _sku = value;

RaisePropertyChanged("SKU");

       }

       }

}

 

private string _name;

public string Name

{

get { return _name; }

set

{

if (_name != value)

{

_name = value;

RaisePropertyChanged("Name");

}

}

}

 

private string _desc;

public string Description

{

get { return _desc; }

set

{

if (_desc != value)

{

_desc = value;

RaisePropertyChanged("Description");

}

}

}

Next we’ll add some properties to track the state of the object:

IsDirty – to track if the item properties have been modified, so that we can limit which rows to validate against

IsValid – to track if the item has passed validation,

IsInitialLoad – to ensure that the dirty flag is not set if the values of the properties are being changed on the initial load of the instance.

The first two properties will not need to raise a PropertyChanged event, but the IsValid flag will need to. You’ll see later in the example why this is required.

public bool IsDirty { get; set; }

 

public bool IsIntialLoad { get; set; }

 

public bool IsValid

{

get { return _isValid; }

set

{

if (_isValid != value)

{

_isValid = value;

RaisePropertyChanged("IsValid");

}

}

}

 

Next, define another class which will serve as the collection of items. I just defined the class within the same file as the Product class, but you can break it out into separate files. Name the class, Products, defined as an ObservableCollection of Product. Ensure that it implements the INotifyDataErrorInfo interface and add the following members:

#region INotifyDataErrorInfo Members

protected event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

event EventHandler<DataErrorsChangedEventArgs> INotifyDataErrorInfo.ErrorsChanged

{

add

{

this.ErrorsChanged += value;

}

remove

{

this.ErrorsChanged -= value;

}

}

 

System.Collections.IEnumerable INotifyDataErrorInfo.GetErrors(String propertyName)

{

return this.GetErrors(propertyName);

}

 

Boolean INotifyDataErrorInfo.HasErrors

{

get { return this.HasErrors; }

}

#endregion

 

Next, add methods to:

  • 1) validate the Products collection,
  • 2) add errors to an error collection,
  • 3) return the errors collection, and
  • 4) clear the errors collection.

As well, we need to add a flag to indicate if the Products collection has errors.

public void AddError(string propertyName, string errorMessage)

{

if (this._errors.ContainsKey(propertyName))

    this._errors.Remove(propertyName);

 

this._errors.Add(propertyName, errorMessage);

}

 

public void ClearErrors()

{

this._errors.Clear();

}

 

public System.Collections.IEnumerable GetErrors(String propertyName)

{

if (this._errors.ContainsKey(propertyName))

{

     return this._errors[propertyName];

}

else

{

     return null;

}

}

 

public IEnumerable<string> GetAllErrors()

{

return (from e in _errors

select e.Value);

}

 

public Boolean HasErrors

{

get { return this._errors.Keys.Count != 0; }

}

 

/// <summary>

/// Validates the data in the current collection.

/// </summary>

public void ValidateCollection()

{

this.ClearErrors();

int index = 0;

 

#region SKU - Required Field Check

IEnumerable<Product> invalidItems = (from m in this

                              where m.SKU == "" &&

                              m.IsDirty == true

                              select m);

 

foreach (Product product in invalidItems)

{

    this.AddError(String.Format("SKU[{0}]", index.ToString()),

       String.Format("Item ID is a required field (Name: {0}).",

product.Name));

 

    index++;

 

    if (this.IndexOf(product) >= 0)

        this[this.IndexOf(product)].IsValid = false;

}

#endregion

 

#region Name - Required Field Check

invalidItems = (from m in this

                where m.Name == "" &&

                m.IsDirty == true

                select m);

 

foreach (Product product in invalidItems)

{

    this.AddError(String.Format("Name[{0}]", index.ToString()),

        String.Format("Item Name is a required field (ID: {0}).",

        product.SKU));

 

    index++;

 

    if (this.IndexOf(product) >= 0)

        this[this.IndexOf(product)].IsValid = false;

}

#endregion

 

#region SKU - Verify Unique Values

IEnumerable<string> invalidIDs = (from m in this

                                  group m by m.SKU into g

                                  where g.Count() > 1

                                  select g.Key).ToArray();

 

foreach (Product product in this)

{

    if (invalidIDs.Contains(product.SKU) &&

        !string.IsNullOrEmpty(product.SKU))

    {

        product.IsValid = false;

        string dupSKU = String.Format("DupSKU[{0}]", product.SKU);

        if (this.GetErrors(dupSKU) == null)

        {

            this.AddError(String.Format("DupSKU[{0}]", product.SKU),

     String.Format("Item ID Codes must be unique (Duplicate ID: {0}).",

product.SKU));

        }

    }

    }

    #endregion

}

 

We need to add one more class, which will be used as our color converter to define the color of the border in each cell in the DataGrid.

If a product is found to be invalid (IsValid property is false), the converter will set the brush color to red. If the product is valid, it will reset the brush to transparent. This is what will control the border color around each cell in the grid.

using System;

using System.Globalization;

using System.Windows;

using System.Windows.Data;

using System.Windows.Media;

namespace DataGridBulkValidation

{

public class ColorConverter : IValueConverter

{

    public object Convert(object value,

     Type targetType,

     object parameter,

     CultureInfo culture)

    {

        bool cellIsValid = bool.Parse(value.ToString());

        if (!cellIsValid)

        {

            return new SolidColorBrush(Colors.Red);

        }

        else

        {

            return new SolidColorBrush(Colors.Transparent);

        }

    }

 

    public object ConvertBack(object value,

  Type targetType,

  object parameter,

  CultureInfo culture)

    {

        return new SolidColorBrush(Colors.Transparent);

    }

}

}

 

In our Main.xaml file, we need to add the namespace for the local ColorConverter class. Add the following as an attribute in the UserControl element:

xmlns:local="clr-namespace:DataGridBulkValidation"

 

Next, add the color converter as a resource:

<UserControl.Resources>

<local:ColorConverter x:Key="cc" />

</UserControl.Resources>

 

Add the following markup to the main Grid as follows to add a couple of buttons (Add and Validate) and define their Click events:

<Grid.RowDefinitions>

<RowDefinition Height="40"></RowDefinition>

<RowDefinition Height="*"></RowDefinition>

</Grid.RowDefinitions>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="100"></ColumnDefinition>

<ColumnDefinition Width="100"></ColumnDefinition>

<ColumnDefinition Width="*"></ColumnDefinition>

</Grid.ColumnDefinitions>

<Button x:Name="addButton" Click="addButton_Click" Content="Add" Width="80" Margin="5" Visibility="Visible" Grid.Row="0" Grid.Column="0"/>

<Button x:Name="validateButton" Click="validateButton_Click" Content="Validate" Width="80" Margin="5" Visibility="Visible" Grid.Row="0" Grid.Column="1"/>

 

Drag the Silverlight DataGrid control from the Toolbox to your User Control. Modify the markup for the DataGrid as follows:

<sdk:DataGrid x:Name="gridMain"

AutoGenerateColumns="False"

ItemsSource="{Binding}"

Grid.Row="1"

Grid.ColumnSpan="3">

</sdk:DataGrid

Now define each of the 3 columns that are needed to represent each of the Product’s properties. Below is the example of the SKU column markup:

<sdk:DataGridTemplateColumn x:Name="SKU" Header="SKU">

<sdk:DataGridTemplateColumn.CellTemplate>

<DataTemplate>

<Border BorderBrush="{Binding IsValid, Converter={StaticResource cc}}"

BorderThickness="1">

    <TextBlock Text="{Binding SKU}" />

</Border>

</DataTemplate>

</sdk:DataGridTemplateColumn.CellTemplate>

<sdk:DataGridTemplateColumn.CellEditingTemplate>

<DataTemplate>

    <TextBox Text="{Binding SKU, Mode=TwoWay}" MaxLength="30"

             BorderBrush="{Binding IsValid, Converter={StaticResource cc}}" BorderThickness="1" />

</DataTemplate>

</sdk:DataGridTemplateColumn.CellEditingTemplate>

</sdk:DataGridTemplateColumn>

 

For this example, the Name and Description will be similar.

In the Main.xaml.cs file, add the following to the code behind:

private void addButton_Click(object sender, RoutedEventArgs e)

{

   Product product = new Product();

   product.IsIntialLoad = false;

   ((Products)gridMain.ItemsSource).Add(product);

   gridMain.Focus();

}

 

 

private void validateButton_Click(object sender, RoutedEventArgs e)

{

   //reset the IsValid flag on each item

   ((Products)gridMain.ItemsSource).ToList().ForEach(c => c.IsValid = true);

   Products products = (Products)gridMain.ItemsSource;

   products.ValidateCollection();

   if (products.HasErrors)

   {

       ErrorList errorsList = new ErrorList(products.GetAllErrors());

       errorsList.Show();

   }

}

 

 

When the Add Button is clicked, it will add a new blank row to the Products collection. Since the DataGrid is bound to the collection, the new blank row will be displayed in the DataGrid.

When the Validate button is clicked, the first action we’ll take is to ensure that the products are marked as valid first. This becomes evident in cases where the first validation failed, and the UI displays rows in an error state. On subsequent validation attempts, we want to reset the rows to valid so that if the errors were corrected, the visual representation of the previous error state is cleared from the screen.

Now run the example, and add some data as shown below. Click the Validate button, and note the errors.

Example project can be downloaded from here.