Manish Dalal's blog

Exploring .net!

August 2008 - Posts

Silverlight Business Application Part 3: Validation (sync)

This is part three 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

Almost all business application will have varying degree of validations. For our example, let say we have a business rule that states that Age must be a number and be greater than 0 and less than 200.

DataGrid already prevents non numeric entry in Age column, thanks to backing field of type integer. Behind the scene, DataGrid uses System.Convert by way of DataGridValueConverter to coerce values to proper type.

Try to change age to a non number, you will see that as soon as you exit cell, it reverts back to old values. This is because DataGrid tries to convert string to number and when it fails (FormatException), it reverts data back to original value. (If you examine Output window of Visual Studio Editor you will find the message: A first chance exception of type 'System.FormatException' occurred in mscorlib.dll)

You can see this by turning on two attributes on the binding, ValidatesOnExceptions and NotifyOnValidationError. Setting ValidatesOnExceptions to true tells the binding engine to create a validation error when an exception occurs and setting NotifyOnValidationError to true tells the binding engine to raise the BindingValidationError event when a validation error occurs.

Change the DataGrid declaration from

<data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />

to

<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="Age"
            >
            <data:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock 
                        Text="{Binding Age}"
                        />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellTemplate>
            <data:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox
                        Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
                        />
                </DataTemplate>
            </data:DataGridTemplateColumn.CellEditingTemplate>
        </data:DataGridTemplateColumn>
        <data:DataGridTextColumn
            Header="City"
            DisplayMemberBinding="{Binding City}"
            />
    </data:DataGrid.Columns>
</data:DataGrid>

Now setup event handler for BindingValidationError on the peopleDatagrid in the page.xaml.cs file as

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

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
}

F5 and run the application. Enter non numeric value in Age field.

If you examine the “Output” window, you will see that we are getting exception - Input string was not in a correct format..

image

image

We can use above event handler to provide user with more descriptive error message as well as visual feedback. Add following code to BindingValidationError event handler

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        this.Dispatcher.BeginInvoke(() => HtmlPage.Window.Alert(e.Error.Exception.Message));
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
    }
}

Now when you try to enter string, our code gets the notification, user is presented with a alter box and color of cell changes to red to indicate error.

image

You can use Exception type and Source to differentiate between different columns and customize the message. Change BindingValidationError event handler as shown

void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    System.Diagnostics.Debug.WriteLine(e.Error.Exception.Message);
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        string message = e.Error.Exception.Message;
        if (e.Error.Exception is System.FormatException
            && "Age" == ((Control)e.Source).Tag.ToString()) {
            message = "Age must be a number between 0 and 200";
        }
        this.Dispatcher.BeginInvoke(() => HtmlPage.Window.Alert(message));
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.White);
    }
}

Also change xaml CellEditingTemplate for Age as follows

<data:DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <TextBox
            Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
            Tag="Age"
            />
    </DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
 
Notice addition of Tag to identify Age column. If you now try to enter non number in Age field, you get custom error message

image

Let consider domain validation for age. It should not be negative and between 0 and 200. Lets add that validation. Most logical place is setter of Age property in Person class. Change Age setter as

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

Here we are checking to make sure that age is not negative and less than 200. If not, we throw exception.

image

One alternative to disruptive alert message box is to use tooltip. Change BindingValidationError event handler as shown

private void peopleDataGrid_BindingValidationError(object sender, ValidationErrorEventArgs e) {
    if (e.Action == ValidationErrorEventAction.Added) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.Red);
        ((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, e.Error.Exception.Message);
    } else if (e.Action == ValidationErrorEventAction.Removed) {
        ((Control)e.Source).Background = new SolidColorBrush(Colors.White);
        ((Control)e.Source).SetValue(ToolTipService.ToolTipProperty, null);
    }
}

Also change xaml CellEditingTemplate for Age as follows

<data:DataGridTemplateColumn.CellEditingTemplate>
    <DataTemplate>
        <TextBox
            Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
            ToolTipService.ToolTip="Please provide Age between 0 and 200"
            />
    </DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>

Notice addition of ToolTipService.ToolTip to Age column. This provides user with nice help when Age goes in edit mode

image

And when business rules fails

image

It will be nice to prevent user form entering string or negative values in first place. You can do that using AttachedProperties to extend control behavior (similar to AjaxControlToolkit FilterTextboxExtender), but that’s topic for another post!

Above takes care of all sync validations that provide user with immediate feedback. However, what about async validation? We will tackle that next!

Silverlight Business Application Part 2: Delete item

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

Deleting an item is quite similar to adding an item. Instead of adding new item to the collection, we just remove the selected item from the collection.

To allow user to delete the selected person, add new Delete button to StackPanel

<StackPanel Orientation="Horizontal">
    <!--<Button x:Name="addButton" Content="Add" Margin="5"/>-->
    <Button x:Name="deleteButton" Content="Delete" Margin="5"/>
</StackPanel>

Now add handler for delete click event in the page.xaml.cs code behind

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


void deleteButton_Click(object sender, RoutedEventArgs e) {
    DeletePerson();
}

private void DeletePerson() {
    if (null == this.peopleDataGrid.SelectedItem) {
        return;
    }
    //
    Person person = this.peopleDataGrid.SelectedItem as Person;
    if (null == person) {
        return;
    }
    //
    _data.Remove(person);
}

F5 and run the application, try to add some new items and delete them

 image

You can also allow user to delete using key board

void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    //if (Key.Insert == e.Key) {
    //    _data.Add(new Person());
    //}  
    if (Key.Delete == e.Key) {
        DeletePerson();
    }
}

On problem with above solution is the fact that user can delete our empty row at the end and then they have no way of adding new item! Lets fix that by checking for removal of emptyPerson in the People class

protected override void RemoveItem(int index) {
    Person personToRemove = this[index] as Person;
    if (emptyPerson != personToRemove) {
        base.RemoveItem(index);
    }
}

Now when user tries to remove last empty row used to add new item, we detect it and prevent removal of emptyPerson.

In a real application, DeletePerson will eventually call backend application server to remove the item from database. A nice enhancement would be to provide ability recover deleted item, similar to Recycle Bin functionality, by maintaining list of deleted items on the client side.

One alternative to deleting immediately is to collect deleted items in a separate list and batch delete them when user elects to save changes. You can either maintain separate list or keep item in the same list and just change the state to mark them as deleted. This can be further combined with data binding to make DataGrid row read only and rendered with different color to indicate deleted row status. This gives user visibility of all changes and provides a chance to change mind before committing!

At this point we have a complete functioning application that allow user to add data, update data and delete. Next we will take on validation.

Silverlight Business Application Part 1: Add new item

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

In classical applications, the DataGrid or similar UI control takes over the control of data and servers as both the visualizer and data container. When writing logic in such applications, developer deals with DataGrid directly. However with XAML (Silverlight/WPF) there is more stricter separation of duties, UI provides data visualization and all manipulation is done to the data itself.

In order to add new item (row to DataGrid), you have to first add new item to underlying data source, People. When a new Person is added to Person collection (People), a new row appears in the DataGrid. Behind the scenes, when you add new item to the People, the observable collection fires collection changed event that triggers DataGrid to update the UI.

Add a new button to UI by expanding root grid to two rows and adding stack panel to hold the button 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"
    >
    <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"/>
        </StackPanel>
        <data:DataGrid x:Name="peopleDataGrid" Grid.Row="1" />
    </Grid>
</UserControl>

Add handler for button click event in code behind

public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    this.addButton.Click += new RoutedEventHandler(addButton_Click);
}

void addButton_Click(object sender, RoutedEventArgs e) {
    _data.Add(new Person());
}

F5 and run the application. Now when you click Add button a new empty row is added to the DataGrid

image

You can also setup keyboard handlers to allow user to insert item.

public Page() {
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(Page_Loaded);
    this.addButton.Click += new RoutedEventHandler(addButton_Click);
    this.peopleDataGrid.KeyDown += new KeyEventHandler(peopleDataGrid_KeyDown);
}

void peopleDataGrid_KeyDown(object sender, KeyEventArgs e) {
    if (Key.Insert == e.Key) {
        _data.Add(new Person());
    }
}

Add New Item – Take 2

Instead of allowing user to add infinite empty rows, another option is to always provide empty row at the end to allow user to add data. When that row is utilized, a new row is automatically added (similar to behavior seen in Sql Server Management Studio when editing data in a table).

To see this in action, just add following code to the People class (and comment out/remove all previous Add functionality)

private Person emptyPerson;

public People() {
    emptyPerson = new Person();
    emptyPerson.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(emptyPerson_PropertyChanged);
    base.InsertItem(this.Count, emptyPerson);
}

void emptyPerson_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
    emptyPerson.PropertyChanged -= new System.ComponentModel.PropertyChangedEventHandler(emptyPerson_PropertyChanged);
    emptyPerson = new Person();
    emptyPerson.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(emptyPerson_PropertyChanged);
    base.InsertItem(this.Count, emptyPerson);
}

protected override void InsertItem(int index, Person item) {
    if (index >= this.Count) {
        index = this.Count - 1;
        if (index < 0) index = 0;
    }
    base.InsertItem(index, item);
}

Here we are creating and tracking a emptyPerson object. Whenever user makes change to any of the emptyPerson properties, we add another new emptyPerson to the list at the end. Note that InsertItem is also overridden to ensure that all other items are inserted above the last item, which is emptyPerson.

We will also need to modify Page.xaml.cs to instruct DataGrid to commit changes when we automatically add new empty person

Add collection changed event handler for People as

void Page_Loaded(object sender, RoutedEventArgs e) {
    _data = People.GetTestData();
    this.peopleDataGrid.ItemsSource = _data;
    this._data.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_data_CollectionChanged);
}

void _data_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
    this.peopleDataGrid.EndEdit(true, true);
}

When you run the application, a new empty line is always positioned at the end.

image

One issue with above solution is that it will create new empty row even if user just changes a single property and then backs out of the change. A more elegant solution would be to insert new row only when user has committed changes. We will revisit Add and enhance it to provide this behavior when we implement IEditableObject interface in future.

Technorati Tags:
Building Business Application with Silverlight 2 (Beta 2)

 

Silverlight 2 (b2) provides a new way to develop and deploy business applications in the familiar .net environment. Such applications are generally written to automate business processes, provide UI to visualize/manipulate data, and have various business rules ranging from data integrity to complex conditional logic.

In next couple of posts, I am planning to introduce the basic building blocks of a data centric application (primarily using DataGrid control), which can be used to develop full fledge business application.

Caution/Disclaimer: Code is for demo purpose only. It is neither production quality nor does it use any of the best practices.

We will start with the basic data entry functionality, introduce validation and work our way to more advance concepts. Following is a rough outline:

Part 0: Getting Started with Silverlight

Part 1: Adding new item

Part 2: Deleting item

Part 3: Validation (sync)

Part 4: Validation (async, across the wire)

Part 5: Validation (refactored)

Part 6: IEditableObject and Add new item (Take 3!)

Part 7: Beyond Validation, Prevention

Silverlight Business Application Part 0: Getting Started with Silverlight

I am assuming that you have already installed Silverlight (SDK and tools) and are familiar with basic concepts. If not, please head over to http://silverlight.net/GetStarted/, install requisite software and explore various getting started tutorials (Be sure to read Scott Guthrie's 8-part blog series)

Ok, let get started!

Launch Visual Studio and create a new Silverlight application

image

Also create the corresponding web application to host and test the Silverlight application

image

Data Model:

We will use a simple data model consisting of two entities, Person and People. Person class consists of FirstName, LastName, Age and City fields and People class to represent list of persons.

image

Add a new class called Person.cs to the Silverlight project with the following code (you will also need to add using System.ComponentModel namespace directive)

public class Person : INotifyPropertyChanged {
 
    #region Constructors
    public Person() {
    }
 
    public Person(string firstName, string lastName, int age, string city) {
        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;
            _age = value;
            OnPropertyChanged("Age");
        }
    }
 
    private string _city;
 
    public string City {
        get { return _city; }
        set {
            if (value == _city) return;
            _city = value;
            OnPropertyChanged("City");
        }
    }
    #endregion
 
    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnPropertyChanged(string name) {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    #endregion
 
}

Add another class People.cs (to the Silverlight project ) as follows (add  using System.Collections.ObjectModel namespace directive)

public class People : ObservableCollection<Person> {
    public static People GetTestData() {
        return new People() {
            new Person("Homer", "Simpson", 38, "Springfield"),
            new Person("Marge", "Simpson", 33, "Springfield"),
            new Person("Bart", "Simpson", 8, "Springfield")
        };
    }
}

Now add reference to System.Windows.Controls.Data and System.Windows.Controls.Extended to the Silverlight project.

image

DataGrid:

Open Page.xaml and switch to Xaml view to add DataGrid to Xaml.  First add xml name space reference for System.Windows.Controls.Data assembly with prefix data as

xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"

Now add DataGrid xaml declaration to Grid node. Following shows the complete 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"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <data:DataGrid 
            x:Name="peopleDataGrid"
            />
    </Grid>
</UserControl>

In code behind page.xaml.cs file, setup Loaded event and load DataGrid with test data as shown

 
public partial class Page : UserControl {
    People _data;

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

    void Page_Loaded(object sender, RoutedEventArgs e) {
        _data = People.GetTestData();
        this.peopleDataGrid.ItemsSource = _data;
    }
}
 

Now F5 and run the application, you should have following UI

image

Congratulation! Play around with various DataGrid features and get a feel for the UI.

Now that we have a running application, next task is to allow user to add new items, make changes and delete items. We will tackle adding new item in next post.

Technorati Tags:
More Posts