In my last post, I went over some cross-field validation scenarios and provided some sample code, including a CompareValidatorAttribute. We’ve now covered single-field validation and cross-field validation, but there’s yet another level of validation supported by RIA Services—Entity-Level validation. As you’ll see in this post, entity-level validation is very similar to what we’ve already seen.
Entity-Level Validation Declaration
Your entity types can declare entity-level validation using either the [CustomValidation] attribute approach where a custom validation method is supplied, or by having an attribute that derives from ValidationAttribute. Either way, to indicate that a validation rule applies at the entity-level (or type-level) as opposed to the property-level, you simply put the attribute on the class itself instead of on a specific property. See how simple this is:
[CustomValidation(typeof(MeetingValidators), "PreventExpensiveMeetings")]
public partial class Meeting
{
...
}
In this example, I’m using the [CustomValidation] attribute approach. Because I rarely find entity-level validation to be reusable across entities, I typically go that route. But if your business rules lead to scenarios where you do in fact have common entity-level validation, you might benefit from creating a derived validation attribute.
Here’s the implementation of my PreventExpensiveMeetings validation method.
/// <summary>
/// Ensure that long meetings don't include too many attendees.
/// </summary>
/// <param name="meeting">The meeting to validate.</param>
/// <returns>
/// A <see cref="ValidationResult"/> with an error or <see cref="ValidationResult.Success"/>.
/// </returns>
public static ValidationResult PreventExpensiveMeetings(Meeting meeting)
{
TimeSpan duration = meeting.End - meeting.Start;
int attendees = (meeting.MaximumAttendees + meeting.MinimumAttendees) / 2;
int cost = attendees * 50 * duration.Hours;
if (cost > 10000)
{
return new ValidationResult("Meetings cannot cost the company more than $10,000.");
}
return ValidationResult.Success;
}
There are some differences between this validation method and the ones we saw for single-field and cross-field validation:
- The value parameter is strongly-typed to our Meeting type;
- The ValidationContext parameter is not accepted as it doesn’t need to be used;
- The ValidationResult instance returned does not specify any member names;
If you were creating a reusable entity-level validator, you might find that you want to accept the ValidationContext parameter so that you can use the ValidationContext.ObjectType property, or quite possibly additional state information that can be provided by ValidationContext. Similarly, there’s no reason why you can’t have your method specify the MemberName(s) for the ValidationResult; this should be done if you have clear direction to give the user on what field(s) to change to correct the error.
Entity-Level Validation Triggers
It can be tricky to differentiate between cross-field validation and entity-level validation. In fact, the PreventExpensiveMeetings example is performing cross-field validation. It’s validating values across the Start, End, MinimumAttendees, and MaximumAttendees fields. Previously, we saw validation rules defined that validated the Start/End property pairs and the MinimumAttendees/MaximumAttendees properties. Each of those validators was applied to both properties and users got notification of errors as soon as one of the fields was put into conflict with the other. The PreventExpensiveMeetings scenario is different for a few reasons though, and these are what I look for when deciding whether to implement validation at the property-level or the entity-level.
- There’s no certain data entry path that will lead to the validation error. Because PreventExpensiveMeetings is based on 4 disjoint fields, it is unclear what property we’d want to declare the validation rule for. The model is unaware of what order the user will enter values in, so we cannot simply put the validation rule on the “last” field. We could apply the validator to all four properties, but this leads to a few issues.
- The value parameter could represent either a DateTime or an Integer, depending on which member is being validated. This will complicate the validation code.
- Because property validation occurs before the value is set on the property, you must use the value parameter conditionally, based on ValidationContext.MemberName, further complicating the code.
- The validation attribute would be repeated on your model, which causes a headache and can lead to maintenance mistakes.
- There’s no clear user guidance on how to correct the error. If users hit this error, would you advise them to shorten the meeting, or decrease the number of attendees? Furthermore, would you suggest changing the Start time or the End time; or the Minimum Attendees, or the Maximum Attendees? Sometimes, only the end user knows how to correct complex errors.
- Property-Level validation could lead to “noise” for the end user, while the properties are being edited. With validation rules that involve many fields, it becomes quite probable that users will hit situations where changes temporarily violate validation rules. If these rules are executed too aggressively, users will become frustrated by errors they know will go away as soon as they finish making their changes. Just think about how annoyed you get when Visual Studio tells you about build errors while you’re refactoring something.
- The validation rule relies upon property-level validation being successful. If you look closely at the validation logic for PreventExpensiveMeetings, you’ll realize that a meeting that lasts -6 hours and has between 10 and -200 attendees would cost the company $28,500 and display a validation error. This of course isn’t right. Before invoking this validation rule, we should ensure that the meeting doesn’t result in time travel and that the min/max attendees range is valid.
In the Validation Triggers article, we learned that entity-level validation occurs in a few places:
- Ending Edit Sessions - Entity.EndEdit()
- Submitting Changes - DomainContext.SubmitChanges()
- Custom Entity Update Methods
For each of those triggers, property-level validation is performed first, and only if all properties are valid are entity-level validators invoked. In light of our validation rule relying upon property-level validation being successful, it now makes a lot more sense why this short circuit is in place. Here’s a reminder of the validation stages:
- Execute all [Required] validators on the entity’s properties. If any required properties are missing values, validation fails and stops (but multiple required-field validation errors can occur).
- Execute the remainder of the property-level validators. If any property is found to be invalid, validation fails and stops (but multiple property validation errors can occur).
- Execute all entity-level validators. If any entity-level validator is found to be invalid, validation fails and stops (but multiple validation errors can occur).
- (Server Only) Check the entity for an IValidatableObject implementation; if implemented, execute the Validate() method. If any validation errors are returned, validation fails.
Displaying Entity-Level Validation Errors
Also mentioned in the Validation Triggers post, DataGrid and DataForm have deep support for validation; this includes entity-level validation. Both of these controls are able to display entity-level validation right out of the box. Here’s what you will see when an entity-level validation error occurs in each of these controls.
If you are not using the DataGrid or DataForm, there’s a trick that can be applied to display a single entity-level validation error when one occurs. This is to have a control on your page that has a binding to the entity itself, and not to a property on the entity. For instance, the following XAML would lead to having a TextBox that will show the first entity-level validation error that occurs on the Meeting that is set to be the DataContext of the Grid.
<Grid>
<Grid.DataContext>
<my:Meeting />
</Grid.DataContext>
<TextBox Text="Schedule a Meeting"
DataContext="{Binding}"
BorderThickness="0"
IsReadOnly="True"
HorizontalAlignment="Left" />
</Grid>
The key is having something on the TextBox bound to the Meeting instance using a {Binding} without a Path. This binding can exist on any property, not just Text; in this case, I used the DataContext property. Of course, this XAML is contrived and won’t actually set up the ability to edit a meeting. But using this TextBox declaration in a different context, my page shows the following when I end editing on the meeting:
I am by no means a designer though (can you tell?), so I will stop short of advising you on how best to show your entity-level errors to your end users. If you are interested, Regis Brid’s whitepaper on INotifyDataErrorInfo includes a reusable ValidationErrorViewer control that can be used to show a list of entity-level validation errors, using a trick similar to the {Binding} markup shown above.
RIA Services Validation Recap
We can now see that some occurrences of cross-field validation should really be broken out to the entity-level stage of validation. This allows the validation rule to execute on the entity in a state where all property values are known to be valid, and the logic can be implemented very easily. Declaring the entity-level validation is simple too: just apply a [CustomValidation] attribute to the entity’s class rather than on a property. DataGrid and DataForm will both display entity-level validation errors by default, and you can use a {Binding} trick to get some primitive controls to display an entity-level validation error too.
This article is part of an in-depth series on RIA Services Validation. Here’s the full series:
[9/6/2011] The source code for everything shown during the series is available on GitHub.
Digging Deeper
We have already covered a great deal, but there are plenty of validation topics left to cover. Still to come, we’ll be exploring the power of ValidationContext and I’ll also provide a validator factory implementation that can consume validation rules from other types so that validation rules from entities can be inherited into a ViewModel!