Silverlight Data Validation
New interfaces introduced in Silverlight 4 greatly enhance validation
April 16, 2010
It used to be simple. We'd write a desktop application and write code in the UI that ensured a user didn't give us bad data. In those days a one-tier or two-tier application was common, and doing the validation directly in the UI was adequate. But much has changed. For more information on Silverlight, see "Sizing Up Client Development Choices: XAML-Silverlight or JavaScript-HTML5" and "The Case for Silverlight in a HTML5 World."
Now that we're separating our UI from our data, it's becoming increasingly important to have a simple system for gathering validation errors and displaying them to users. This is especially interesting because we need to validate our data on the client and on the server. Since Silverlight 3, Microsoft has invested in making this easier. Let's look at how data validation works now in Silverlight.
How Silverlight Validation Works
In Silverlight 4 (and 3) most edit controls include implicit support for exposing validation errors directly to a user via the user interface. In Figures 1 and 2, you can see how the default TextBox shows validation errors.
Figure 1: Valid Data
The look of the validation is exposed directly in the ControlTemplate of most controls, so that you can style the validation look just like any other part of a control. Typically, this is implemented as a VisualStateManager group. Because the controls can now support showing validation errors, you'll need a systematic way of exposing validation errors to the user interface.
Figure 2: Invalid Data
Validation in Silverlight 3
Silverlight 3 is the first release that included these validation-friendly controls. To take advantage of the controls' validation features, data binding included two important properties of the markup extension: NotifyOnValidationError and ValidatesOnExceptions. The following code shows a simple set of controls using these binding properties to enable showing validation errors:
The NotifyOnValidationError property allows the BindingValidationError event to be called when a validation error happens. The BindingValidationError event could be caught directly by you but more commonly it's caught by the validation infrastructure so that you can show the validation error. But when does a validation error actually happen? That's where the ValidatesOnExceptions property comes in. This property tells the control that if an exception happens during transfer of the property back to the underlying data source, then treat the exception as a validation error. For example, if we have a simple class with a Name property, we could throw an exception in the setter of the Name property if it was blank (since it's required). You can see this code in Figure 3.
public class Game{ string _name; public string Name { get { return _name; } set { if (string.IsNullOrEmpty(value)) { throw new ValidationException("Name is required"); } _name = value; } }...}
Figure 3 shows validation of the new Name value and throwing an exception if it is invalid. While this example shows a ValidationException being thrown, any exception will be treated as a validation error. To make this simpler, a number of attributes exist in the System.ComponentModel.DataAnnotations namespace. For example, you can use the Required attribute to get the same behavior we had before, as shown in Figure 4.
public class Game{ string _name; [Required] public string Name { get { return _name; } set { _name = value; } }...}
These validation attributes let you specify the error message or, alternatively, an error message resource name and type (to make localizing the error messages easy). These validation attributes are shown in Figure 5.
Attribute |
---|
Figure 5: Validation Attributes |
Required |
StringLength |
Range |
RegularExpression |
CustomValidation |
These attributes are really helpful in performing property validation. But simply placing them on the properties does not do the trick. You'll need to fire off the validation inside each of your property setters. You can do this with the ValidationContext and Validator classes, as shown in Figure 6.
[Required]public string Name{ get { return _name; } set { // Use the Validation classes to // validate against attribute var ctx = new ValidationContext(this, null, null); ctx.MemberName = "Name"; Validator.ValidateProperty(value, ctx);_name = value; }}
The ValidationContext needs to contain a reference to the object to be validated and the member name to be validated. Calling the Validator.ValidateProperty static method checks the proposed value against any and all validation attributes. If it fails, it throws a ValidationException so that it is propagated to the control(s).
This validation strategy seems simple if you're building your own client-side classes but, in practice, it's not a viable strategy. The problem is that most of our bound data is coming in the form of data from the server. This data is exposed as code-generated classes and these code-generated files make it difficult to add these attributes and the validation code. One solution is to use WCF RIA Services as your data layer.
By using WCF RIA Services to wrap your server-side code, the client-side code that is generated will include both the attributes and generated code in the client to force the validation. For example, in our client-side Game class, our Name property is not only automatically annotated with the attributes, it calls a method inside the setter to perform the data validation (as seen in Figure 7).
[DataMember()][Description("The Name of the Game")][Required()][StringLength(100)]public string Name{ get { return this._name; } set { if ((this._name != value)) { this.ValidateProperty("Name", value); this.OnNameChanging(value); this.RaiseDataMemberChanging("Name"); this._name = value; this.RaiseDataMemberChanged("Name"); this.OnNameChanged(); } }}
Explaining how WCF RIA Services works is outside the scope of this article, but if you want to use these attributes without having to create your own client-side classes, WCF RIA Services is the most straightforward way to accomplish that in Silverlight 3.
The reality is that the Silverlight 3 solution for validation does not go far enough. It assumes that property-level validation is all that is needed. In addition, it forces you to use a specific data layer to get any real functionality. While the appearance of validation in Silverlight 3 was helpful, it was severely handicapped by how it worked. Luckily for us, in Silverlight 4 this has changed.
Changes for Silverlight 4
This problem of surfacing validation errors is not new. In fact, in Windows Forms, this problem was solved with a simple interface called IDataErrorInfo. This interface exposes a way to communicate errors back to a UI container about errors on the object. When Silverlight 3 announced its validation framework, many developers asked why not just support IDataErrorInfo, since it's a well-worn way to communicate validation errors. In Silverlight 4, Microsoft did just that—but also went beyond that simple support.
The IDataErrorInfo interface is simple; it has an interface for retrieving object- and property-level validation errors, as seen in the following code:
public interface IDataErrorInfo{ string Error { get; } string this[string columnName] { get; }}
The Error property should have object-level validation information while this property supports retrieving validation errors about specific "columns" of data. Since it is an interface, it becomes trivial to add to the partial classes that are created by WCF Services, WCF Data Services, or even WCF RIA Services. But because this validation information is not communicated though exceptions, we can implement it by validating the actual data. Figure 8 shows an example implementation.
public partial class Game : IDataErrorInfo{ string IDataErrorInfo.Error { get { if (Description.CompareTo(Name) == 0) { return "Description and Name cannot be the same string"; } return ""; } } string IDataErrorInfo.this[string columnName] { get { switch (columnName) { case "Name": { if (string.IsNullOrEmpty(Name)) { return "Name is Required"; } break; } default: break; } return ""; } }}
To use this interface, you must tell the data bindings that you expect your object to have an IDataErrorInfo interface implementation. To do this, add the ValidatesOnDataErrors property on the binding as shown in the code below:
Name
This introduces a troubling problem. Using different binding properties to indicate the different types of validation implies that my UI must know how my underlying objects are going to relate their validation errors. This troubles me but currently this is just the way it is implemented, so we're going to have to live with it.
The indexer for the interface makes it relatively simple to check for valid data and return the appropriate property-level validation. This interface works well for communicating the property- or column-level validation errors, but the object-level validation leaves a bit to be desired. In fact, this interface is included to use existing implementations that already expose this interface. For new development, the Silverlight team has come up with a better solution: the INotifyDataErrorInfo interface.
The INotifyDataErrorInfo interface is a better fit for Silverlight because it allows for asynchronous validation (to support server-side validation). The INotifyDataErrorInfo interface supports the interface shown in the following code:
public interface INotifyDataErrorInfo{ bool HasErrors { get; } event EventHandler ErrorsChanged; IEnumerable GetErrors(string propertyName);}
The big difference in this interface and IDataErrorInfo is the inclusion of an event to let the listener know that the errors have changed. In addition, instead of an indexer for retrieving the errors, it changes this to a method because the results may be different based on when you call it. Note that the GetErrors method returns a list of errors instead of a simple string, because a property may have more than one error to address. Currently, passing in an empty string or a null for the propertyName should return object-level validation errors. So this method is there to handle property- and object-level validation.
Note that the example shows a Dictionary that contains a list of errors for each property name. That way when we implement HasErrors and GetErrors, we can simply refer to this list of errors. While it might seem simpler to check during GetErrors or HasErrors, this pattern is especially crucial for asynchronous support for validation (e.g., going to a server to retrieve the status of a property or the object). Figure 9 shows a web service being called to validity of the ReleaseDate as it changes.
public partial class Game : INotifyDataErrorInfo{ Dictionary> errors; Dictionary> AsyncErrors { get { if (errors == null) { errors = new Dictionary>(); } return errors; } } event EventHandler errorsChanged; event EventHandler INotifyDataErrorInfo.ErrorsChanged { add { errorsChanged += value; } remove { errorsChanged -= value; } } IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) { if (!AsyncErrors.ContainsKey(propertyName)) { AsyncErrors.Add(propertyName, new ObservableCollection()); } return AsyncErrors[propertyName]; } bool INotifyDataErrorInfo.HasErrors { get { return AsyncErrors.Where(d => d.Value.Count() > 0).Any(); } }}
In the callback from the web service, we add the error to the collection of errors and throw the event so that anyone who is listening for the change will be alerted to get a new list. This is shown in Figure 10. By including the new interfaces in Silverlight 4, we can handle validation in many other scenarios that were simply impossible in prior versions.
partial void OnReleaseDateChanged() { var client = new GamesServiceClient(); client.CheckReleaseDateCompleted += OnCheckReleaseDateComplete; client.CheckReleaseDateAsync(ReleaseDate); } void OnCheckReleaseDateComplete(object s, CheckReleaseDateCompletedEventArgs e) { if (e.Error != null || !e.Result) { AsyncErrors["ReleaseDate"].Add("Release Date is not valid."); if (!AsyncErrors.ContainsKey("ReleaseDate")) { AsyncErrors["ReleaseDate"] = new ObservableCollection(); } if (errorsChanged != null) { errorsChanged(this, new DataErrorsChangedEventArgs("ReleaseDate")); } } else { if (AsyncErrors["ReleaseDate"].Count > 0) { AsyncErrors["ReleaseDate"].Clear(); if (errorsChanged != null) { errorsChanged(this, new DataErrorsChangedEventArgs("ReleaseDate")); } } } }
Where We Are
Because Silverlight is touted as a solution for building line-of-business applications, data validation is crucial to streamlining development. Starting in Silverlight 3, we can see the beginnings of a real validation framework. Now that we are approaching Silverlight 4, the validation framework is maturing so that it can be used by more than simple applications.
The new interfaces introduced in Silverlight 4 allow us to use existing code (that may use the IDataErrorInfo interface) and to start exposing validation from our new objects (via INotifyDataError). Because the controls now support this validation, we can expose validation errors directly to our users.
Shawn Wildermuth ([email protected]) is president of COM Guru, a Boston area consulting company that develops large-scale websites and Windows 2000 implementations.
About the Author
You May Also Like