- Throw exceptions from within properties of the data bound object.
- Use the IDataErrorInfo interface on data bound objects.
- Use Custom validation rules - subclass of System.Windows.Controls.ValidationRule.
The validation processing happens in the above order, with the exception of custom validation rules which have a ValidationStep property which controls the stage at which they are applied. The reality is probably a combination of these will be the way to go. Ideally throwing exceptions in properties is not a great idea and sometimes a given data object does not implement the IDataErrorInfo interface and cannot be partialed.
My preferred combination is use IDataErrorInfo where you can, this pushes some validation back to the business object so it cannot be avoided.In some situations its better to encapsulate the validation rule into a ValidationRule subclass so it can be shared among other like types.
Here's a demo of the three validation methods:
Of course the red border and asterisk is customisable by altering the style/template.
The first test box is validated using IDataErrorInfo interface. This is implemented by the source object the textbox is binding to.
The Model name text box is validated using exceptions. If an exception is thrown while trying to set the bound property the ExceptionValidationRule will show the red border.
The Unit Cost field is validated using a custom validation rule that derives from ValidationRule.
Here's what the Xaml looks like for the 3 text-boxes:
<TextBox Grid.Column="1" Margin="10" Text="{Binding Path=ModelNumber, NotifyOnValidationError=True, ValidatesOnDataErrors=True}" /> ...
<TextBox Grid.Column="1" Grid.Row="1" Margin="10" Text="{Binding Path=ModelName, NotifyOnValidationError=True, ValidatesOnExceptions=True}" /> ...
<TextBox Grid.Column="1" Grid.Row="2" Margin="10"> <TextBox.Text> <Binding NotifyOnValidationError="true" Path="UnitCost"> <Binding.Converter> <local:PriceConverter /> </Binding.Converter> <Binding.ValidationRules> <local:PositivePriceRule IsNullAndEmptyAllowed="False" Max="99999.99" Min="1000.00" /> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
For Exception Validation and for IDataErrorInfo there is a convenient short-hand syntax that can be added to the binding mark-up. But for custom validation rules you must use the long-hand syntax for binding so you can reference your custom validation rule (in this example the PositivePriceRule).
When using the IDataErrorInfo interface you must implement two members: a string property "Error" and a default indexer property that takes a string indexer and returns a string. This idea is WPF will call this interface passing in the bound property name and if an instance of a string is returned then an error condition results; which then shows the red border style. Here's the relevant code:
/// <summary> /// Gets an error message indicating what is wrong with this object. /// Not used by WPF. /// </summary> /// <returns> /// An error message indicating what is wrong with this object. The default is an empty string (""). /// </returns> public string Error { get { return null; } } /// <summary> /// Gets the error message for the property with the given name. /// </summary> /// <returns> /// The error message for the property. The default is an empty string (""). /// </returns> /// <param name="columnName">The name of the property whose error message to get. </param> public string this[string columnName] { get { return this.Validate(columnName); } } private string Validate(string propertyName) { switch (propertyName) { case "ModelNumber": bool invalid = this.ModelNumber.ToCharArray().Any(c => !char.IsLetterOrDigit(c)); return invalid ? "The Model Number can only contain letters or digits." : null; default: // return valid by default return null; } }
WPF does not use the Error property, it will only call the default indexer.
Throwing exceptions from within properties doesn't really need further explanation, just works as expected.
Implementing a custom validation rule is pretty straight forward also. The only requirement is to override the abstract function "Validate()". Here's the relevant code from my PositivePriceRule class:
public class PositivePriceRule : ValidationRule {
...
/// <summary> /// When overridden in a derived class, performs validation checks on a value. /// </summary> /// <param name="value">The value from the binding target to check.</param> /// <param name="cultureInfo">The culture to use in this rule.</param> /// <returns> /// A <see cref="T:System.Windows.Controls.ValidationResult"/> object. /// </returns> public override ValidationResult Validate(object value, CultureInfo cultureInfo) { decimal price = 0; var stringValue = value as string; if (string.IsNullOrEmpty(stringValue)) { return this.IsNullAndEmptyAllowed ? new ValidationResult(true, null) : new ValidationResult(false, "Null and/or Empty not allowed."); } try { if (stringValue.Length > 0) { // Allow number styles with currency symbols like $. price = decimal.Parse(stringValue, NumberStyles.Any); } } catch (Exception) { return new ValidationResult(false, "Illegal characters."); } if ((price < this.Min) || (price > this.Max)) { return new ValidationResult(false, "Not in the range " + this.Min + " to " + this.Max + "."); } return new ValidationResult(true, null); }
}
There's nothing to stop you from using a combination of all three techniques. Most commonly using ValidatesOnExceptions and ValidatesOnDataErrors together as follows:
<TextBox Grid.Column="1" Margin="10" Text="{Binding Path=ModelNumber, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}" />
Or using the long-hand syntax combining all three techniques:
<TextBox Grid.Column="1" Grid.Row="2" Margin="10"> <TextBox.Text> <Binding NotifyOnValidationError="true" Path="UnitCost"> <Binding.Converter> <local:PriceConverter /> </Binding.Converter> <Binding.ValidationRules> <local:PositivePriceRule IsNullAndEmptyAllowed="False" Max="99999.99" Min="1000.00" /> <ExceptionValidationRule /> <DataErrorValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
In summary, it seems to me to prefer using IDataErrorInfo and ValidatesOnDataErrors where ever possible pushing the validation logic into the bound business objects. Where exceptions are possible use ValidatesOnExceptions. And if there is a strong reason to, create a custom rule to keep the validation in the presentation layer of your application. Sometimes this makes sense when there are more rules you want to impose than those provided by the bound data object.
Here's a great article from MSDN magazine covering more advanced validation options.
This covers validation binding groups which is handy to control when validation occurs for a group of controls and bindings. (If a control's value is dependent on a valid value in another control).
No comments:
Post a Comment