Z Code
Build Data-Driven Client Validation
Here's a design for creating data-driven validation rules for distributed apps. It lets you distribute the rules from to clients without modifying client-side application code.
Build Data-Driven Client Validation
Create data-driven validation rules for distributed apps, so you can distribute the rules to clients without modifying client-side application code.
by Tony Surma and Bill Wagner
Posted April 9, 2004
Technology Toolbox: C#, XML
Data can come from diverse sourcesmany .NET applications involve smart clients, Web services, and a database engine for storage. You also might use a DataSet to transfer collections of data from the Web service to the client application, and a DataGrid to display these elements to users. Despite this complexity, or perhaps because of it, you need to provide users with validating feedback when they edit collections of data.
The validations can be as simple as requiring a field to be present, or as complex as a calculation involving multiple columns and tables. You can enforce simple validation rules easily by adding constraints to the DataSet. However, you must write code to implement complicated business rules, and you'll have to duplicate this code on both the server and client sides of your application. Worse yet, these business rules change as new practices are put into place.
We'll show you a design that enables you to create data-driven validation rules for your distributed applications. You can distribute the data driving the business rules from server to client without modifying the application's code. As business rules change or get extended, you need to modify only the data stored on the server. None of the clients need any code modifications.
A variety of methods are available for validating user input. We created a set of classes that integrate with the DataSet class and set error information on the row or column involved whenever users enter invalid information. We wanted the validation class to be completely data-driven, with the specific rules encoded in data members that you can serialize from the server to client, or even store persistently. The specific rule loads the data at run time and attaches event handlers that respond to data-change events in the DataSet. The validator runs its rules and sets or clears the proper error information when users change particular data elements.
Of course, one class can't do all that work; too many different kinds of validation rules are possible. So we wrote a hierarchy of validators that perform different types of validation. The base class attaches event handlers and stores the table and column names along with custom error messages. Each derived class is distinguished by the code used to perform the validation. For example, one class models the WebForms' RangeValidator control, and another uses the DataTable's Compute function to execute a formula on a set of rows and a given column. Both classes use logic you can serialize and modify at run time.
We used Microsoft's sample Pubs database to create a sample application that demonstrates this functionality (see Figure 1). The sample loads a list of titles, a list of authors for each title, and the royalty schedule for the titles. You can edit any of the fields, though we didn't add code that would enable the sample app to save changes back to the database.
Add Custom Validators
The sample app contains custom validators for two of the fields. One rule enforces a range validator for the total royalty percentage for a title. It decrees that authors must receive between 5 and 40 percent of the book price as royalty. The second rule is more complex: When multiple authors write a book, total royalties are split between them, and the split percentages must add up to 100 percent of the total royalty.
The sample classes work in a distributed application, but you can download one sample that works in a more contained environment. This sample connects to the local Pubs database directly to get data. All the data access code makes use of the Data Access Application Block (see Additional Resources). The code in the DAAB handles all the database calls, retrieving data and storing it in a DataSet. You need to modify the Connection string to run the application on your system. The Connection string is stored in the RoyaltyData.cs file as a constant.
Write the validation rules starting with the simplest, the range validator. This validator needs to read a column value as it changes and report any errors. The logic is simple: Attach a method to the ColumnChanged event on the roysched table. This event handler examines the column name. If it's the royalty column, the event handler ensures that the value is between five and 40 (see Listing 1).
The base validator is an abstract class tasked with storing the table name, along with the name of the column in the table for a particular validator. The base validator also stores the error message that displays whenever the column contains an error. We implemented two more methods in this abstract class: ConnectTo() and Validate(). ConnectTo() connects the ColumnChanged event for the table to the event handler defined in the class. The event handler examines the column name for the changed value and calls the abstract Validate() method if the changed value is for the column defined for this validator. All derived classes must implement the Validate() method to process the business rules on the changed value.
You can use this validation framework as a base for adding custom validators and making the task as simple as implementing a derived class. The key to creating these custom validators is to make them as data-driven as possible. You should install the validators with your client application once, then let them modify their behavior at run time whenever the business rules change. The range validator needs to compare the proposed value for any change with the stored minimum and maximum values (see Listing 2).
The RangeValidator class is a simple first validatorthe Validate() method compares the new value to the stored minimum and maximum. The error is set if the new value falls outside the range (see Figure 2). The constructor sets the minimum and maximum values for the comparison.
Tackle Trickier Validation
Knowing how to implement the simple concept of a range validator should help you deal with harder validators, such as one that ensures a shared percentage royalty adds up to 100 percent of total royalties. Most applications enforce this kind of rule on the server side only. More complicated business rules often morph after an application is deployed. You discover new constraints, or other factors change the rules. Server-side enforcement simplifies updating your validators. This complicates things for users, though, because they don't get feedback until the smart client sends data back to the server for validation.
We created a validator that introduces new features and shows you how to extend its design for a variety of business rules. Many business rules are enforced at the data tier with some form of aggregate function. This sample app's aggregate function is SUM (royaltyper). The Compute method and the DataColumn's Expression property follow the same syntax and usage as the T-SQL aggregate functions.
You can use any of the aggregate functions to validate a column in the data set. More importantly, you can make the class completely data-driven because the aggregate expression and the filter are strings. This validator is somewhat more complicated than the other validator we've described. It has a few more data members, and the validate expression is more complex (see Listing 3).
The logic for the expression validator simply evaluates the functionusing any supplied filter expressionand compares it to the expected value. The test passes and all errors are cleared if the values match. The error message is set if the test fails. Setting the error message is less simple. In this instance, an error doesn't really apply to one row. Rather, it applies to all the rows matching the current filter. The validator supports that concept by setting and clearing the errors from all rows that match the search criteria (see Figure 3).
You hook up this validator exactly the same as with the range validator. It attaches itself to the ColumnChanged event and tracks changes. This validator can't use the ColumnChanging event; it must use the ColumnChanged event. The former would still have the old value stored in the table, and the Compute function would use the old version rather than the new.
You know how to build the validators now, so it's time to serialize the data and hook up the validators. A real enterprise application would probably require modifying these validators to be transmitted from server to client (see Table 1). I'll keep it a little easier here by serializing the validators to an XML file and restoring them from that same file, rather than serialize them between server and client. You create the validators by constructing them, setting the validation parameters in the constructor of this C# code segment:
Validator r = new RangeValidator
("roysched", "royalty",
"Royalty must be between 5 and 40",
5, 40);
_validators.Add (r);
// Add the compute value:
r = new ComputeValidator("authors",
"royaltyper", "Royalties must sum to
100",
"Sum (royaltyper)", "title_id",
(Int64)100, true);
_validators.Add (r);
Save the validators by writing them to a file:
// Quick demo, save the validators:
Stream stream = File.Open("data.xml",
FileMode.Create);
SoapFormatter formatter = new
SoapFormatter();
formatter.Serialize(stream,
_validators);
stream.Close();
Subsequent executions take less effortyou read the data file, and the validators perform the rules stored in the file:
Stream stream = File.Open ("data.xml",
FileMode.Open);
SoapFormatter formatter = new
SoapFormatter ();
_validators = (ArrayList)
formatter.Deserialize (stream);
Now the validators have been created, either directly or by loading them from the file. All you need to do is connect them to the DataSet. We named our DataSet _data; a foreach loop connects the validators to the DataSet:
foreach (Validator v in _validators)
v.ConnectTo (_data);
You can modify the XML file to change the conditions, formulas, or the range on the range validator, and get different business rules without any code changes.
We simplified our sample to make it easier for you to download and use, but you can make several modifications in real-world, smart-client applications. First, you'll need some better error detection and handling. We wanted to show clearly the algorithms these classes use, so we didn't add the necessary error detection for errors outside the scope of the discussion, such as incorrect table names and column names. These problems would throw exceptions. Secondly, you need to enable these classes to send themselves from server to client. You can start with the XML serialization in the sample. Lastly, you'll want to experiment and add validators of your own. There are many ways to determine when input is valid or not, and you can add as many as you need. Just make sure they're data-driven and can change as your requirements change.
About the Authors
Tony Surma assists enterprise customers in envisioning, designing, and providing large-scale, high-volume architectures.
Bill Wagner, a commercial developer, cofounded SRT Solutions Inc. He facilitates .NET adoption, especially the core framework, C#, smart clients, and service-oriented architecture and design. He's Microsoft's regional director for Michigan, and he wrote C# Core Language Little Black Book (The Coriolis Group). He's now writing Effective C#. Reach him at [email protected].