![]() |
Languages »
C# »
Delegates and Events
Intermediate
Delegates and Business ObjectsBy Paul StovellAn approach to implementing validation on custom business rules, using delegates. |
C#, Windows, .NET, ASP.NET, Visual-Studio, WinForms, WebForms, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

The aim of this article is to give you an understanding of how I perform business object validation in Trial Balance, a personal accounting project of mine. This article was originally a blog post that featured on my website, which you can see here. My approach borrows largely from the technique published in Rocky Lhotka's Expert Business Objects book (don't worry if you haven't read it), but I've added a Stovellian twist to spice things up. I'm also going to detour into a discussion of IDataErrorInfo, a really neat interface provided as part of the .NET framework to make our lives easier.
Let's take a very simple example of what I mean when I say business rule:
Account names can't be blank, and can't be longer than 20 characters.
It's not uncommon to see a rule like that implemented like this:
public string Name
{
get { return _name; }
set
{
if (value == null
|| value.Trim().Length == 0
|| value.Trim().Length > 20)
{
throw new ArgumentException("Account names can't be" +
" blank, and can't be longer than 20 characters.");
}
_name = value;
}
}
Whilst this will stop anyone assigning the name of your account to "Supercallafragelisticexpialadocious", I feel it has a few drawbacks:
try/catch blocks to show these errors to the user. Trying to show errors on the form as the user is filling it out becomes very difficult.
try/catch blocks. Rocky's book approaches the idea of validation in a different manner. In Rocky's CSLA framework, a business object won't usually throw exceptions if you set a property to an invalid value, but instead the object will mark itself as invalid. If you try to attempt to save the business object, then an exception might be thrown.
There are two fundamental principles underlying this:
When I was implementing rules for Trial Balance, I loved Rocky's idea, but there were a couple of things that concerned me:
StartDate and an EndDate. Your EndDate business rule would probably say that the EndDate must be greater than your StartDate, right?
What if the following happens:
StartDate = DateTime.MaxValue;
EndDate = DateTime.Now; // EndDate is marked as invalid
StartDate = DateTime.MinValue;
So, although now EndDate is technically valid, the property setter hasn't been called, and thus EndDate is still marked as invalid. You can get around this with a little extra code, but it's not something I'd want to think about much personally.
This led me to the following design goals:
A typical Trial Balance property will look like this:
public string Name {
get { return _name; }
set { _name = value; }
}
Notice how there are no rules in there?
If you take a look at the Account class and scroll about halfway down, you'll see a method called CreateRules that looks somewhat like this:
protected override List<Rule> CreateRules() {
List<Rule> rules = base.CreateRules();
rules.Add(new SimpleRule("Name", "An account name is required" +
" and cannot be left blank.",
delegate { return this.Name.Length != 0; }));
rules.Add(new SimpleRule("Name", "Account names cannot be" +
" more than 20 characters in length.",
delegate { return this.Name.Length <= 20; }));
return rules;
}
For each rule that applies to an object, I'm using delegates to validate the rule. Since these are generally short validation routines, they can usually be done in just one line of code.
Before I dive into the details about how I validate a rule, I'd like to talk about a very important interface you should know about if you're a .NET developer. It's called IDataErrorInfo, and it's in the System.ComponentModel namespace.
IDataErrorInfo is used to inform people of errors on your object. It's got two parts � a string property called Error, where you return a list of all the errors on the object (for example: "Name can't be blank. Email can't be blank. Address can't be blank..."), and an indexer that takes the name of a property and returns an associated error message (for example, account["Name"] might return "Name can't be blank").
In .NET, if a control like a DataGridView is data-bound to an object, and that object implements IDataErrorInfo, the DataGridView is so smart that it will actually call the methods on the business object for you. So, if you've implemented IDataErrorInfo correctly, you might see this:

Alternatively, if you have some data-bound TextBoxes or CheckBoxes or ComboBoxes, you can drag an ErrorProvider onto your form. Simply set the DataSource of the error provider to your business object, and you'll also get the errors reported:

The ErrorProvider works by looking at the data source, seeing what other controls are bound to it, and then displaying errors on those controls when there are errors on the data source.
Take note � in the screens above, the code that triggers the error to be shown is in the business object, not in the GUI. I didn't have to write a single line of GUI code � it's already done by data binding and IDataErrorInfo.
In Trial Balance, my DomainObject base class implements the IDataErrorInfo, so all of my business objects inherit this functionality.
Here's the class diagram to the important classes:

In Trial Balance, every business object has a CreateRules method, which it inherits from the DomainObject base class. When any of the validation methods on the object are called for the first time, the CreateRules method is called to get a list of all the rules that apply. This is an important point that I'd like to stress � unless you actually call one of the validation methods or properties, the CreateRules method will never be called.
The CreateRules method returns a generic List of Rules to the base class. The Rule class is an abstract class, that contains two properties and an abstract method:
PropertyName � Gets the name of the property that the rule belongs to.
Description � Gets the descriptive text about the rule that's been broken. ValidateRule() � This is the abstract method that returns whether or not your code is valid. Since most rules are very simple one-line constructs, there's an additional class that inherits from Rule, called SimpleRule. This class takes a delegate in its constructor, and this delegate is called by the ValidateRule() method.
If you take a look at the DomainObject class, about half way down, you'll see a method called GetBrokenRules():
public virtual ReadOnlyCollection<Rule> GetBrokenRules(string property) {
property = CleanString(property);
// If we haven't yet created the rules, create them now.
if (_rules == null) {
_rules = new List<Rule>();
_rules.AddRange(this.CreateRules());
}
List<Rule> broken = new List<Rule>();
foreach (Rule r in this._rules) {
// Ensure we only validate a rule that
// applies to the property we were given
if (r.PropertyName == property || property == string.Empty) {
bool isRuleBroken = !r.ValidateRule(this);
// [...Snip...]
if (isRuleBroken) {
broken.Add(r);
}
}
}
return broken.AsReadOnly();
}
First, we check to see whether the CreateRules() method has been called, and if not, we'll call it for the first time. Then, we cycle through every rule that was returned. If the rule applies to the property name that was passed in as a parameter to the method, or if no property name was specified, the abstract ValidateRule() method is called. If that returns false, that rule is added to a list to be returned to the caller.
The property indexer that is there, thanks to IDataErrorInfo, makes use of GetBrokenRules():
public virtual string this[string propertyName] {
get {
string result = string.Empty;
propertyName = CleanString(propertyName);
foreach (Rule r in GetBrokenRules(propertyName)) {
result += r.Description;
result += Environment.NewLine;
}
result = result.Trim();
if (result.Length == 0) {
result = null;
}
return result;
}
}
After calling GetBrokenRules(), it cycles through each one, and if they apply to it, they're added as a long list of error messages to be returned. So, for example, if the indexer is called with "Name" as a parameter, only the rules that apply to the Name property will be included in the result.
The other property that we have to implement as part of IDataErrorInfo, Error, calls the indexer with no property name specified. In this case, the indexer will validate all rules, which is exactly what the Error property is supposed to return.
I feel that my approach gives developers a lot of power, because they aren't just limited to using delegates as rules. The Rule class is designed to follow the Strategy pattern, so you can simply subclass Rule to create common, reoccurring rules, such as a NotBlankRule, a DateRangeRule, or even very complex rules that use multiple properties that go off and call web services to validate.
I think I'll summarize this post by pointing out that there's really no single best solution when it comes to how you implement your business rules. It's something that always varies from project to project, and it's up to us, as developers, to make the best call. This article was just an attempt to outline one possible alternative that you may consider when it comes to developing your own system of business rules, and a discussion about what I believe are the important properties of any business rule system.
I'd also like to say that the data binding features in Windows Forms are very rich, and it doesn't take much to use them to your advantage and create a very rich GUI. If you haven't heard of IDataErrorInfo, INotifiesPropertyChanged, or IEditableObject, you would really be doing yourself a favour by heading over to MSDN and having a read through the list of interfaces related to data binding.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 21 May 2006 Editor: Smitha Vijayan |
Copyright 2006 by Paul Stovell Everything else Copyright © CodeProject, 1999-2010 Web20 | Advertise on the Code Project |