October 2009
Volume 24 Number 10
Data Points - Data Validation with Silverlight 3 and the DataForm
By JOHN PAPA | October 2009
Building robust Silverlight applications that edit and validate data got a whole lot easier with the introduction of the DataForm control in Silverlight 3. The DataForm control contains several features to help create forms that allow adding, editing, deleting and navigating through sets of data. The greatest benefits of the DataForm are its flexibility and customization options.
In this month’s column, I will show how the DataForm control works and how it can be customized, and I’ll demonstrate it in action. I’ll begin by presenting a sample application that uses several features to bind, navigate, edit and validate data using the DataForm. Then I’ll walk through how the sample application works, while pointing out the basic requirements to use the control and customizations I recommend. Though the DataAnnotations are not required by the DataForm , they can influence the appearance, validation aspects and behavior of the DataForm. The DataAnnotations can also indicate a custom validation method to invoke for a property or an entire entity. I will show how to implement the DataAnnotations on an entity and how to write your own custom validation methods. All code for this article can be downloaded (msdn.microsoft.com/magazine/msdnmag1009.aspx).
What’s the Endgame?
Before diving into the code, let’s take a look at the sample application that demonstrates the navigation, validation and editing features of the DataForm. Figure 1 shows a DataGrid displaying a list of employees and a DataForm where each employee record can be edited. The employees in the DataGrid can be navigated, changing which is the currently selected employee. The DataForm is bound to the same set of employees and receives that employee.
Toolbar
The DataForm shown in Figure 1 has been placed in edit mode so the user can make changes and either save or cancel them. The toolbar at the upper right of the DataForm shows icons for editing (pencil), adding a new record (plus sign) and deleting a record (the minus sign). These buttons can be styled and disabled as needed. The state of the buttons is determined by the interfaces implemented by the entity, while their visibility can be customized by setting properties on the DataForm.
Labels
The captions for each TextBox in the DataForm can be placed beside or above the control. The content of the labels can be metadata-driven using the Display DataAnnotations attribute. The DataAnnotations attributes are placed on the properties of the entity, in this case the Employee entity, where they help feed the DataForm information, such as the text to display in the field labels.
DescriptionViewer
When the DataForm is in edit mode (as shown in Figure 1), the user can hover over the icon next to the caption beside each TextBox control to see a tooltip that provides more information on what to enter in the field. This feature is implemented by the DataForm when you place a DescriptionViewer control beside the bound TextBox control. The text displayed in the tooltip of the DescriptionViewer is also fed from the DataAnnotations Display attribute.
Figure 1 Validation and Editing with the DataForm
Cancel and Commit
The Cancel button at the bottom of the DataForm is enabled when in edit mode. The Save button is enabled when in edit mode and a user changes a value in the DataForm. The text and styles for these buttons can be customized.
Figure 2 DataForm out of the Box
Validation Notification
NotificationThe DataForm in the sample is shown in the edit state where the user has typed in an invalid e-mail address and phone number. Notice that the field captions for the invalidated fields are in red and the textboxes are bordered in red with a red flag in the upper-right corner. When a user puts the cursor in an invalidated control, a tooltip appears with a message describing what has to be done to correct the problem. Figure 1 also shows a list of the validation errors at the bottom of the screen. All of these features are supported by the DataForm and DataAnnotations and can use custom styles to give a custom look and feel. The validation rules are fed from several of the DataAnnotations to indicate canned rules, such as required fields, or custom rules.
Designing the DataForm
The DataForm, found in the Silverlight Toolkit, can be added to a project once the assembly System.Windows.Controls.Data.DataForm.Toolkit.dll has been referenced. The DataForm can be created with very little setup. Once dragged onto the design surface in Blend or created in XAML—by simply setting the ItemsSource of the DataForm to a collection of entities—the DataForm will automatically generate the fields to display and then enable appropriate editing features. The following XAML will generate the DataForm shown in Figure 2:
<dataFormToolkit:DataForm x:Name="dataForm" ItemsSource="{Binding Mode=OneWay}" />
Note: The binding mode is set to OneWay in the previous example only for explicit purposes. It is not required. However, I find it useful for clarity and for supportability to explicitly set the binding mode.
Additional settings can be hooked up to the DataForm to refine its functionality. Common settings include the ones shown in Figure 3.
Figure 3 Common DataForm Customizations
Often, it is beneficial to style the DataForm’s visual features to coordinate them with the visual appearance of your application. Several aspects of the DataForm can be styled using resources in the document, app.xaml or a resource dictionary. There are several styles that can be set, as shown in Figure 4 in the partial screenshot from Expression Blend. Notice that CancelButtonStyle and the CommitButtonStyle are both set to a value. This is what gives the buttons the blue appearance as shown in the bottom of the screen in Figure 1. The DataForm itself can be styled by setting the Style property. Individual aspects of the DataForm, such as the DataField or ValidationSummary, can also be styled.
By setting some of these more common properties of the DataForm as well as some basic styles, we achieve the appearance and functionality displayed in Figure 1. The settings for the sample application included with this article are shown in Figure 5. Notice that AutoEdit and AutoCommit are both set to false, thus requiring that the DataForm be explicitly put into edit mode by the user clicking on the edit button on the toolbar, and that the user click the Save button to commit the changes.
Figure 4 Styling the DataForm
The navigation button is omitted from the CommandButtonsVisibility property, which means that the toolbar will not show the navigation options. If we include this option either by adding the keyword Navigation to the CommandButtonsVisibility or by changing the setting to the keyword All, the DataForm would also show the navigation toolbar buttons. Since the Commit and Cancel buttons are included in the CommandButtonsVisibility property, they will be visible. While the DataForm has not been edited yet, the Cancel button will be disabled as shown in Figure 6. The navigation buttons allow the user to move around the collection of records. However, in this sample application, the users can also navigate using the data-bound DataGrid, since it is also bound to the same set of data as the DataForm. Therefore, I turned off the navigation buttons in the toolbar purely as a design choice, because the navigation functionality can already be achieved through the DataGrid.
DataAnnotations
A lot of the functionality of the DataForm is driven by the DataAnnotations attributes placed on the entity data bound to the DataForm. The DataAnnotations are available once you reference the assembly System.ComponentModel.DataAnnotations.dll. The DataForm examines the attributes and applies them accordingly. Figure 7 shows several of the DataAnnotations attributes that can decorate properties of an entity and describes their effect.
Figure 5 Customizing the DataForm
<dataFormToolkit:DataForm x:Name="dataForm"
ItemsSource="{Binding Mode=OneWay}"
BorderThickness="0"
FontFamily="Trebuchet MS" FontSize="13.333"
CommitButtonContent="Save"
CancelButtonContent="Cancel"
Header="Employee Details"
AutoEdit="False"
AutoCommit="False"
AutoGenerateFields="True"
CommandButtonsVisibility="Edit, Add, Delete, Commit, Cancel"
CommitButtonStyle="{StaticResource ButtonStyle}"
CancelButtonStyle="{StaticResource ButtonStyle}"
DescriptionViewerPosition="BesideLabel"
LabelPosition="Left" />
The Display attribute feeds the DataForm’s Label and DescriptionViewer with the Name and Description parameters, respectively. If omitted, the DataForm automatically generates these values for the controls using the name of the property. Notice in Figure 8 that the Employee entity’s FirstName property is decorated with 3 DataAnnotations attributes. The Display attribute indicates that the Label should display the text “First Name” and the DescriptionViewer should show a tooltip of “Employee’s first name.”
Figure 6 Customizing the Buttons
The Required attribute indicates that a value must be entered for the FirstName property. The Required attribute, like all validation attributes, accepts an ErrorMessage parameter. The ErrorMessage parameter indicates the message that will be displayed both in a tooltip for the invalidated control and in the ValidationSummary control in the DataForm. The StringLength attribute for the FirstName property is set to indicate that the field cannot exceed 40 characters, and if it does, an error message will be displayed.
The code in Figure 8 shows that when a value is set on the property, the Validate method is invoked. Validate is a method I created in the base class ModelBase I used for all entities in this sample. This method validates the property by checking it against all of its DataAnnotations attributes. If any of them fail, it throws an exception, does not set the property to the invalid value, and directs the DataForm to notify the user. Figure 1 shows the results of emptying the FirstName field, since it is required.
The ModelBase class’s Validate method is invoked in every property setter so each property can be validated when set. The method accepts the new value and the name of the property being validated. These parameters are in turn passed to the Validator class’s ValidateProperty method, which does the actual validation on the property value:
protected void Validate(object value,
string propertyName) {
Validator.ValidateProperty(value,
new ValidationContext(this, null, null) {
MemberName = propertyName });
}
The Validator is a static class that allows you to perform validation using several different techniques:
- The ValidateProperty method checks a single property's values and throws DataAnnotations.ValidationException if the value is invalid.
- The ValidateObject method checks all properties on the entire entity and throws DataAnnotations.ValidationException if any value is invalid.
- The TryValidateProperty method performs the same validation checks as ValidateProperty, except it returns a Boolean value instead of throwing an exception.
- The TryValidateObject method performs the same validation checks as ValidateObject, except it returns a Boolean value instead of throwing an exception.
- The ValidateValue method accepts a value and a collection of validation attributes. If the value fails any of the attributes, a ValidationException is thrown.
- The TryValidateValue method performs the same validation checks as ValidateValue, except that it returns a Boolean value instead of throwing an exception.
Figure 7 Common DataAnnotations
Figure 8 FirstName Property with DataAnnotations
private string firstName;
[Required(ErrorMessage = "Required field")]
[StringLength(40, ErrorMessage = "Cannot exceed 40")]
[Display(Name = "First Name", Description = "Employee's first name")]
public string FirstName
{
get { return firstName; }
set
{
if (firstName == value) return;
var propertyName = "FirstName";
Validate(value, propertyName);
firstName = value;
FirePropertyChanged(propertyName);
}
}
I also created a method in the ModelBase class named IsValid, which calls the TryValidateObject method, as shown below. This method can be called on the entity at any time to determine if it is in a valid state:
public bool IsValid() {
return Validator.TryValidateObject(this,
new ValidationContext(this, null, null),
this.validationResults, true);
}
Figure 9 Custom Property Validation
public static class EmployeeValidator
{
public static ValidationResult ValidateAge(int age)
{
if (age > 150)
return new ValidationResult("Employee's age must be under 150.");
else if (age <= 0)
return new ValidationResult(
"Employee's age must be greater than 0.");
else
return ValidationResult.Success;
}
}
Custom Validation
When a canned attribute from the set of DataAnnotations does not suffice, you can create a custom validation method and apply it to an entire object or to a property value. The validation method must be static and return a ValidationResult object. The ValidationResult object is the vehicle that contains the message that will be bound to the DataForm. Figure 9 shows the ValidateAge custom validation method, which verifies that the value is between 0 and 150. This is a simple example, and a Range attribute could have been used. However, creating a specific method allows it to be reused by multiple entities for validation.
Custom validation methods can also be created to validate an entire object. The ValidateEmployee method accepts the entire Employee entity and examines several properties in determining if the entity is in a valid state. This is very helpful for enforcing entity-level validation, as shown below:
public static ValidationResult ValidateEmployee(Employee emp) {
string[] memberNames = new string[] { "Age", "AnnualSalary" };
if (emp.Age >= 50 && emp.AnnualSalary < 50000)
return new ValidationResult(
"Employee is over 50 and must make more than $50,000.",
memberNames);
else
return ValidationResult.Success;
}
The Validation Summary control is displayed by the DataForm when one or more ValidationExceptions are thrown (see Figure 10). The name of the field is highlighted in bold lettering while the validation message is displayed beside it. The style for the ValidationSummary can also be customized to use a resource, if desired.
Figure 10 Validation Summary
Influencers
The DataForm takes its cue from DataAnnotations as well as from the interfaces that its bound data source implements. For example, the Employee entity implements the IEditableObject interface. This interface forces the implementation of the BeginEdit, CancelEdit and EndEdit methods that are invoked by the DataForm at the appropriate times while the DataForm is being edited. In the sample application, I used these methods to implement a cancel behavior so the user can press the Cancel button and restore the original entity values.
This behavior is achieved by creating an instance of the entity in the BeginEdit method (named cache) and copying the original values from the entity. The EndEdit method clears the cache object while the CancelEdit method copies all values form the cache entity back to the original entity, thus restoring its original state.
When the DataForm sees that the collection it is bound to is a CollectionViewSource, it will support editing, sorting, filtering and tracking the current record (also known as currency). The currency feature is important, as it also ensures that the DataForm does not allow a user to navigate away from an invalid entity state without fixing it and committing or canceling the changes. Binding the DataForm to a PagedCollectionView also provides these same features and also offers deleting of an item, adding an item and grouping items. Binding to an ObservableCollection will disable some of these features, as on its own the ObservableCollection does not implement the interfaces that the DataForm looks for.
If your entities are already contained within an ObservableCollection, you can create a PagedCollectionView by passing the ObservableCollection instance into the collection via its constructor, as shown below:
var employees = new DataService().GetPeople();
PagedCollectionView view = new PagedCollectionView(employees);
this.DataContext = view;
Alternatively, you can create a CollectionViewSource by passing the ObservableCollection of employees into the CollectionViewSource’s collection initializer and setting the Source property, as shown below:
Wrapping Up
The DataForm adds a lot of value to applications and to the normally code-intensive and monotonous process of navigating, editing and viewing data. Its features are customizable to suit your needs, and it can be styled visually to blend in with your application’s design.
John Papa*(johnpapa.net) is a senior consultant and a baseball fan who spends summer nights rooting for the Yankees with his family. Papa, a Silverlight MVP, Silverlight Insider and INETA speaker, has authored several books, including his latest, “Data-Driven Services with Silverlight 2” (O’Reilly, 2009). He often speaks at conferences such as VSLive!, DevConnections and MIX.*