Validation

16 187 0
Tài liệu đã được kiểm tra trùng lặp
Validation

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

C H A P T E R 6 ■ ■ ■ 129 Validation Data integrity is a very important part of developing a software product. Users of software are human beings and, being such, are universally fallible. Whenever requiring input from a user—especially free- text input—the values that are supplied should be treated with extreme suspicion. When it comes to user input, data lives in a totalitarian state: it is all guilty until proven innocent and must be vigorously validated before being trusted. ■ Note Much of this chapter is aimed at WPF. However, the “Validation In Silverlight” section deals with Silverlight directly, indicating which parts are common and what can be leveraged specifically in Silverlight. With the release of Silverlight 4, validation is much improved and the gap between the two technologies is ever decreasing. The best way to eliminate bad data is with the preventative measure of data validation. Before the data is allowed to burrow deep into the model layer, it needs to be tested for correctness. Validation cannot unequivocally say that a piece of data is exactly the right piece of data—such bold claims are the remit of data verification. Validation simply informs whether data appears good enough on a more superficial level, answering questions such as: • Is it of the correct data type? • Is it within a certain range or set of values? • Does it contain only characters that are acceptable? • Is it of a specific length? Even such seemingly shallow tests can directly prevent problems that can be introduced by validation errors. Imagine that an application wishes to provide a powerful reporting feature whereby users with SQL knowledge can construct queries directly on the data source. Without validating the free- text input received from the user, the nefarious ne’er-do-well user could enter the command in Listing 6–1 with disastrous effects. Listing 6–1. A SQL Injection Attack DROP TABLE Products; This type of insecurity is so common that it has its own term: the SQL Injection Attack. One way of solving the problem would be to disallow the user from entering plain SQL commands at all, but validation can also be used to ensure that the entered command is harmless. CHAPTER 6 ■ VALIDATION 130 ■ Note I once worked for a multinational corporation on a project for their client that they billed into the hundreds of millions of dollars. However, despite this vast expense, they not only opened themselves up to a SQL Injection Attack by running plain, user-input SQL directly on the database, the interface for the command was a web service. If they had gone live with that set up, it would not have been long before their database was irreparably destroyed. The Validation Process A binding’s validation rules are fired when the source of the binding is updated. As explained in Chapter 2, the catalyst for updating a binding source can be set using the UpdateSourceTrigger property. Once the update has begun, validation progresses as outlined below. If a validation error occurs at any point during the process, the process stops. 1. The binding engine collates all ValidationRule objects whose ValidationStep value is RawProposedValue, calling their Validate method and halting if there is an error or continuing if all rules pass. 2. At this point, any data converters attached to the Binding are executed. The converter may fail at this point, stopping the validation process. 3. The binding engine then collates all attached ValidationRules whose ValidationStep value is set to ConvertedProposedValue, calling the Validate method and halting on an error. 4. At this point, the binding engine sets the binding source value. 5. The binding engine collates all the attached ValidationRules whose ValidationStep value is set to UpdatedValue, calling their Validate method and halting on an error. Any DataErrorValidationRules attached with a default ValidationStep value (which is also UpdatedValue) are collated and tested for validity at this point, too. 6. The final step is to call the Validate method on ValidationRules whose ValidationStep value is set to CommittedValue, and halt on a validation error. Clearly, there are many ways in which the validation process can be customized. Mainly, allowances are made for testing the binding source value at a variety of stages during the binding process, with the aid of the ValidationStep value. A more thorough explanation of the participant classes involved in this process follows below. Should a ValidationRule fail at any point during this process, the binding engine constructs a ValidationError instance and adds it to the Validation.Errors collection of the target Control. If, at the end of the process, this Validation.Errors collection contains at least one error, the Validation.HasError property is set to true. Similarly, if the NotifyOnValidationError property is set to true, the Validation.Error event is raised on the target Control. CHAPTER 6 ■ VALIDATION 131 Binding Validation Rules Binding objects have a ValidationRules property that accepts a list of ValidationRule objects. As explained previously, these are iterated over to determine whether or not an input value is valid. There are two implementations of the ValidationRule class supplied, one for handling exceptions and another for hooking in to the IDataErrorInfo provision of validation. Of course, if neither of these facilities suffice, custom ValidationRule implementations can be created to suit specific situations. Listing 6–2 shows the definition of a binding that contains a single ExceptionValidationRule. Note that the binding is defined in long-form, because the ValidationRules value is a list. Listing 6–2. Adding an Exception Handling Validation Rule to a Binding <TextBox> <TextBox.Text> <Binding> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ValidationRule Class The ValidationRule class provides an abstract base class for the implementation of validation rules that can be applied to individual bindings. Although two implementations are provided, it may be necessary to define custom implementations that correspond to specific validation rules. For example, if the user is presented with a dialog for creating a new customer record, there may be a validation rule that disallows certain characters in the person’s name, enforces a certain minimum or maximum age limit, or ensures the validity of a zip or postal code. Validate Method The ValidationRule class specifies a single abstract method that must be overridden by all subclasses (see Listing 6–3). It is supplied with the value of object type that requires validation and a CultureInfo object in case the validation rule requires knowledge of the current culture. Listing 6–3. The ValidationRule Method Signature public abstract ValidationResult Validate(object value, CultureInfo cultureInfo) The return value is of type ValidationResult. It is a class created for the sole purpose of allowing validation methods to return two different values: a Boolean signifying the value’s validity and an object that provides data about the error generated when the validation result is false. The ValidationResult constructor typically accepts a 'true, null' pairing if the validation has succeeded, or a 'false, "explanation" ' pairing if the validation has failed, where “explanation” is a string of text informing the user what went wrong. Of course, a string explanation is not exclusively required; any sort of error data could be provided. Listing 6–4 shows an example implementation of the Validate method. It ensures that the supplied input value is a string that can be parsed into an integer value. Note that the CHAPTER 6 ■ VALIDATION 132 ValidationResult.ValidResult static property is used as a shortcut for new ValidationResult(true, null). Listing 6–4. A Trivial Implementation of a Validation Rule public override ValidationResult Validate(object value, CultureInfo cultureInfo) { ValidationResult result = new ValidationResult(false, "An unknown validation error occurred"); if(value is string) { int integerValue = int.MinValue; if(int.TryParse(value as string, out integerValue)) { result = ValidationResult.ValidResult; } else { result = new ValidationResult(false, "The string provided could not be parsed as an integer value"); } } else { result = new ValidationResult(false, "The value provided is not a string"); } return result; } ■ Tip The example in Listing 6–4 uses the Single Exit Pattern, which can often result in more understandable code. The theory is that all methods should only contain a single return statement and it should be located right at the end. More often than not, this is easily achievable and code is consequently more comprehensible, but it is, as ever, a personal preference. Constructors The ValidationRule has two constructors. The default constructor is most commonly used and simply initializes the class to use default values for the two properties that affect the behavior of this rule. These are the ValidatesOnTargetUpdated and ValidationStep properties, which govern whether the validation rule runs when the target of the associated Binding is updated and in which part of the validation process the rule runs, respectively. Listing 6–5 shows an example. Listing 6–5. Default ValidationRule Constructor public ValidationRule() { ValidationStep = ValidationStep.RawProposedValue; ValidatesOnTargetUpdated = false; } CHAPTER 6 ■ VALIDATION 133 An alternative constructor is provided to supply these two values on creation (see Listing 6–6). Listing 6–6. Alternative ValidationRule Constructor public ValidationRule(ValidationStep validationStep, bool validatesOnTargetUpdated) The default constructor is more likely to be used in binding situations, as the validation rule is declared in XAML. As the validation rule is constructed, the two properties can be set via the usual methods, as shown in Listing 6–7. Listing 6–7. Constructing a ValidationRule and Setting Its Behavioral Properties <TextBox> <TextBox.Text> <Binding> <Binding.ValidationRules> <local:MyValidationRule ValidationStep="UpdatedValue" ValidatesOnTargetUpdated="True" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> Exceptions for Validation When the target control of a binding is updated, an exception is sometimes thrown somewhere in the process of setting the new value on the binding source. A common example is when the value entered does not match the value required by the binding source. Whenever an exception is thrown in this situation, it can be caught and handled as a validation error. The ExceptionValidationRule class is provided for this purpose and can be added to a binding’s ValidationRules list, as shown in Listing 6–8. Listing 6–8. Constructing a ValidationRule and Setting Its Behavioral Properties <TextBox> <TextBox.Text> <Binding> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> If the user entered a string value that could not be automatically parsed into an integer, yet the binding source required an integer value, an exception would be thrown. Normally, this would present your user with an unhelpful message that would likely cause confusion or irritation. By presenting the exception as a broken validation rule, the user will readily understand the problem and can work to rectify the error without ruining their experience. Filtering Validation Exceptions There may be a situation whereby certain specific exceptions should be handled in different ways. Perhaps an InvalidCastException would prompt a different response than a DivideByZeroException. CHAPTER 6 ■ VALIDATION 134 Both should be caught, but the user should be presented with a different response for each. Listing 6–9 shows how to add an UpdateSourceExceptionFilter to a Binding, which will then call the specified UpdateSourceExceptionFilterCallback method in the code behind. Listing 6–9. Filtering Exceptions that Occur When Updating a Binding Source <TextBox> <TextBox.Text> <Binding UpdateSourceExceptionFilter="MyExceptionFilter"> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> Listing 6–10 shows the method signature required for such callbacks. Listing 6–10. The UpdateSourceExceptionFilterCallback Delegate public delegate object UpdateSourceExceptionFilterCallback(object bindExpression, Exception exception) The delegate returns an object instance that has a different intention depending on the returned value, as shown in Table 6–1. Table 6–1. The Possible Exception Filter Return Values and Their Respective Intent Return Value Intent Null Creates a ValidationError using the exception as the ErrorContent property and adds the error to the bound control’s Validation.Errors collection. Any object Creates a ValidationError using the returned object as the ErrorContent property and adds the error to the bound control’s Validation.Errors collection. A ValidationError instance For fine-grained control of how the ValidationError is constructed (note that it is automatically constructed in the prior examples), merely add this ValidationError to the bound control’s Validation.Errors collection. The Exception object that is passed into the filter callback can then be used to determine the specific subclass thrown and the response varies on that, as shown in Listing 6–11. Listing 6–11. Testing the Thrown Exception’s Type so as to Respond Differently public object MyExceptionFilter(object bindExpression, Exception exception) { if (exception is InvalidCastException) { // Respond to an invalid cast CHAPTER 6 ■ VALIDATION 135 } else if (exception is DivideByZeroException) { // Respond to a division by zero } } Note that this property is set on the Binding, not the ExceptionValidationRule, but it is specific to validation scenarios, so the ExceptionValidationRule must be present in the Binding’s ValidationRules list. Exceptions Represent Exceptional Circumstances There is a problem with using exceptions for validation: the term exception implies that it represents an exceptional circumstance. Conveying the contravention of arbitrary, business-oriented validation rules is not the intent of an exception. In fact, one major point of input validation is to avoid throwing an exception. A Validation Framework So far, I have looked at implementing custom ValidationRule classes and leveraging the usable, if flawed, ExceptionValidationRule. However, what is needed is a more robust validation framework, whereby the rules used for validation are in close proximity to the rest of the business logic. After all, validation rules are business rules. The .NET Framework provides an interface that is used to supply user interfaces with custom error information. Windows Forms and ASP.NET MVC both make use of the IDataErrorInfo interface to present validation error messages that users can comprehend. WPF and Silverlight allow the adaptation of this interface for use as Binding ValidationRules with the use of the DataErrorValidationRule subclass. If your project involves porting an existing Windows Forms application to WPF and the original code makes use of IDataErrorInfo, then it is advisable to reuse this code and hook the validation code to the user interface using DataErrorValidationRule. IDataErrorInfo Interface This interface has two properties, as shown in Listing 6–12. Listing 6–12. The IDataErrorInfo Interface Definition public interface IDataErrorInfo { string Error { get; } string this[string columnName] { get; } } There are two getter properties that comprise this interface. The Error property returns a string explaining—in natural language for the user interface to present to the user—what is wrong with the object’s current state. CHAPTER 6 ■ VALIDATION 136 The other property is an indexer that requires a property name as an argument. The property name matches a property on the implementing object and the returned string value explains what is wrong with this specific property. So, the object itself can be in error and have its own error message. Individual properties can also be in error and have individual explanation strings as to what is wrong with them. The default return value for both is the empty string ( string.Empty ). In an MVVM application, there are two possible places where you may wish to implement the IDataErrorInfo interface. Both have their merits, but the key is to be consistent: choose which layer will house the validation code, reconcile why this is the most applicable, and stick to it. Model Implementation One possible location to implement the IDataErrorInfo is in the model implementation. That is, along with the rest of the problem solution. This makes sense because validation is business logic and the two are interlinked. Listing 6–13 shows an example of implementing the IDataErrorInfo on a Customer class that disallows an Age value of less than 21. Listing 6–13. Implementing the IDataErrorInfo in a Model Class public class Customer : IDataErrorInfo { public Customer(string name, int age) { Name = name; Age = age; } public string Name { get { return _name; } set { _name = value; } } public int Age { get { return _age; } set { _age = value; } } public string Error { get { throw new NotImplementedException(); } } public string this[string columnName] { get { string error = string.Empty; switch (columnName) { case "Name": if (_name.IndexOfAny(IllegalCharacters) > 0) CHAPTER 6 ■ VALIDATION 137 { error = string.Format("The customer's name contains illegal characters ({0})", new string(IllegalCharacters)); } break; case "Age": if (_age < 21) { error = "Customers must be 21 or over to shop here!"; } break; } return error; } } private string _name; private int _age; private static readonly char[] IllegalCharacters = new char[] {'_', '!', '@', '%'}; } There is a problem with this approach: the strings being returned are for user interface consumption and the model classes should be ignorant of their use in a user interface. Furthermore, domain modeling purists would argue that the interface implementation requirement is too heavy a burden on the model and contravenes the Single Responsibility Principle. They would argue that model classes should be POCOs and should have no extraneous constraints. This is theoretically correct but, as ever, there is a time when purity becomes pedantry. As long as all stakeholders are aware of the potential risks or issues, the expediency of implementing the validation code in the model may well make it the right solution. ViewModel Implementation The alternative to implementing the IDataErrorInfo in the model is implementing it in the ViewModel classes, as shown in Listing 6–14. Listing 6–14. Implementing the IDataErrorInfo in a ViewModel Class public class CustomerViewModel : IDataErrorInfo { public string Name { get; set; } public int Age { get; set; } CHAPTER 6 ■ VALIDATION 138 public string Error { get { throw new NotImplementedException(); } } public string this[string columnName] { get { string error = string.Empty; switch (columnName) { case "Name": if (Name.IndexOfAny(IllegalCharacters) > 0) { error = string.Format("The customer's name contains illegal characters ({0})", new string(IllegalCharacters)); } break; case "Age": if (Age < 21) { error = "Customers must be 21 or over to shop here!"; } break; } return error; } } public void CreateCustomer() { _customer = new Customer(Name, Age); } private Customer _customer; private static readonly char[] IllegalCharacters = new char[] { '_', '!', '@', '%' }; } This makes sense for many practical reasons unrelated to an abstract notion of “code purity.” Firstly, the intent of the ViewModel is to bridge the gap between the business logic that solves a programming problem and the user interface that interacts with the user. The IDataErrorInfo also mediates between the user interface and the domain layer by explaining errors to the user in natural language. The interface does not provide any contract for implementing validation rules; it provides a contract for reporting validation rules that have already been broken. Notice, also, that the ViewModel implementation maintains the encapsulation of the Customer business object: the Customer’s Name and Age properties can be immutable and the CreateCustomer method is only called when the data has been validated. [...]...CHAPTER 6 ■ VALIDATION DataErrorValidationRule Class Once the IDataErrorInfo interface has been implemented, the DataErrorValidationRule class is the adapter between a ValidationRule and the IDataErrorInfo.Error property See Listing 6–15 for an example Listing 6–15 Enabling a Binding to Check for IDataErrorInfo Errors 143 CHAPTER 6 ■ VALIDATION This style definition will apply... property (see Listing 6–18) Listing 6–18 Enable Support for Server-Side Validation A sample implementation of the INotifyDataErrorInfo interface, using a ViewModel class, is provided in Listing 6–19 140 CHAPTER 6 ■ VALIDATION Listing 6–19 Implementing Server-Side Validation in the ViewModel Layer of a Silverlight Application public . method on ValidationRules whose ValidationStep value is set to CommittedValue, and halt on a validation error. Clearly, there are many ways in which the validation. Control. CHAPTER 6 ■ VALIDATION 131 Binding Validation Rules Binding objects have a ValidationRules property that accepts a list of ValidationRule objects.

Ngày đăng: 03/10/2013, 01:20

Tài liệu cùng người dùng

Tài liệu liên quan