Monday, October 05, 2009
« CSLA .NET 3.8 Beta 1 available | Main | Gartner too cautious toward Win7 »

One of the new features in CSLA .NET 3.8 is the ability to use the data annotation attributes from System.ComponentModel.DataAnnotations.

The DataAnnotations namespace was added in .NET 3.5 SP1, and includes a ValidationAttribute class that acts as the base class for validation attributes. An example of a validation attribute is Required, which is used to indicate that a property is a required value.

DataAnnotations are available in both .NET and Silverlight, though it turns out that their implementations aren’t quite the same. Still, their usage is the same, in that you decorate properties with data annotation attributes.

The idea behind DataAnnotations is that they are UI independent. If you put a Required attribute on a property, and then you build a UI using a technology that understands these ValidationAttribute subclasses, the UI will honor the attribute. The Silverlight DataForm is one example of a UI technology that does understand these attributes, and I suspect we’ll see many more UI technologies start to leverage them.

What’s interesting about this, is that the attribute object itself contains the validation rule logic. So the UI doesn’t actually implement the Required rule logic – it just asks the attribute object to do the evaluation. This is where things aren’t the same on .NET and Silverlight. On .NET ValidationAttribute subclasses override IsValid(), returning true if the rule is satisfied, false if not. On Silverlight ValidationAttribute subclasses override GetValidationResult() which returns null if the rule is satisfied, and a result object if the rule is broken. Either way the basic concept is the same, but the implementation code is different.

I wanted to support these attributes in CSLA .NET (for Windows and Silverlight). This is really a two-part process.

First, I needed to have a way to detect the attribute on a property, and attach a CSLA validation rule to that property so CSLA knows to execute the attribute’s rule when appropriate. CSLA .NET already has a business/validation rule subsystem that knows how to execute rules – I just needed to auto-add rules for data annotation attributes.

Second, I needed a way to execute any ValidationAttribute subclass, since that’s how one of these rules is evaluated. Again, CSLA .NET already has a validation rule subsystem that says validation rules are methods. So I just needed to create a CSLA-style rule method that knows how to invoke a ValidationAttribute to actually evaluate the rule’s condition.

I added a ValidationRules.AddDataAnnotations() method that you can call in your AddBusinessRules() override. This method reflects against your properties, detects any ValidationAttribute subclasses on any of your properties and adds a CSLA-style rule to link that property to the rule attribute. I also changed BusinessBase.AddBusinessRules() (this is the base method) so AddDataAnnotations() is called if you don’t override AddBusinessRules() – effectively making the use of data annotation attributes a default behavior.

Then I created a Csla.Validation.CommonRules.DataAnnotation() rule method. This is the rule method that is automatically associated with properties by AddDataAnnotations(). The DataAnnotation() rule method is pretty simple – it just executes the attribute object’s IsValid() or GetValidationResults() methods (depending on whether the code is running on .NET or Silverlight).

The end result is that when using CSLA .NET 3.8 you can apply data annotation attributes to your business class properties, and (assuming AddDataAnnotations() is called) they’ll be automatically linked into the normal CSLA .NET validation subsystem and executed like any other CSLA .NET business or validation rules.

[Serializable]
public class Data : BusinessBase<Data>
{
  private static PropertyInfo<int> IdProperty = 
    RegisterProperty<int>(c => c.Id);
  [Range(0, 10, ErrorMessage = "Id must be between 0 and 10")]
  public int Id
  {
    get { return GetProperty(IdProperty); }
    set { SetProperty(IdProperty, value); }
  }

  private static PropertyInfo<string> NameProperty =
    RegisterProperty<string>(c => c.Name);
  [Required(ErrorMessage = "Name is required")]
  public string Name
  {
    get { return GetProperty(NameProperty); }
    set { SetProperty(NameProperty, value); }
  }
}

Notice that this class doesn’t override AddBusinessRules(), so the base implementation is used, and the base implementation calls ValidationRules.AddDataAnnotations().

When AddDataAnnotations() is called, it detects the Range and Required attributes on the properties, and those attributes are automatically linked into the normal CSLA .NET business rule subsystem. This means the rules are automatically checked when a new instance of the object is created, and any time the property values are set – exactly the behavior you’d get with regular CSLA .NET rule methods.

But what’s nice about this is that these attributes are also honored by some UI technologies (like DataForm). This means that you automatically get the UI behaviors (if any) and yet you know that the rules will be run in the business layer regardless of whether the UI honors the attributes or not.

So this object will work nicely in the Silverlight DataForm, and will have the same business behavior in Windows Forms, behind a WCF service or anywhere else the object is used – even if the UI technology doesn’t honor the attributes.

You can also use this approach to create your own validation attributes by subclassing the ValidationAttribute base class in System.ComponentModel.DataAnnotations.

The only limitation I’ve found is that these attribute-based rules can only operate on a single property value. You can’t use this technique to do multi-property rules, or rules that operate on child collections or across other objects. You’ll need to use the normal CSLA .NET business/validation rule method approach to implement anything beyond simple single-property rules.

Monday, October 05, 2009 12:29:37 PM (Central Standard Time, UTC-06:00)
I use this as well for my office, love using this with ASP.NET pages. Lets us define validation rules on the property instead of cut/paste validators between pages.
Sean
Monday, October 05, 2009 12:42:41 PM (Central Standard Time, UTC-06:00)
Sean, I agree - I really like having the rules in the business layer, because that way they are guaranteed to run and be consistent everywhere. But having a model where UI technologies can choose to express the rules to the user is also really nice - especially in web settings where expressing the rules to the user is otherwise somewhat difficult.
Monday, October 05, 2009 5:26:29 PM (Central Standard Time, UTC-06:00)
Exactly the information I was looking for Rocky - thanks!

Your implementation looks really neat :)
Paul
Tuesday, October 06, 2009 1:58:11 AM (Central Standard Time, UTC-06:00)
One additional limitation on this approach is that the error message coded into the attribute cannot be localized.
William
Tuesday, October 06, 2009 7:11:32 AM (Central Standard Time, UTC-06:00)
That's not actually true - localization is supported. The data annotation attributes are designed to get their error message text either directly or from a resource file. If you have them read from a resource file then the messages can be localized.
Tuesday, October 06, 2009 7:21:41 AM (Central Standard Time, UTC-06:00)
The only limitation I’ve found is that these attribute-based rules can only operate on a single property value. You can’t use this technique to do multi-property rules, or rules that operate on child collections or across other objects. You’ll need to use the normal CSLA .NET business/validation rule method approach to implement anything beyond simple single-property rules.

The above statement is not completely true. You can create custome validation inherited from CustomValidationAttribute class in silverlight to support complicate validation rules.
Chen Lu
Tuesday, October 06, 2009 7:30:20 AM (Central Standard Time, UTC-06:00)
Example of multi-property validation

[Compare(typeof(string), "Password", "ConfirmPassword", ErrorMessage = "Password does not match confirm password.")]
public partial class AddUserViewModel : IAddUserViewModel
{
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)]
public class CompareAttribute : ValidationAttribute
{
private Type _compareType;
private string _sourcePropertyName;
private string _targetPropertyName;

public Type CompareType
{
get
{
return _compareType;
}
}

public string SourcePropertyName
{
get
{
return _sourcePropertyName;
}
}

public string TargetPropertyName
{
get
{
return _targetPropertyName;
}
}

#if !SILVERLIGHT
/// <summary>
/// Gets a unique identifier for this attribute.
/// </summary>
public override object TypeId {
get {
return this;
}
}
#endif

public CompareAttribute(Type compareType, string sourcePropertyName, string targetPropertyName)
{
_compareType = compareType;
_sourcePropertyName = sourcePropertyName;
_targetPropertyName = targetPropertyName;
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo sourcePropertyInfo = validationContext.ObjectType.GetProperty(_sourcePropertyName, BindingFlags.Public | BindingFlags.Instance);
PropertyInfo targetPropertyInfo = validationContext.ObjectType.GetProperty(_targetPropertyName, BindingFlags.Public | BindingFlags.Instance);

if (_compareType == typeof(string))
{
if (Convert.ToString(sourcePropertyInfo.GetValue(validationContext.ObjectInstance, null)) != Convert.ToString(targetPropertyInfo.GetValue(validationContext.ObjectInstance, null)))
{
return new ValidationResult(ErrorMessage);
}
else
{
return null;
}
}
else if (_compareType == typeof(int))
{
}
else if (_compareType == typeof(double))
{
}
else if (_compareType == typeof(DateTime))
{
}

return null;
}
}
Chen Lu
Tuesday, October 06, 2009 7:37:39 AM (Central Standard Time, UTC-06:00)
I am not convinced that will actually work, specfically because in the .NET implementation the IsValid() method only takes one parameter: the value to validate.

It is true that the Silverlight GetValidationResult() method takes a validation context, and so your code probably works in Silverlight - but I suspect it won't work in .NET.
Tuesday, October 06, 2009 7:40:44 AM (Central Standard Time, UTC-06:00)
Looking at the API a bit further, it isn't clear how to pass in the object reference in Silverlight either. The ValidationContext object doesn't have a constructor where that value can be provided. The constructor does accept a dictionary, so maybe it is a magic string value in the dictionary?
Tuesday, October 13, 2009 9:57:15 PM (Central Standard Time, UTC-06:00)
That is really, really nice.

Sometimes you scare me. :)

Dave
Friday, November 06, 2009 5:55:15 AM (Central Standard Time, UTC-06:00)
Now this is an idea i like. I actually created something similar called [SmartProperty] maybe 7 years ago to deal with, at least, part of this problem - i.e. auto formatting, constraining, ranges, localizing, etc. when binding UI controls to business objects.

I was motivated by two goals:
1. make it easy for good UI developers to pre validate entry to avoid depending on business object broken rules (and reducing roundtrips)
2. make it very hard for a lazy developer to bypass validation and depend only on broken rules (in one app i saw there was different or no entry validation on several screens bound to the same business object property!).

But mostly I think it gets us nearer that goal of <<Load, Bind, user interact, Save>> simplicity in UI interaction with the business layer.
jh72i
Comments are closed.