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:
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 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:
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:
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:
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:
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.
18.217.150.123