Implementing data validation

In a perfect world, every user that uses our forms would enter the exact right data at the right places. In such a world, data validation would be useless. We aren't living in such world, and so data validation is a concept no form can go without. Whenever you are interacting with the user, you want to make sure that he inputs what you expect him to input and nothing else. Silverlight 4 introduced two new interfaces for validation—IDataErrorInfo and INotifyDataErrorInfo. These two interfaces join the other approach of data validation in Silverlight known as the data binding exception approach, which includes the NotifyOnValidationError, ValidatesOnExceptions, ValidatesOnDataErrors, and ValidatesOnNotifyDataErrors objects.

Throughout this topic, we are going to work with a sole project—Chapter5-DataValidation. The project contains a simple grid with a few text boxes that we will validate using the various data validation approaches in Silverlight. If you build and run the application right now, it will look, as shown in the following screenshot:

Implementing data validation

Using exception-based validation

The main player in exception-based validation is the ValidatesOnExceptions object of the Binding class. Setting this object to true will inform the binding system to report any binding exception to the object that used that binding expression. This is also the simplest way to add validation to an element. Let's add this form of validation to the Duration textbox. Change the binding expression of the Duration textbox as follows:

{Binding NumDays,Mode=TwoWay,ValidatesOnExceptions=True}

Now, the Binding class knows it needs to notify the user about exceptions. But where do exceptions come from? If your guess was the numDays property's Setter method, you were correct. If we throw an exception in a property's Setter method, and the same property is bound to an element, and has ValidatesOnException set as true; whatever we throw in the exception will be shown to the user. We have already set ValidatesOnException to true in the NumDays binding expression, so let's throw an exception now if the data the user enters isn't a number. Open the FormEntity.cs file and change the numDays property as follows:

public string NumDays
{
get { return numDays; }
set
{
try
{
int number = Convert.ToInt32(value);
numDays = number.ToString();
OnPropertyChanged("NumDays");
}
catch
{
throw new Exception("Only numbers are allowed!");
}
}
}

In the preceding code snippet, we are trying to convert the value that the user inputs to the Int32 type. If the conversation succeeds, then we will set the internal numDays property to the value. If it fails, we will throw an exception to the UI letting the user know that something went wrong. Run the application now and enter letters in the Duration textbox. You will see the result, as shown in the following screenshot:

Using exception-based validation

Using this method, we are not limited to just one exception. If we want to add another exception where the user inputs 0 as the duration length, all we had to do is to change the property as follows:

public string NumDays
{
get { return numDays; }
set
{
try
{
int number = Convert.ToInt32(value);
if(number==0)
throw new Exception("Invalid duration!");
numDays = number.ToString();
OnPropertyChanged("NumDays");
}
catch
{
throw new Exception("Only numbers are allowed!");
}
}
}

Now, when the user inputs 0, another exception will raise, as shown in the following screenshot:

Using exception-based validation

Implementing the IDataErrorInfo interface

With Silverlight 4, the IDataErrorInfo and INotifyDataErrorInfo interfaces were introduced. These interfaces offer a much more flexible alternative to handle exceptions regardless of whether or not the setter of the property was called. The IDataErrorInfo interface contains two properties as follows:

  • Error: This allows you to set a single error message that applies to the entire project
  • Item: This is a collection of error messages that are indexed by the property name, which allows you to set individual error messages to specific fields

In order for our XAML to respond to the IDataErrorInfo inherited class, we must add the ValidatesOnDataErrors object of the Binding class to our binding expression and set its value to true. This object tells the binding engine that instead of responding to exception-based errors, it needs to watch the IDataErrorInfo inherited class for any error reporting.

Add the ValidatesOnDataErrors property to the binding of the Password field, so its Password property looks as follows:

Password="{Binding Password,Mode=TwoWay,ValidatesOnDataErrors=True}"

Now, it's time to change the FormEntity class so that it inherits from IDataErrorInfo as well as INotifyPropertyChanged. Change the class declaration as follows:

public class FormEntity:INotifyPropertyChanged,IDataErrorInfo

Implement the new interface, and you should see two new properties added to your class:

public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
get { throw new NotImplementedException(); }
}

These are the two properties we mentioned earlier. Error is the class-level error message property, and the second property is the Item property, which is based on a column name returning an error message. As Error is referring to the object itself, we have no use for it for now, and we will deal with the Item property now.

The validation we wish to implement for the Password field is that the minimum length for a password must be five characters. Let's change the Item property of the IDataErrorInfo interface to implicate our logic:

public string this[string columnName]
{
get
{
string errorMessage = string.Empty;
switch (columnName)
{
case "Password":
if (Password.Length < 5)
errorMessage = "Minimum password length is 5 charecters";
break;
}
return errorMessage;
}
}

By using a switch/case method on the columnName argument, we can set the logic for any object in our application that has the ValidatesOnDataErrors object set to true in its binding expression. Once we find the column that raised the property, we will just set the error message we wish and return it to the object. Running the application now will result in the following screenshot:

Implementing the IDataErrorInfo interface

Implementing the INotifyDataErrorInfo interface

While IDataErrorInfo works synchronously and doesn't support multiple errors for the same property, the INotifyDataErrorInfo interface works asynchronously and supports multiple errors on the same property. And why do we need asynchronous data validation, you will ask. Well that's easy. In our form, we have a UserName field. If we want to check whether or not a specific username is already taken without blocking the UI layer, we have to check it asynchronously. The INotifyDataErrorInfo interface contains three members as follows:

  • GetErrors: This is a method that returns a collection (of the IEnumerable type) of validation errors for a specific field. As the method returns a collection, we can return more than one error per field.
  • HasErrors: This is a simple Boolean property that returns true if the object contains any errors, or false if it doesn't.
  • ErrorsChanged: This is an event similar to the PropertyChanged event in the INotifyPropertyChanged interface, which must be raised when you either add, remove, or change errors to notify the binding system that a change has occurred.

It's important to note that Silverlight will call the GetErrors method on each public member of the class, even if you didn't specifically set it up. If you implement the INotifyDataErrorInfo interface, you must take care of the GetErrors method.

First, let's add an event handler just like we did with the INotifyPropertyChanged interface so that the properties can raise the ErrorsChanged event. Add the following code snippet to your FormEntity.cs file:

private void NotifyErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
handler(this, new DataErrorsChangedEventArgs(propertyName));
}

Next, we need to add a Dictionary object that uses the property name as key and a list of strings for the error messages for that property. Add the following private dictionary to your FormEntity.cs file:

private Dictionary<string, List<string>> errors;

To read data from the errors dictionary, add the following property:

public Dictionary<string,List<string>> Errors
{
get
{
if (errors == null)
{
errors = new Dictionary<string, List<string>>();
}
return errors;
}
}

When we call the Errors property for the first time, it will initiate the dictionary. The next time, it will just return it.

Next, let's implement the HasErrors property. This is going to be a very easy implementation as the HasErrors property's job is just to return true if there are any errors we stored or not. Change the HasErrors property as follows:

public bool HasErrors
{
get { return Errors.Values.Count > 0; }
}

The last method of the INotifyDataErrorInfo interface members we need to implement is the GetErrors method. Just like the IDataErrorInfo interface's Item property, the GetErrors method gets a property name as argument and outputs a list of errors for that specific property name. Change the GetErrors method as follows:

public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (!Errors.ContainsKey(propertyName))
{
Errors.Add(propertyName, new List<string>());
}
return Errors[propertyName];
}

Within our web project we have a WCF service. The service is called UsernameService.svc and it contains a single method, CheckUsername, which gets a username as an argument and checks if it's one of the three predefined occupied usernames. If the supplied username is already taken, the method will return false; if it's free, it will return true.

To add a reference to the web service in the Silverlight project, right-click on References, add service reference, and click on Discover. Use UsernameService as the namespace.

Once we have the entire infrastructure ready, let's add a call to the method to actually check if the username is taken or not. In the FormEntity.cs file, change the Login property setter as follows:

set
{
login = value;
OnPropertyChanged("Login");
CheckForUsername();
}

Now, we must add the CheckForUsername method. Add the following code snippet to the FormEntity.cs file:

public void CheckForUsername()
{
UsernameService.UsernameServiceClient proxyClass = new UsernameService.UsernameServiceClient();
proxyClass.CheckUsernameCompleted += new EventHandler<UsernameService.CheckUsernameCompletedEventArgs> (proxy_CheckUsernameCompleted);
proxyClass.CheckUsernameAsync(Login);
}

Just like we did in the previous chapter, we will add a proxy class for the Username service, attach a complete event handler, and call the CheckUsername method asynchronously, passing to it the currently typed login name.

The implementation of the completed event handler of the Checkusername method is as follows:

void proxy_CheckUsernameCompleted(object sender, UsernameService.CheckUsernameCompletedEventArgs e)
{
if (e.Error == null)
{
bool result = e.Result;
if (result == false)
{
CreateErrorInDictionary("Login");
Errors["Login"].Add("Username taken!");
NotifyErrorsChanged("Login");
}
}
}

First, we cast the result of the method as Boolean, as we know it can return either true or false. If the result is false, that means the username is not available, and we should notify the user. To do that, we call the CreateErrorInDictionary method, which checks if the property already has a key in the dictionary and if not, it creates a key. The implementation of CreateErrorInDictionary is as follows:

private void CreateErrorInDictionary(string property)
{
if (!Errors.ContainsKey(property))
{
Errors.Add(property, new List<string>());
}
}

Once we know for sure that the Errors dictionary contains a key for our property, we will add an error message and use the NotifyErrorChanged method we created earlier to let the binding engine know an error was raised.

That's it! You've created a working implementation of the INotifyDataErrorInfo! Run your application, and try to enter one of the following usernames—Silverlight, SL4, or Packt. Your screen will be similar to the following screenshot:

Implementing the INotifyDataErrorInfo interface

In a real-world scenario, the web service will probably perform a check against a database or something similar to check if a username is taken. An action like this can be time consuming and, thus, performing this activity asynchronously makes a lot of sense. We want the user to keep on going with the form, and once we get back a result from the web service, we should update him.

That concludes our discussion on data validation in Silverlight. I hope you'll take what you learned here and use it to create a better, safer, and interactive experience for the user while interacting with him/her through your application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.236.27