Manish Dalal's blog

Exploring .net!

September 2008 - Posts

ComboBox in DataGrid

This post examines usage of ComboBox in DataGrid. In particular, it shows how to implement foreign key scenarios in lieu of missing SelectedValue property. It also highlights workaround for a bug in RC0 that causes ComboBox dropdown to close immediately in DataGrid.

ComboBox

The ComboBox control is used to present users with a list of values to select from. This can be used to show a simple list of valid values that user can choose from or you might be showing user a complicated type. ComboBox.ItemTemplate can be used visualize the complex item, but you will find that current ComboBox implementation in Silverlight 2 (RC0) is missing SelectedValue and SelectedValuePath property. This is a common requirement in business applications, where in majority of cases foreign keys are used for represents joins/relationships. In these cases you normally use foreign key values to select the item and result of use selection is also saved as foreign key value. However user is shown a friendly name/description in place of foreign key value which might be numeric or guid.

Lets first see how to bind to a simple property, a string list and then we will modify the code to implement foreign key scenario by binding to a complex type .

Simple Data Binding

Create a new Silverlight application project. Also create the corresponding web application to host and test the Silverlight application. Next add System.Windows.Control.Data reference to the Silverlight project.

Add following code to page.xaml

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    Width="400" Height="300">
    <UserControl.Resources>
        <src:CityProvider x:Key="cityProvider"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid x:Name="dataGrid" AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Street Name" Binding="{Binding StreetName}"/>
                <data:DataGridTemplateColumn Header="City">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding CityName}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox SelectedItem="{Binding CityName, Mode=TwoWay}" 
                                      ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}" 
                                  />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTextColumn Header="Zip Code" Binding="{Binding ZipCode}"/>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

Add following code to page.xaml.cs code behind

public partial class Page : UserControl {
    public Page() {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(Page_Loaded);
        }

    void Page_Loaded(object sender, RoutedEventArgs e) {
        this.dataGrid.ItemsSource = new List<Address>() { new Address() { StreetName = "Street 1", CityName="City 1", ZipCode = "1"},
                                                          new Address() { StreetName = "Street 2", CityName="City 2", ZipCode = "2"},
                                                          new Address() { StreetName = "Street 3", CityName="City 3", ZipCode = "3"}
        };
    }
}

public class Address {
    public string StreetName { get; set; }
    public string CityName { get; set; }
    public string ZipCode { get; set; }
}

public class CityProvider {
    public List<string> CityList {
        get {
            return new List<string> { "City 1", "City 2", "City 3", "City 4" };
        }
    }
}

 

Here we are showing user Address business entity in the DataGrid. Address class consists of StreetName, CityName and ZipCode fields. User can change value of CityName using ComboBox, that provides list of valid cities.

ComboBox ItemsSource is bound to CityProvider that provides a list of cities(string list) to select from. ComboBox SelectedItem property is bound to CityName field of the Address business class. This selects proper value in ComboBox when it is shown and allows saving user selection back to the business class.

F5 and test the application. Change value of City field. Notice proper selection of item in ComboBox dropdown after the change.

Foreign Key Data Binding (alternative to missing SelectedValue property)

Now instead of binding to simple string City field, lets say that we are storing CityId (an integer) in Address class in place of City. Here CityId represents the foreign key. Lets modify code to select using CItyId foreign key and also save result of selection as CityId back to Address business class.

Modify Page.xaml as follows

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    Width="400" Height="300">
    <UserControl.Resources>
        <src:CityProvider x:Key="cityProvider"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid x:Name="dataGrid" AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Street Name" Binding="{Binding StreetName}"/>
                <data:DataGridTemplateColumn Header="City">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding CityInfo.CityName}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <ComboBox SelectedItem="{Binding CityInfo, Mode=TwoWay}" 
                                      ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}" 
                                      DisplayMemberPath="CityName"
                                  />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTextColumn Header="Zip Code" Binding="{Binding ZipCode}"/>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

Change Page.xaml.cs code behind as:

public partial class Page : UserControl {
    public Page() {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(Page_Loaded);
        }

    void Page_Loaded(object sender, RoutedEventArgs e) {
        this.dataGrid.ItemsSource = new List<Address>() { new Address() { StreetName = "Street 1", CityId=1, ZipCode = "1"},
                                                          new Address() { StreetName = "Street 2", CityId=2, ZipCode = "2"},
                                                          new Address() { StreetName = "Street 3", CityId=3, ZipCode = "3"}
        };
    }
}

public class Address {
    public string StreetName { get; set; }
    //public string CityName { get; set; }
    public int CityId { get; set; }
    public string ZipCode { get; set; }
    //
    private City _cityInfo;

    public City CityInfo {
        get {
            if (null == _cityInfo) {
                _cityInfo = new CityProvider().CityList.Where(c => c.CityId == CityId).SingleOrDefault();
            }
            return _cityInfo; 
        }
        set { 
            _cityInfo = value;
            CityId = _cityInfo.CityId;
        }
    }

}

public class CityProvider {
    public List<City> CityList {
        get {
            //return new List<string> { "City 1", "City 2", "City 3", "City 4" };
            return new List<City> { new City() { CityName ="City 1", CityId=1},  
                                    new City() { CityName ="City 2", CityId=2},   
                                    new City() { CityName ="City 3", CityId=3},  
                                    new City() { CityName ="City 4", CityId=4} };
        }
    }
}

public class City {
    public string CityName { get; set; }
    public int CityId { get; set; }

    public override bool Equals(object obj) {
        if (null == obj) {
            return false;
        }
        return this.CityId == ((City)obj).CityId;
    }

    public override int GetHashCode() {
        return CityName.GetHashCode();
    }
}

We have changed CityProvider to return City class that consists of CityName and CityId fields. We have also modified Address business class by removing CityName and adding CityId field to store value of foreign key.

Notice that we have also added new CityInfo property to the Address class. Address class provides this helper property so that we can bind it to ComboBox SelectedItem property. It is this helper property that is key to the data binding in foreign key scenarios. In get method we use current CityId value from Address business class and return an instance of City class. This instance sets the SelectedItem by way of data binding. In set method, we get values from ComboBox selected item and set CityId to the selection. (Demo Note: In production code please consider caching CityList)

Now when you run the application, and change values, proper CityId is saved back to Address class.

image image

RC0 ComboBox Issue

In RC0, there is issue with ComboBox displaying properly in DataGrid. When you try to open ComboBox, it display and then immediately closes. I posted about it here and  Lee found a work around. Here is the work around. Use MyComboBox in place of ComboBox in above code.

public class MyComboBox : ComboBox {
    public MyComboBox() {
        DefaultStyleKey = typeof(ComboBox);
        this.Loaded += new RoutedEventHandler(MyComboBox_Loaded);
    }
    void MyComboBox_Loaded(object sender, RoutedEventArgs e) {
        IsDropDownOpen = true;
    } 

}

 

 

 

 

 

 

Alternatively if you wound rather not inherit and create new control, you can use Attached Property to provide same behavior modification. I have already blogged about using Attached Property to provide service and this uses the same technique to force open ComboBox dropdown

public class ComboBoxService {
    public static readonly DependencyProperty ForceOpenProperty =
        DependencyProperty.RegisterAttached("ForceOpen", typeof(bool), typeof(ComboBoxService),
                                            new PropertyMetadata(OnForceOpenChanged));

    public static bool GetForceOpen(DependencyObject d) {
        return (bool)d.GetValue(ForceOpenProperty);
    }

    public static void SetForceOpen(DependencyObject d, bool value) {
        d.SetValue(ForceOpenProperty, value);
    }

    private static void OnForceOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        ComboBox comboBox = d as ComboBox;
        if ((bool)e.OldValue) {
            comboBox.Loaded -= new RoutedEventHandler(comboBox_Loaded);
        }
        if ((bool)e.NewValue) {
            comboBox.Loaded +=new RoutedEventHandler(comboBox_Loaded);
        }
    }

    static void comboBox_Loaded(object sender, RoutedEventArgs e) {
        ComboBox comboBox = sender as ComboBox;
        if (null == comboBox) {
            comboBox = e.OriginalSource as ComboBox;
        }
        //
        comboBox.IsDropDownOpen = true;
    }
}

 

Usage:

<ComboBox SelectedItem="{Binding CityInfo, Mode=TwoWay}" 
          ItemsSource="{Binding CityList, Source={StaticResource cityProvider}}" 
          DisplayMemberPath="CityName"
          src:ComboBoxService.ForceOpen="true"
      />

Source Code: ComboBoxUsage.zip

Prevention : The first line of defense, with Attach Property Pixie dust!

In the Building Business Application with Silverlight series we have seen how to validate data in various conditions. While it is necessary to validate data after user has entered it, it will be even better if we can prevent user from  entering invalid data in the first place. Consider for example the Age field. User should only be allowed to enter positive numbers. Negative numbers or alpha characters should not be allowed. A simple way to ensure this rule is to check data as part of validation, if it is negative or non numeric, we can throw exception or set some error state. This typically occurs after user has entered data, in setter of data fields. User will then reenter the new data, resulting in duplicate work.

Instead of waiting for user to complete the data entry and then validating, we can trap user keystrokes as data is being entered and prevent invalid keys. This can carried out by subscribing to the KeyDown event and rejecting any keys that are not proper.

KeyDown event provides information on the Key pressed as part of KeyEventArgs. The KeyDown event is a bubbling event and can be canceled by setting Handled property of KeyEventArgs to true. This prevents it from routing to further objects along the event route, effectively canceling the event.

We will implement  text filtering feature as a service utilizing attached property to provide value for the filter. Another option is to inherit from TextBox and create a new control. However since we are just modifying existing control behavior and only working with with public properties, it is simpler to use attached property to add functionality rather then creating new control.

Attached Property Basics

An attached property is a dependency property that is defined by one object but settable on other object. Attached properties are defined as a specialized form of dependency property that do not have the conventional property wrapper on the type defining the property. In addition, storage is provided by the type on which the property is set and not the defining type.

In XAML, you set attached properties by using the syntax AttachedPropertyProvider.PropertyName. Attached property value itself is stored by the object on which it is set. Think of the object as providing a name value pair dictionary, where property is the key and points to the value, to be used by property definer or even other objects.

ParentPropertyProvider.Propertyname

The most typical use of attached property is where parent type defines property to be set on the child elements. The parent type then iterates its child elements, obtains the values, and acts on those values in some manner. For instance when you layout using Grid, you specify Row and Column as attached property on children elements. The Grid.Row property is created as an attached property because it is designed to be set on elements that are contained within a Grid, rather than on Grid itself. Grid then uses Row and Column values for layout.

ServicePropertyProvider.PropertyName

However another alternate use is to impart functionality, where by the type that defines the attached property represents a service. Then when the element that set the attached property is evaluated in the context of the service, the attached property values are obtained by the service class and desired behavior carried out. An example of this is TooltipService with attached property Tooltip. In this scenario, AttachedPropertyProvider represents a Service and attached property provides value for the service to act on.

We will use this alternate way to define TextBoxFilterService with attached property Filter, to represent various type of filters. There is also another use of attached property to store arbitrary data, that we will explore later.

TextBoxFilterService

Create a new Silverlight application project. Also create the corresponding web application to host and test the Silverlight application. In the Silverlight project, add new class, called TextBoxFilterService.cs. Define attached property Filter as:

// Filter Attached Dependency Property
public static readonly DependencyProperty FilterProperty =
    DependencyProperty.RegisterAttached("Filter", typeof(TextBoxFilterType), typeof(TextBoxFilterService),
                                        new PropertyMetadata(OnFilterChanged));
// Gets the Filter property. 
public static TextBoxFilterType GetFilter(DependencyObject d) {
    return (TextBoxFilterType)d.GetValue(FilterProperty);
}
// Sets the Filter property.
public static void SetFilter(DependencyObject d, TextBoxFilterType value) {
    d.SetValue(FilterProperty, value);
}

What enables use of attached property for service provider scenario is the delegate OnFilterChanged. This delegate is called whenever attached property value changes. As part the call, you get access to the object on with attached property is defined. Once you have handle to the object, you can attach event handlers to modify the default behavior.

Add OnFilterChanged delegate as shown

// Handles changes to the Filter property.
private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    TextBox textBox = d as TextBox;
    if (TextBoxFilterType.None != (TextBoxFilterType)e.OldValue) {
        textBox.KeyDown -= new KeyEventHandler(textBox_KeyDown);
    }
    if (TextBoxFilterType.None != (TextBoxFilterType)e.NewValue) {
        textBox.KeyDown += new KeyEventHandler(textBox_KeyDown);
    }
}

In the delegate above we are attaching to KeyDown event. We will implement our filtering logic in the KeyDown event handler.

// Handles the KeyDown event of the textBox control. private static void textBox_KeyDown(object sender, KeyEventArgs e) { // bypass other keys! if (IsValidOtherKey(e.Key)) { return; } TextBoxFilterType filterType = GetFilter((DependencyObject)sender); TextBox textBox = sender as TextBox;

if (null == textBox) {
    textBox = e.Source as TextBox;
}

switch (filterType) { case TextBoxFilterType.PositiveInteger: e.Handled = !IsValidIntegerKey(textBox, e.Key, e.PlatformKeyCode, false); break; case TextBoxFilterType.Integer: e.Handled = !IsValidIntegerKey(textBox, e.Key, e.PlatformKeyCode, true); break; case TextBoxFilterType.PositiveDecimal: e.Handled = !IsValidDecmialKey(textBox, e.Key, e.PlatformKeyCode, false); break; case TextBoxFilterType.Decimal: e.Handled = !IsValidDecmialKey(textBox, e.Key, e.PlatformKeyCode, true); break; case TextBoxFilterType.Alpha: e.Handled = !IsValidAlphaKey(e.Key); break; } } // Determines whether the specified key is valid other key. private static bool IsValidOtherKey(Key key) { // allow control keys if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) { return true; } // allow // Back, Tab, Enter, Shift, Ctrl, Alt, CapsLock, Escape, PageUp, PageDown // End, Home, Left, Up, Right, Down, Insert, Delete // except for space! // allow all Fx keys if ( (key < Key.D0 && key != Key.Space) || (key > Key.Z && key < Key.NumPad0)) { return true; } // we need to examine all others! return false; } // Determines whether the specified key is valid integer key for the specified text box. private static bool IsValidIntegerKey(TextBox textBox, Key key, int platformKeyCode, bool negativeAllowed) { if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) { return false; } if (Key.D0 <= key && key <= Key.D9) { return true; } if (Key.NumPad0 <= key && key <= Key.NumPad9) { return true; } if (negativeAllowed && (key == Key.Subtract || (key == Key.Unknown && platformKeyCode == 189))) { return 0 == textBox.Text.Length; } return false; } // Determines whether the specified key is valid decmial key for the specified text box. private static bool IsValidDecmialKey(TextBox textBox, Key key, int platformKeyCode, bool negativeAllowed) { if (IsValidIntegerKey(textBox, key, platformKeyCode, negativeAllowed)) { return true; } if (key == Key.Decimal || (key == Key.Unknown && platformKeyCode == 190)) { return !textBox.Text.Contains("."); } return false; } // Determines whether the specified key is valid alpha key. private static bool IsValidAlphaKey(Key key) { if (Key.A <= key && key <= Key.Z) { return true; } return false; }

When we receive the KeyDown notification, first thing we do is to check for valid keys such as Backspace, Enter and other non alpha numeric keys. If key is one of the allowed ones, we just return, and let the normal processing occur. Next we look for type of filter that has been set and try to check for valid keys for that filter. For instance if Filter has been set to PositiveInterger, only numeric keys are allowed. If key is not valid for given filter, we set Handled to true and stop processing, preventing key from having any effect.

We also need to add TextBoxFilterType enum that defines type of filter we support.

public enum TextBoxFilterType {
    None,
    PositiveInteger,
    PositiveDecimal,
    Integer,
    Decimal,
    Alpha,
}

To test TextBoxFilterService, first add namespace declaration as

xmlns:src="clr-namespace:Silverlight"

to the UserControl opening tag in the Page.xaml. Next add following XAML markup to Page.xaml Grid element.

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Only Positive Integer TextBox"/>
<TextBox Grid.Column="1" src:TextBoxFilterService.Filter="PositiveInteger"/>
<TextBlock Grid.Column="0" Grid.Row="2" Text="Only Integer TextBox"/>
<TextBox Grid.Column="1" Grid.Row="2" src:TextBoxFilterService.Filter="Integer"/>
<TextBlock Grid.Column="0" Grid.Row="3" Text="Only Positive Decimal TextBox"/>
<TextBox Grid.Column="1" Grid.Row="3" src:TextBoxFilterService.Filter="PositiveDecimal"/>
<TextBlock Grid.Column="0" Grid.Row="4" Text="Only Positive Decimal TextBox"/>
<TextBox Grid.Column="1" Grid.Row="4" src:TextBoxFilterService.Filter="Decimal"/>
<TextBlock Grid.Column="0" Grid.Row="5" Text="Only Alphabets TextBox"/>
<TextBox Grid.Column="1" Grid.Row="5" src:TextBoxFilterService.Filter="Alpha"/>

Finally run the application

image

Try out various of the options. Note that only valid keys are allowed, others are simply ignored. You can extend code to handle other scenarios that might be common in your business domain.

DataPropertyProvider.PropertyName

Use of attached property to implement service is very powerful concept and can be leveraged in many other situations. Another equally useful but lesser know use of attached property is to save arbitrary data. For instance, Tag property found on many elements allows user to save custom strings. However if the element does not provide one, like say ColumnDefinition, it is easy to add your own Tag. Define a custom tag property, MyTag in page.xaml.cs as

public static readonly DependencyProperty MyTagProperty =
    DependencyProperty.RegisterAttached("MyTag", typeof(string), typeof(Page),
                                        new PropertyMetadata(null));

public static string GetMyTag(DependencyObject d) {
    return (string)d.GetValue(MyTagProperty);
}

public static void SetMyTag(DependencyObject d, string value) {
    d.SetValue(MyTagProperty, value);
}

You can now used MyTag property on ColumnDefinition as

<ColumnDefinition Width="Auto" src:Page.MyTag="CustomTag"/>

Add new RowDefinition to LayoutRoot grid and add new Button (call it myTagButton) to test functionality. Add following code for the Button click event

void myTagButton_Click(object sender, RoutedEventArgs e) {
    HtmlPage.Window.Alert(GetMyTag(this.LayoutRoot.ColumnDefinitions[0]));
}

Test the MyTag functionality

image

Actually there is not need for the custom GetMyTag and SetMyTag helper methods as well, you can get data directly from object on which you define attached property using GetValue and SetValue methods. The only requirement to use of attach property is that, the object on which you are setting attached property must derive from DependencyObject. It is the DependencyObject that provides GetValue and SetValue methods.

Replace code in button click with following

void myTagButton_Click(object sender, RoutedEventArgs e) {
    //HtmlPage.Window.Alert(GetMyTag(this.LayoutRoot.ColumnDefinitions[0]));
    HtmlPage.Window.Alert(this.LayoutRoot.ColumnDefinitions[0].GetValue(MyTagProperty).ToString());
}

If you the test application again, you will get the same results.

Hopefully you have found new appreciation for the attached property and gained knowledge to start using in you own service provider or to store you own data.

Source Code: FilterService.zip

Silverlight Business Application Part 7: Beyond Validation, Prevention.

If you have been following the Building Business Application with Silverlight series, this is essentially Part 7, going beyond validation. We have the Age filed in the Person class that is currently being validated by PersonValidator using following code

public void ValidateAge(int newValue) {
    InvalidAge = (newValue < 0 || newValue > 200);
}

If you enter negative number in the Age filed, it is caught by the validator and user is displayed the error

image

Let's use the TextBoxFilterService to prevent entry of negative numbers. First add code for TextBoxFilterService to the Silverlight project. Next modify page.xaml and wire up age TextBox to the filter service

<TextBox
    Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
    Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
    ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}" 
    src:TextBoxFilterService.Filter="PositiveInteger"
    Tag="Age"
    >
</TextBox>

Now when you run the application, it will only allow positive numbers, all other entries are ignored. Note that validation is still required since user can paste invalid data.

Source Code for the Business Application:BusinessApp1.zip

Silverlight Business Application Part 6: IEditableobject and Add new item (Take 3!)

This is part six of Building Business Application with Silverlight series that showcases the basic building blocks of a data centric application.  

Series Link: Part 0, Part 1, Part 2, Part 3, Part 4, Part 5

We have seen how to add new items, delete items and how to validate data using various mechanisms. Now we will tackle some of the essentials features.

IEditableObject

When a validation error occurs in a cell, user can back out of changes using Escape key. However if user has made changes to other columns in same row, those changes will still persists. User may wish to back out of all changes. This is where IEditableObject interface comes into play. IEditableObject is used to represent an object that has an editing mode and the ability to commit or revert changes. By implementing this interface, when user Escapes first time, cell value is reverted, when user Escapes again (second time), entire row is reverted back.

Add IEditableObject to the Person class declaration in the Silverlight project.

public class Person : INotifyPropertyChanged, IEditableObject {

Add following code just before end of the class to implement IEditableObject interface.

 
#region IEditableObject Members
Person _backup;
bool _editing;

public void BeginEdit() {
    if (!_editing) {
        _editing = true;
        _backup = this.MemberwiseClone() as Person;
    }
}

public void CancelEdit() {
    if (_editing) {
        FirstName = _backup.FirstName;
        LastName = _backup.LastName;
        Age = _backup.Age;
        City = _backup.City;
        _editing = false;
    }
}


public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
    }
}
#endregion

Here we are making copy of current data to the field backup in BeginEdit. If user decides to cancel, we just restore original data from backup, otherwise we just remove the backup. Please note that MemberwiseClone method only creates a shallow copy and it is ok for the demo, but for production code please create a deep copy.

F5 and run the application. Make change to the age filed. Next make change to Last name and First Name.

image

Now undo change by pressing Esc. On first Escape, First name is reverted back.

image

Esc again. On second Escape, Age and Last name is reverted back.

image

Delay or Not to Delay - That is the question!

In our example, we have kept property change notification live, in real time. Anytime there is a change to data, ProeprtyChanged event is fired and other parts of UI are updated. This is prefect for GridView / DetailsView kind of scenario, but may create issue if you are performing complicated calculation or other changes in response to change to property. One option is to delay property change notification till end. Only when user commits, data is compared to backup copy and property change notification sent out. If user cancel, no notification is sent and rest of application is not affected by rollback to original values.

Now that we have implemented IEditableObject, we know exactly when user has committed changes. We can use this information to enhance Add New Item functionality.

Add New Item - Take 3

If you recall from Part 1, when we implemented Add Item, we hooked on to PropertyChanged event notification to trigger adding of new row. It worked, except when user backed out of change by deleting data, code still added new row. This might be ok, but we can remedy this undesirable behavior by utilizing out now found ability to track user commits!

Add new event ChangesCommited to Person class as follows

public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
        if (null != ChangesCommitted) {
            OnChangesCommitted();
        }
    }
}

bool _fireChangesCommited;
public event EventHandler<EventArgs> ChangesCommitted;

protected void OnChangesCommitted() {
    if (null != ChangesCommitted) {
        ChangesCommitted(this, new EventArgs());
    }
}

We have defined new ChangesCommitted event that will fire when user finishes editing and commits changes, through call to EndEdit (by DataGrid). If use rollbacks changes, no Commit event is fired and we will not add unnecessary empty row.

Next change People class to subscribe to ChangesCommitted in place of PropertyChanged. Also add emptyPerson_ChangesCommitted event handler. Comment out old emptyPerson_PropertyChanged event handler.
 
public People() {
    emptyPerson = new Person();
    //emptyPerson.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(emptyPerson_PropertyChanged);
    emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    base.InsertItem(this.Count, emptyPerson);
}

void emptyPerson_ChangesCommitted(object sender, EventArgs e) {
    emptyPerson.ChangesCommitted -= new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    emptyPerson = new Person();
    emptyPerson.ChangesCommitted += new EventHandler<EventArgs>(emptyPerson_ChangesCommitted);
    base.InsertItem(this.Count, emptyPerson);

}

When we receive ChangesCommitted on emptyPerson, we know that user has committed changes and we add new emptyPerson, resulting in new empty row being added to DataGrid.

F5 and test the application. Change First Name, notice new row is not added. Change Last name. Now undo all name changes. No new row will be added.

We still have a big issue to take care of. Validation! Try this, just enter First Name, and navigate away from row. DataGrid will commit changes and new row will added. This is not what we want, we need to make sure that Person is valid before committing changes and adding new row. Lets add that crucial validation. What we need to do is to validate data in EndEdit and if data is valid, fire ChangesCommitted event. PersonValidator already has method "Validate" to validate person out of band and we will use that.

Now if we had just to deal with sync validation only, this would turn out to be a simple call, if result of validation is true, we will fire event. However, we are validating city in async manner and that complicates things quite a bit. We need to delay firing of committed event till we get results back from async city validation. Lets see how to do this.

Modify EndEdit as follows

public void EndEdit() {
    if (_editing) {
        _editing = false;
        this._backup = null;// new Person();
        if (null != ChangesCommitted) {
            _fireChangesCommited = true;
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => _validator.Validate());
        }
    }
}
 
Note that we are first validating data and postponing firing ChangesCommitted. Once data is validated, PersonValidator needs to let Person class notify of availability of validation result. This is required since we are carrying out City validation in async fashion. If you do not have any async validation, you can just validate inline and if valid, fire event immediately.
 
Replace PersonValidator with following code:
using Silverlight.CityServiceReference;
using System.ComponentModel;
using System.Collections.Generic;
using System.Windows.Threading;

namespace Silverlight {
    public class PersonValidator : INotifyPropertyChanged {

        #region field/properties
        public const string PROPERTY_NAME_INVALID = "Invalid";
        private Person _data;
        private Dictionary<string,string> _errors;
        private List<string> _propertiesToValidate;
        private bool _fireValidated;

        #region Age
        public const string PROPERTY_NAME_AGE = "Age";

        public bool InvalidAge {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_AGE);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_AGE, "Age must be greater than 0 and less than 200");
                } else {
                    ClearError(PROPERTY_NAME_AGE);
                }
            }
        }

        public void ValidateAge(int newValue) {
            InvalidAge = (newValue < 0 || newValue > 200);
        }
        #endregion

        #region LastName
        public const string PROPERTY_NAME_LASTNAME = "LastName";

        public bool InvalidLastName {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_LASTNAME);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_LASTNAME, "Last Name is required");
                } else {
                    ClearError(PROPERTY_NAME_LASTNAME);
                }
            }
        }

        public void ValidateLastName(string newValue) {
            if(string.IsNullOrEmpty(newValue)){
                InvalidLastName = true;
                return;
            }
            InvalidLastName = (0 == newValue.Trim().Length);
        }
        #endregion

        #region City
        public const string PROPERTY_NAME_CITY = "City";

        public bool InvalidCity {
            get { 
                return _errors.ContainsKey(PROPERTY_NAME_CITY);
            }
            set {
                if (value) {
                    RegisterError(PROPERTY_NAME_CITY, "City is not valid.");
                } else {
                    ClearError(PROPERTY_NAME_CITY);
                }
            }
        }

        private void ValidateCity(string city) {
            if (string.IsNullOrEmpty(city)) {
                InvalidCity = true;
                return;
            }
            CityServiceClient proxy = new CityServiceClient();
            proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
            ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");
            proxy.IsCityValidAsync(city, city);
        }

        void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
            ((Page)Application.Current.RootVisual).EndWait(null);
            InvalidCity = !e.Result;
        }
        #endregion

        #region IsValid
        public const string PROPERTY_NAME_ISVALID = "IsValid";

        public bool IsValid {
            get { 
                return (0 == _errors.Keys.Count);
            }
            set {
                OnPropertyChanged(PROPERTY_NAME_ISVALID);
                Application.Current.RootVisual.Dispatcher.BeginInvoke(() => _data.RaisePropertyChanged("Validator"));
            }
        }
        #endregion

        private bool _manualValidation;
        // use to turn off automatic property validation
        // used with IEditableObject 
        public bool ManualValidation {
            get { return _manualValidation; }
            set { _manualValidation = value; }
        }

        #endregion

        #region Constructors
        public PersonValidator(Person data) {
            _data = data;
            _data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Person_PropertyChanged);
            // default valid
            _errors = new Dictionary<string, string>();
            
        }

        public PersonValidator(Person data, bool defaultInvalid)
            : this(data) {
            if (defaultInvalid) {
                _errors.Add(PROPERTY_NAME_AGE, PROPERTY_NAME_AGE);
                _errors.Add(PROPERTY_NAME_CITY, PROPERTY_NAME_CITY);
                _errors.Add(PROPERTY_NAME_LASTNAME, PROPERTY_NAME_LASTNAME);
            }
        }
        #endregion

        #region Validation Management
        // automatic validation
        void Person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
            if (e.PropertyName == "IsValid") {
                return;
            }
            if (ManualValidation) {
                // can be used to cache list of properties that need to be validated
                return;
            }
            switch (e.PropertyName) {
                case PROPERTY_NAME_AGE:
                    ValidateAge(_data.Age);
                    break;
                case PROPERTY_NAME_LASTNAME:
                    ValidateLastName(_data.LastName);
                    break;
                case PROPERTY_NAME_CITY:
                    ValidateCity(_data.City);
                    break;

            }
        }
        
        // manual, on demand validation
        public void Validate() {
            _fireValidated = true;
            _propertiesToValidate = new List<string>() { 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_AGE, 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_CITY, 
                PROPERTY_NAME_INVALID + PROPERTY_NAME_LASTNAME };
            //
            ValidateAge(_data.Age);
            ValidateLastName(_data.LastName);
            ValidateCity(_data.City);
        }

        public bool AllPropertiesValidated(string propertyValidated) {
            _propertiesToValidate.Remove(propertyValidated);
            return (_propertiesToValidate.Count == 0);
        }

        public event EventHandler<EventArgs> Validated;

        protected void OnValidated() {
            if (null != Validated) {
                Validated(this, new EventArgs());
            }
        }
        #endregion

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string name) {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            //
            if (_fireValidated && AllPropertiesValidated(name)) { 
                _fireValidated = false;
                Application.Current.RootVisual.Dispatcher.BeginInvoke(() => OnValidated());
            }
        }
        #endregion

        #region Error/State management
        public string this[string propertyName] {
            get {
                if (_errors.ContainsKey(propertyName)) {
                    return _errors[propertyName];
                } else {
                    return null;// propertyName;
                }
            }
        }

        public void RegisterError(string propertyName, string message) {
            if (_errors.ContainsKey(propertyName)) {
                _errors[propertyName] = message;
            } else {
                _errors.Add(propertyName, message);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => IsValid = false);
        }

        public void ClearError(string propertyName) {
            if (_errors.ContainsKey(propertyName)) {
                _errors.Remove(propertyName);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => IsValid = true);
        }
        #endregion
    }
}
 
We have added Validated event and OnValidated method to fire event. We also midfield Validate method to turn on delay firing of validated event by setting fireValidated to true. Next OnProeprtyChanged is modified to check if all properties are validated and if we have pending validated event to fire, then we trigger validated event. Note the use of Dispatcher to fire events asynchronously.
 
Now we need to change Person class to subscribe to Validated event.
 
public Person() {
    _validator = new PersonValidator(this);
    _validator.Validated += new EventHandler<EventArgs>(_validator_Validated);
}

void _validator_Validated(object sender, EventArgs e) {
    if (_fireChangesCommited) {
        _fireChangesCommited = false;
        if (_validator.IsValid) {
            Application.Current.RootVisual.Dispatcher.BeginInvoke(() => OnChangesCommitted());
        }
    }
}
 
In constructor, when we are setting up validator, we are also subscribing to validated event. In event handler, we fire ChangesCommitted if data is valid.
 
Finally, F5 and test the application. Add First name, last name and navigate away from row.
 
image
 
No new row is added, and City field has turned red to indicate invalid city.
 
Now enter age and valid city. When you navigate away from row, new row is added
 
image
 
You may have noticed that in order to add new row, you have to navigate up from the last row. This is not normal user behavior. User will try to navigate down, not up! Lets fix that. Modify Page.xaml.cs as follows
 
public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    //this.addButton.Click += new RoutedEventHandler(addButton_Click);
    this.peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
    this.deleteButton.Click += new RoutedEventHandler(deleteButton_Click);
    this.peopleDataGrid.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(peopleDataGrid_BindingValidationError);
    this.peopleDataGrid.KeyUp += new KeyEventHandler(peopleDataGrid_KeyUp);
}

void peopleDataGrid_KeyUp(object sender, KeyEventArgs e) {
    if (Key.Down == e.Key || Key.Enter == e.Key) {
        peopleDataGrid.EndEdit(true, true);
    }
}
 
When user presses Down or Enter key, we force DataGrid to EndEdit, which in turn will trigger validation and if everything checks out ok, eventually resulting in addition of new person/row.
 
We now have a complete application that allows to add new "validated" item, delete item and shows how to carry out extensive validation. We have tackled both sync and async validation. Hopefully you have gained enough know how to start building you own LOB application.
 
While validation is required, it would be better if we can restrict user input and prevent erroneous entry in the first place. We will see how to achieve that next time!
 
Silverlight Business Application Part 5: Validation - Refactored!

This is part five of Building Business Application with Silverlight series that showcases the basic building blocks of a data centric application.  

Series Link: Part 0, Part 1, Part 2, Part 3, Part 4

So far we have seen how to validate data in sync and async fashion. We did validation in response to the changes to data, in setter of the fields. This works fine for doing individual field validation, but we also need a way to validate and track state of the entire object.

Rather than crowding the domain model, the Person class with validation and error state management logic, we will factor out validation into a separate class, PersonValidator. PersonValidator will house logic to validate individual fields and also provide method to validate entire object on demand, not just in response to changes to the data field. It will also be responsible for maintaining overall state of the object.

PersonValidator

Add a new class called PersonValidator.cs to the Silverlight project. Copy over validation code from Person.cs as shown.

using Silverlight.CityServiceReference;
using System.ComponentModel; 
 
namespace Silverlight {  
  
    public class PersonValidator : INotifyPropertyChanged {
 
        private bool _invalidAge;         
        public bool InvalidAge {            
            get { return _invalidAge; }            
            set {                
                if (_invalidAge == value) return;                
                _invalidAge = value;                
                OnPropertyChanged("InvalidAge");            
            }        
        }         
        public void ValidateAge(int newValue) {            
            InvalidAge = (newValue < 0 || newValue > 200);        
        }         
 
        private bool _invalidLastName;         
        public bool InvalidLastName {            
            get { return _invalidLastName; }            
            set {                
                if (_invalidLastName == value) return;                
                _invalidLastName = value;                
                OnPropertyChanged("InvalidLastName");            
            }        
        }         
        public void ValidateLastName(string newValue) {
            if (string.IsNullOrEmpty(newValue)) {
                InvalidLastName = true;
                return;
            }
            InvalidLastName = (0 == newValue.Trim().Length);
        }      
   
        private bool _invalidCity;         
        public bool InvalidCity {            
            get { return _invalidCity; }            
            set {                
                if (_invalidCity == value) return;                
                _invalidCity = value;                
                OnPropertyChanged("InvalidCity");            
            }        
        }         
        
        private bool _isValid;         
        public bool IsValid {            
            get {                 
                return _isValid;             
            }            
            set {                
                if (_isValid == value) return;                
                _isValid = value;                
                OnPropertyChanged("IsValid");            
            }        
        }             
 
        private void ValidateCity(string city) {
            if (string.IsNullOrEmpty(city)) {
                InvalidCity = true;
                return;
            }
            CityServiceClient proxy = new CityServiceClient();            
            proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);            
            ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");            
            proxy.IsCityValidAsync(city, city);        
        }        
 
        void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {            
            ((Page)Application.Current.RootVisual).EndWait(null);            
            InvalidCity = !e.Result;        
        }          
        
        Person _data;        
        public PersonValidator(Person data) {            
            _data = data;            
            _data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_data_PropertyChanged);            
            // default valid            
            _isValid = true;        
        }        
 
        public PersonValidator(Person data, bool defaultInvalid) : this(data) {            
            if (defaultInvalid) {                
                _invalidAge = true;                
                _invalidLastName = true;                
                _invalidCity = true;                
                _isValid = false;            
            }        
        }     
   
        void _data_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {            
            if (e.PropertyName == "IsValid") {                
                return;            
            }            
            switch (e.PropertyName) {                
                case "Age":                    
                    ValidateAge(_data.Age);                    
                    break;                
                case "LastName":                    
                    ValidateLastName(_data.LastName);                    
                    break;                
                case "City":                    
                    ValidateCity(_data.City);                    
                    break;             
            }        
        }   
      
        public void Validate() {            
            ValidateAge(_data.Age);            
            ValidateLastName(_data.LastName);            
            ValidateCity(_data.City);        
        }   
      
        #region INotifyPropertyChanged Members        
        public event PropertyChangedEventHandler PropertyChanged;         
        protected void OnPropertyChanged(string name) {            
            if (PropertyChanged != null)                

PropertyChanged(this, new PropertyChangedEventArgs(name));

            //
            IsValid = !(_invalidAge || _invalidLastName || _invalidCity);        
        }        
        #endregion    
    }
}

Here we are validating three properties:

  1. Age - must be greater than 0 and less then 200
  2. LastName - required field (can not be null or empty)
  3. City - must be from valid list of cities (validated using web service).

We are tracking each field individually and also tracking overall object status using IsValid property.

Now that we have PersonValidator, we have two options for validating data

Option 1 - Manually

From each field setter in Person class, call corresponding validation method in PersonValidator class. This has advantage of being able to throw exception if validation fails and hence using the built in support for basic data validation.

Option 2 - Automatically

Validate data in response to data change notification. This has advantage of being able to change validation without affecting main domain model. We can wire up new field to validate and/or change existing rules, all without changing Person class. (Consider new validation rule that was added for LastName)

While option 1 may seems easier, it relies on exception mechanism which is only available for immediate sync validation. We will use option 2, as it provides consistent behavior across both sync and async validation. Another alternative will be to use a variation of option 1 that does not rely on exception. Main difference is who is in charge, in option 2, PersonValidator works semi autonomously where as in other options, Person class is in charge, directing when/what to validate.

In order to automate validation process, PersonValidator subscribes to PropertyChanged event from Person. Whenever any of the Person property to be validated changes, Person class fires property change event notification, and data is validated  by PersonValidator in property changed event handler. PersonValidator in turn provides valid/invalid notification, which can be used to change the UI.

Now add Personvalidator variable declaration to the Person class and initialize it with object to validate(this) as shown. (Remember to comment out validation logic in Person.cs.)

using Silverlight.CityServiceReference;
using System.Windows.Browser;
 
namespace Silverlight {
    public class Person : INotifyPropertyChanged {
 
        PersonValidator _validator;
 
        #region Constructors
        public Person() {
            _validator = new PersonValidator(this);
        }
 
        public Person(string firstName, string lastName, int age, string city): this() {
            this._firstName = firstName;
            this._lastName = lastName;
            this._age = age;
            this._city = city;
        }
        #endregion
 
        #region Properties
        private string _firstName;
 
        public string FirstName {
            get { return _firstName; }
            set {
                if (value == _firstName) return;
                _firstName = value;
                OnPropertyChanged("FirstName");
            }
        }
 
        private string _lastName;
 
        public string LastName {
            get { return _lastName; }
            set {
                if (value == _lastName) return;
                _lastName = value;
                OnPropertyChanged("LastName");
            }
        }
 
        private int _age;
 
        public int Age {
            get { return _age; }
            set {
                if (value == _age) return;
                //if (value < 0 || value > 200) {
                //    throw new Exception("Age must be between 0 and 200");
                //} 
                _age = value;
                OnPropertyChanged("Age");
            }
        }
 
        private string _city;
 
        public string City {
            get { return _city; }
            set {
                if (value == _city) return;
                _city = value;
                OnPropertyChanged("City");
                //ValidateCity(_city);
            }
        }
 
        //private void ValidateCity(string city) {
        //    CityServiceClient proxy = new CityServiceClient();
        //    proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
        //    ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");
        //    proxy.IsCityValidAsync(city, city);
        //}
 
        //void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
        //    ((Page)Application.Current.RootVisual).EndWait(null);
        //    if (!e.Result) {
        //        HtmlPage.Window.Alert(e.UserState.ToString() + " is not valid, please correct...");
        //    }
        //}
 
 
        public PersonValidator Validator {
            get {
                return this._validator;
            }
        }
        #endregion
 
 
        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
 
        protected void OnPropertyChanged(string name) {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
        #endregion
 
    }
}

We are creating instance of PersonValidator in constructor of Person class and also exposing it has Validator property for data binding. With above in place, it is time to rewire DataGrid to display validation results. Change Page.xaml to following.

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    >
    <UserControl.Resources>
        <src:InvalidToBrushConverter x:Key="brushConverter"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White" Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
            <Button x:Name="deleteButton" Content="Delete" Margin="5"/>
        </StackPanel>
        <!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
        <data:DataGrid 
            x:Name="peopleDataGrid"
            AutoGenerateColumns="False"
            Grid.Row="1"
            >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn 
                    Header="First Name"
                    DisplayMemberBinding="{Binding FirstName}"
                    />
                <!--<data:DataGridTextColumn 
                    Header="Last Name" 
                    DisplayMemberBinding="{Binding LastName}"
                    />-->
                <data:DataGridTemplateColumn
                    Header="Last Name"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}">
                                <TextBlock 
                                Text="{Binding LastName}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn
                    Header="Age"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}">
                            <TextBlock 
                                Text="{Binding Age}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                ToolTipService.ToolTip="Please provide Age between 0 and 200"
                                Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <!--<data:DataGridTextColumn
                    Header="City"
                    DisplayMemberBinding="{Binding City}"
                    />-->
                <data:DataGridTemplateColumn
                    Header="City"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}">
                                <TextBlock 
                                Text="{Binding City}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
        
        <src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
 
    </Grid>
</UserControl>

Note that we have added xmlns:src="clr-namespace:Silverlight" namespace declaration and also added InvalidToBrushConverter as a static resource. InvalidToBrushConverter is used to change background color to Red when validation fails. Add new class called InvalidToBrushConverter.cs as follows.

public class InvalidToBrushConverter : IValueConverter {

    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return new SolidColorBrush(((bool)value ? Colors.Red : Colors.Transparent));
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotImplementedException();
    }

    #endregion
}

Also add using System.Windows.Data namespace direction for IValueConverter. IValueConverter is used to convert the data as it passes through the binding engine, so as to display data in a format that differs from how it is stored. In our case, InvalidToBrushConverter converts invalid value to red color brush.

Ok, with all the code in place, it is time to test. F5 and run the application. Change age field to 222 and city to New City. Clear Last Name.

image

All changes fail validation check and UI is updated via data binding to indicate errors. One thing missing is error message for user. In order to provide user with information about errors, lets add a new converter class, PersonStateToErrMsgConverter.cs to the silverlight project. This class will convert details of person state, represented by instance of PersonValidator to a user friendly message.

using System.Windows.Data;
 
namespace Silverlight {
    public class PersonStateToErrMsgConverter : IValueConverter {
 
        #region IValueConverter Members
 
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            if ((bool)value) {
                return InvalidMessage(parameter.ToString());
            } else {
                return parameter.ToString());
            }
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            throw new NotImplementedException();
        }
 
 
        private string InvalidMessage(string propertyName) {
            switch (propertyName) {
                case "Age":
                    return "Age must be greater than 0 and less then 200";
                case "LastName":
                    return "Last Name is required";
                case "City":
                    return "Please provide valid city";
                default:
                    return propertyName;
            }
        }
        #endregion
    }
}

Here we are using ConverterParameter to pass PropertyName for which we are checking validation result. Also modify Person.xaml to include ToolTip as follows:

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    >
    <UserControl.Resources>
        <src:InvalidToBrushConverter x:Key="brushConverter"/>
        <src:PersonStateToErrMsgConverter x:Key="messageConverter"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White" Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
            <Button x:Name="deleteButton" Content="Delete" Margin="5"/>
        </StackPanel>
        <!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
        <data:DataGrid 
            x:Name="peopleDataGrid"
            AutoGenerateColumns="False"
            Grid.Row="1"
            >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn 
                    Header="First Name"
                    DisplayMemberBinding="{Binding FirstName}"
                    />
                <data:DataGridTemplateColumn
                    Header="Last Name"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border 
                                Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator.InvalidLastName, Converter={StaticResource messageConverter},ConverterParameter=LastName}"                                
                                >
                                <TextBlock 
                                Text="{Binding LastName}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator.InvalidLastName, Converter={StaticResource messageConverter},ConverterParameter=LastName}"                                
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn
                    Header="Age"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
                                    ToolTipService.ToolTip="{Binding Path=Validator.InvalidAge, Converter={StaticResource messageConverter},ConverterParameter=Age}"                                
                                    >
                            <TextBlock 
                                Text="{Binding Age}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                ToolTipService.ToolTip="{Binding Path=Validator.InvalidAge, Converter={StaticResource messageConverter},ConverterParameter=Age}"                                
                                Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn
                    Header="City"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"
                                    ToolTipService.ToolTip="{Binding Path=Validator.InvalidCity, Converter={StaticResource messageConverter},ConverterParameter=City}"                                
                                    >
                                <TextBlock 
                                Text="{Binding City}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator.InvalidCity, Converter={StaticResource messageConverter},ConverterParameter=City}"                                
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
        
        <src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
 
    </Grid>
</UserControl>

And now when validation fails, user is presented with a friendly message about the error.

image

PersonValidator - Take 2

While above works fine, we still have an issue with automatic validation carried out by DataGrid itself, as it is separate from our custom validator. Currently we have two places where validation occurs, DataGrid and PersonValdiator. It will be advantageous if we can combine results of all validations and have a single entity to work with from UI.

One way to achieve above is for PersonValidator to provide a common method to Register validation failure, whether it was carried out inside of PersonValidator or externally, like by DataGrid or some other external class. We will also enhance PersonValidator to provide error messages. Change PersonValidator.cs with following code: (I have kept commented out old code for comparison)

using Silverlight.CityServiceReference;
using System.ComponentModel;
using System.Collections.Generic;
 
namespace Silverlight {
    public class PersonValidator : INotifyPropertyChanged {
 
        public const string PROPERTY_NAME_INVALID = "Invalid";
 
        //private bool _invalidAge;
        public const string PROPERTY_NAME_AGE = "Age";
 
        public bool InvalidAge {
            get { 
                //return _invalidAge;
                return _errors.ContainsKey(PROPERTY_NAME_AGE);
            }
            set {
                //if (_invalidAge == value) return;
                //_invalidAge = value;
                if (value) {
                    RegisterError(PROPERTY_NAME_AGE, "Age must be greater than 0 and less than 200");
                } else {
                    ClearError(PROPERTY_NAME_AGE);
                }
                //OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_AGE);
            }
        }
 
        public void ValidateAge(int newValue) {
            InvalidAge = (newValue < 0 || newValue > 200);
        }
 
        //private bool _invalidLastName;
        public const string PROPERTY_NAME_LASTNAME = "LastName";
 
        public bool InvalidLastName {
            get { 
                //return _invalidLastName;
                return _errors.ContainsKey(PROPERTY_NAME_LASTNAME);
            }
            set {
                //if (_invalidLastName == value) return;
                //_invalidLastName = value;
                if (value) {
                    RegisterError(PROPERTY_NAME_LASTNAME, "Last Name is required");
                } else {
                    ClearError(PROPERTY_NAME_LASTNAME);
                }
                //OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_LASTNAME);
            }
        }
 
        public void ValidateLastName(string newValue) {
            if (string.IsNullOrEmpty(newValue)) {
                InvalidLastName = true;
                return;
            }
            InvalidLastName = (0 == newValue.Trim().Length);
        }
 
        //private bool _invalidCity;
        public const string PROPERTY_NAME_CITY = "City";
 
        public bool InvalidCity {
            get { 
                //return _invalidCity;
                return _errors.ContainsKey(PROPERTY_NAME_CITY);
            }
            set {
                //if (_invalidCity == value) return;
                //_invalidCity = value;
                if (value) {
                    RegisterError(PROPERTY_NAME_CITY, "City is not valid.");
                } else {
                    ClearError(PROPERTY_NAME_CITY);
                }
                //OnPropertyChanged(PROPERTY_NAME_INVALID + PROPERTY_NAME_CITY);
            }
        }
 
        //private bool _isValid;
        public const string PROPERTY_NAME_ISVALID = "IsValid";
 
        public bool IsValid {
            get { 
                //return _isValid; 
                return (0 == _errors.Keys.Count);
            }
            set {
                //if (_isValid == value) return;
                //_isValid = value;
                OnPropertyChanged(PROPERTY_NAME_ISVALID);
                _data.RaisePropertyChanged("Validator");
            }
        }
    
 
        private void ValidateCity(string city) {
            if (string.IsNullOrEmpty(city)) {
                InvalidCity = true;
                return;
            }
            CityServiceClient proxy = new CityServiceClient();
            proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
            ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");
            proxy.IsCityValidAsync(city, city);
        }
 
        void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
            ((Page)Application.Current.RootVisual).EndWait(null);
            InvalidCity = !e.Result;
        }
 
 
        Person _data;
 
        public PersonValidator(Person data) {
            _data = data;
            _data.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_data_PropertyChanged);
            // default valid
            //_isValid = true;
            _errors = new Dictionary<string, string>();
            
        }
        public PersonValidator(Person data, bool defaultInvalid)
            : this(data) {
            if (defaultInvalid) {
                //_invalidAge = true;
                //_invalidLastName = true;
                //_invalidCity = true;
                //_isValid = false;
                _errors.Add(PROPERTY_NAME_AGE, PROPERTY_NAME_AGE);
                _errors.Add(PROPERTY_NAME_CITY, PROPERTY_NAME_CITY);
                _errors.Add(PROPERTY_NAME_LASTNAME, PROPERTY_NAME_LASTNAME);
            }
        }
 
        void _data_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
            if (e.PropertyName == "IsValid") {
                return;
            }
            switch (e.PropertyName) {
                case PROPERTY_NAME_AGE:
                    ValidateAge(_data.Age);
                    break;
                case PROPERTY_NAME_LASTNAME:
                    ValidateLastName(_data.LastName);
                    break;
                case PROPERTY_NAME_CITY:
                    ValidateCity(_data.City);
                    break;
 
            }
        }
 
        public void Validate() {
            ValidateAge(_data.Age);
            ValidateLastName(_data.LastName);
            ValidateCity(_data.City);
        }
 
        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
 
        protected void OnPropertyChanged(string name) {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            //
            //IsValid = !(_invalidAge || _invalidLastName || _invalidCity);
            //
        }
        #endregion
 
        public string this[string propertyName] {
            get {
                if (_errors.ContainsKey(propertyName)) {
                    return _errors[propertyName];
                } else {
                    return null;// propertyName;
                }
            }
        }
 
        private Dictionary<string,string> _errors;
 
        public void RegisterError(string propertyName, string message) {
            if (_errors.ContainsKey(propertyName)) {
                _errors[propertyName] = message;
            } else {
                _errors.Add(propertyName, message);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            IsValid = false;
        }
 
 
        public void ClearError(string propertyName) {
            if (_errors.ContainsKey(propertyName)) {
                _errors.Remove(propertyName);
            }
            OnPropertyChanged(PROPERTY_NAME_INVALID + propertyName);
            IsValid = true;
        }
 
 
 
    }
}

Above shows another way of tracking validation errors. Instead of individual fields tracking result of validation, we are using dictionary to track errors. If there is an entry in dictionary, we have an error, otherwise data is valid. If you have a very large number of properties to track and validate, it will be advantageous to use dictionary as it will reduce memory footprint. Invalid<PropertyName/> methods are now just helper method, making is easy and efficient to databind. Also note the use of indexer for providing common access to errors.

Next modify Person.cs to add RaisePropertyChanged helper method that will be used by PersonValidator to trigger Validator change notification. (This is required to let UI know of changes in validator state. Alternatively, you can expose a property on validator itself that will be a self reference and trigger change notification for that property)

internal void RaisePropertyChanged(string name) {
    OnPropertyChanged(name);
}

Now modify PersonStateToErrMsgConverter.cs to user validator to retrieve error messages as follows:

using System.Windows.Data;
 
namespace Silverlight {
    public class PersonStateToErrMsgConverter : IValueConverter {
 
        #region IValueConverter Members
 
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            PersonValidator validator = value as PersonValidator;
            string propertyName = parameter.ToString();
            if (null != validator) {
                string message = validator[propertyName];
                if (string.IsNullOrEmpty(message)) {
                    // valid
                    return propertyName;
                } else {
                    // invalid
                    return message;
                }
            }
            //
            return value;
            //if ((bool)value) {
            //    return new TextBlock() { Text = InvalidMessage(propertyName) };
            //} else {
            //    return propertyName;// ValidMessage(propertyName);
            //}
        }
 
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            throw new NotImplementedException();
        }
 
 
        //private string InvalidMessage(string propertyName) {
        //    switch (propertyName) {
        //        case "Age":
        //            return "Age must be greater than 0 and less then 200";
        //        case "LastName":
        //            return "Last Name is required";
        //        case "City":
        //            return "Please provide valid city";
        //        default:
        //            return string.Format("Error for {0}", propertyName);
        //    }
        //}
        #endregion
    }
}

Tip: PersonStateToErrMsgConverter.cs above shows how to databind to an indexer. We are using ConverterParameter to provide key to lookup value using indexer.

Now change Page.xaml to use Validator as tooltip provider.

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    >
    <UserControl.Resources>
        <src:InvalidToBrushConverter x:Key="brushConverter"/>
        <src:PersonStateToErrMsgConverter x:Key="messageConverter"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White" Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
            <Button x:Name="deleteButton" Content="Delete" Margin="5"/>
        </StackPanel>
        <!--<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />-->
        <data:DataGrid 
            x:Name="peopleDataGrid"
            AutoGenerateColumns="False"
            Grid.Row="1"
            >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn 
                    Header="First Name"
                    DisplayMemberBinding="{Binding FirstName}"
                    />
                <data:DataGridTemplateColumn
                    Header="Last Name"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border 
                                Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"                                
                                >
                                <TextBlock 
                                Text="{Binding LastName}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=LastName,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"   
                                Tag="LastName"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn
                    Header="Age"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
                                    ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}"                                
                                    >
                            <TextBlock 
                                Text="{Binding Age}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}"                                
                                Tag="Age"
                                >
                            </TextBox>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn
                    Header="City"
                    >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Border Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"
                                    ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=City}"                                
                                    >
                                <TextBlock 
                                Text="{Binding City}" 
                                />
                            </Border>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox
                                Text="{Binding Path=City,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                                Background="{Binding Path=Validator.InvalidCity, Converter={StaticResource brushConverter}}"
                                ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=City}"                                
                                Tag="City"
                                />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
        
        <src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
 
    </Grid>
</UserControl>

And finally modify validation error handler peopleDataGrid_BindingValidationError in Page.xaml.cs to register errors.

private void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    if (e.Action == ValidationErrorEventAction.Added) {
        Person person = ((Control)e.Source).DataContext as Person;
        if (null != person && null != ((Control)e.Source).Tag) {
            person.Validator.RegisterError(((Control)e.Source).Tag.ToString(), e.Error.Exception.Message);
        }
        //((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        //((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, e.Error.Exception.Message);
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        Person person = ((Control)e.Source).DataContext as Person;
        if (null != person && null != ((Control)e.Source).Tag) {
            person.Validator.ClearError(((Control)e.Source).Tag.ToString());
        }
        //((Control)e.Source).Background = new SolidColorBrush(Colors.White);
        //((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, null);
    }
}

When a validation exception occurs, we are registering error with validator, using Tag to provide property name.

Build and run the application

image

We are now capturing external errors and displaying them using the same unified mechanism. Same method can also be used to add other external messages.

One thing that you might have noticed is that we are using two different properties for conveying the same error information. Consider following xaml code snippet:

Background="{Binding Path=Validator.InvalidLastName, Converter={StaticResource brushConverter}}"
ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"                                

We are using InvalidLastName for triggering Background color and Validator with converter parameter LastName, to trigger ToolTip for the same field validation, LastName. You can replace Background binding to Validator as well.

Background="{Binding Path=Validator, Converter={StaticResource brushConverter},ConverterParameter=LastName}"
ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=LastName}"                                

You will have to change InvalidToBrushConverter to use Validator in place of boolean value (just as in PersonStateToErrMsgConverter).

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
    PersonValidator validator = value as PersonValidator;
    if (null != validator) {
        return new SolidColorBrush((!string.IsNullOrEmpty(validator[parameter.ToString()]) ? Colors.Red : Colors.Transparent));
    }
    return new SolidColorBrush(((bool)value ? Colors.Red : Colors.Transparent));
}

This approach is useful if you have lot of properties to validate and /or dynamically building the UI. It does have one disadvantage, any time a property is changed, all binding will be re-evaluated, not just the one that changed. This is because we are binding to the common Validator property in place of field specific Invalid<PropertyName/> property.

You can also optimize some of the async validation code. In case of city validation, instead of directly using city service, we can use a wrapper that will cache results of validation. Cache can then be used as first pass to perform local validation, relying on remote call only when needed.

This takes care of majority of validation situations. The separate validator class also provides us with a starting point to further automate the validation process. One idea is to use attribute based validation. For instance, instead of checking for Last Name, we can decorate LastName property with RequiredField attribute (similar to Asp.net RequiredFieldValidator) and have code to automatically check for that. (This will be is similar to Enterprise Library Validation Application Block functionality, but in context of Silverlight.).

Next we will implement IEditableObject and with PersonValidator, we have all pieces in place to enhance Add New Item functionality.

Silverlight Business Application Part 4: Validation (async)

This is part four of Building Business Application with Silverlight series that showcases the basic building blocks of a data centric application.  

Series Link: Part 0, Part 1, Part 2, Part 3

In the previous post we saw how to validate data on client side in a sync fashion. User enters data, it is immediately validated and any errors are displayed to user. However not all data can be validated on client side.

Consider for example, we may be required to validate city from list of cities stored in some back end data store or a web service. In that case we will have to call back end application server and check for validity of city.

Lets add a Silverlight enabled WCF Service to the SilverlightWeb project to validate the City field.

image

CityService has just one function, IsCityValid to validate the city. Add following code to CityService.svc.cs file

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class CityService {
    [OperationContract]
    public bool IsCityValid(string city) {
        return ("Springfield" == city || "Shelbyville" == city) ;
    }
}

Now add Service Reference to Silvelight project (click on Discover button to display list of available services)

image

Now modify City setter in Person.cs class to call the service and validate city as follows (also add using Silverlight.CityServiceReference; and using System.Windows.Browser; namespace directives)

private string _city;

public string City {
    get { return _city; }
    set {
        if (value == _city) return;
        _city = value;
        OnPropertyChanged("City");
        ValidateCity(_city);
    }
}


private void ValidateCity(string city) {
    CityServiceClient proxy = new CityServiceClient();
    proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
    proxy.IsCityValidAsync(city, city);
}

void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
    if (!e.Result) {
        HtmlPage.Window.Alert(e.UserState.ToString() + " is not valid, please correct...");
    }
}

Now when user enters an invalid city, they are shown an error message.

image

Normally in a traditional application, you will just make a sync call, which will block the UI and on call return inform user if validation failed.

However with Silverlight, all calls to back end systems are async. That means that, as soon as call is issued, call returns and UI is no longer blocked and user is free to continue. Now in general situation, this is a good thing, you do not want to block the UI when carrying out lengthy backend operation.

But let say that we need to block the flow and let user know of the results of validation. We will need some way to prevent user from interaction with page and inform of the operation in progress. We will use a custom user control, Wait Control to accomplish this.

WaitControl for Async Validation

Add a new user control to the Silverlight project and name it WaitControl

image

Add following XAML code in WaitControl.xaml

<UserControl x:Class="Silverlight.WaitControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <Grid x:Name="LayoutRoot">
        <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Opacity="0.75" Fill="WhiteSmoke" />
        <Border CornerRadius="30" Background="CornflowerBlue" Width="600" Height="250">
            <TextBlock Text="{Binding}" Padding="60" FontSize="30"/>
        </Border>
    </Grid>
</UserControl>

We are using Rectangle to fill out the entire background. It is made semi transparent with Opacity of 0.75.

Add following code to WaitControl.xaml.cs (code behind)

public void StartWait() {
    this.Visibility = Visibility.Visible;
}

public void StopWait() {
    this.Visibility = Visibility.Collapsed;
}

On Start method we show the control by toggling the Visibility and on Stop we hide it.

Ok, now that we have the WaitControl, lets add it to the main page. First add xml name space declaration xmlns:src="clr-namespace:Silverlight" in the UserControl start tag as shown

<UserControl x:Class="Silverlight.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:src="clr-namespace:Silverlight"
    >

Now insert following code just before Grid closing tag in Page.xaml

<src:WaitControl Grid.RowSpan="2" x:Name="waitControl" Visibility="Collapsed"/>
 
And finally we need to provide methods to start and end wait. Add following code to Page.xaml.cs
 
public void StartWait(string message) {
    this.waitControl.DataContext = message;
    this.waitControl.StartWait();
}

public void EndWait(string message) {
    this.waitControl.DataContext = message;
    this.waitControl.StopWait();
}

With above in place, it is easy to display screen when making async call and hide it on call back. Modify City validation code in Person.cs class as follows

private void ValidateCity(string city) {
    CityServiceClient proxy = new CityServiceClient();
    proxy.IsCityValidCompleted += new EventHandler<IsCityValidCompletedEventArgs>(proxy_IsCityValidCompleted);
    ((Page)Application.Current.RootVisual).StartWait("Please wait, validating City");
    proxy.IsCityValidAsync(city, city);
}

void proxy_IsCityValidCompleted(object sender, IsCityValidCompletedEventArgs e) {
    ((Page)Application.Current.RootVisual).EndWait(null);
    if (!e.Result) {
        HtmlPage.Window.Alert(e.UserState.ToString() + " is not valid, please correct...");
    }
}

Now when you make a change to city filed, it will display a waiting screen and block the user.

During the call

image

At end of the call

image

Above is certainly not the only option, though easiest! Other option would be to just block city cell (make is read only) and disable any action that would force UI/page changes(like save etc), but leave rest of the UI as is. This way if user is doing data entry, they can continue doing work and while UI is validating data in background.

If validation call to back end returns really fast as in our example, user just sees screen flicker because of WaitScreen. It will be nice to prevent WaitScreen from showing if validation code returns back in few seconds. One easy way to accomplish that behavior is with use of animation.

Add following xaml code to WaitControl.xaml

<UserControl x:Class="Silverlight.WaitControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <UserControl.Resources>
        <Storyboard x:Name="showStoryBoard">
            <DoubleAnimation 
                Storyboard.TargetName="LayoutRoot"
                Storyboard.TargetProperty="Opacity"
                From="0.0" To="1.0" BeginTime="0:0:1" Duration="0:0:1" AutoReverse="False"  />
        </Storyboard>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Opacity="0.0">
        <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Opacity="0.75" Fill="WhiteSmoke" />
        <Border CornerRadius="30" Background="CornflowerBlue" Width="600" Height="250">
            <TextBlock Text="{Binding}" Padding="60" FontSize="30"/>
        </Border>
    </Grid>
</UserControl>
 
Here we are delaying start using BeginTime animation property. Now we just call begin animation at start of wait. Replace current StartWait and StopWait functions in WaitControl.xaml.cs code behind with following ones.
 
public void StartWait() {
    this.Visibility = Visibility.Visible;
    this.showStoryBoard.Begin();
}

public void StopWait() {
    this.showStoryBoard.Stop();
    this.Visibility = Visibility.Collapsed;
}
 
Now when you call StartWait, display is delayed for 1 sec. If call return back in under 1 sec, animation is stopped, and user never sees the wait screen and the resulting flicker.
Other Validation Considerations

We have just seen how to validate individual field/data values, but we also need to track overall state of the object. For instance if Age is not valid, that instance of Person will also not be valid and should not be saved to database.

There are couple of options. Easiest is to maintain separate invalid flag for each field and/or business rules being validated. Then have a property that returns a validate state flag. One nice thing would be to display that as an indicator to user. You can even go further and aggregate various states - error, new, changed and update indicator color by data binding to state!

It will also be good idea to refractor all validation logic into a separate validator class, PersonValidator and delegate validation and error state management to that class, thus separating domain model from validation logic. This has side benefit of being able to validate data at any time(if needed), not just during property changes.

More Posts