Some Good Things:
End-user feel is pretty nice out of the box
Isn't tied to any underlying data/object technology... I demo'd against an Entity Framework, and also POCO + sprocs
Whilst it can be hard trying to work out how it all hangs together (because of the code gen involved there's a bit of magic to wade through) I don't think it'll take too long to get a feel for it.
Unfortunately it seems to fall down as soon as you step beyond the trivial samples done as online tutorials, and into the real-world of writing enterprise apps. The rest of this post is going to revolve around complex validation. And not crazy 'I need to call a web service to determine if this is valid at this point in time' type complex validation... something really pretty simple in business apps - a conditionally mandatory field.
Imagine a text field, which is required to be entered if and only if a flag is set on another property... it can be optionally populated whenever you want, but if the flag is set, it better be there.
public bool IsFunkyRequired{get;set;}
public string Funky {get;set;}
As the new DataAnnotations suite coming in .Net 4 is attribute based, it suffers the same problems as the Entlib Validation block (and the original Avanade library): because the validation is based on a single property via the attribute, any sort of cross-property validation is surprisingly difficult.
Funky can't get marked [Required()], because it's not required all the time.
Adding logic of the form
if (String.IsNullOrEmpty(Funky) && IsFunkyRequired) { throw new ValidationException(blah blah); }
into the property setters causes issues when initially loading data from the database (eg, when the flag is set but the text field hasn't yet been loaded), and doesn't really fit in with the whole attribute based way of doing things.
The next logical step is to write your own validator attribute, by (funnily enough) inheriting from ValidationAttribute. After realising that the obvious IsValid(object value) method is not sufficient (as it is only useful if you care about the value set in the property, which isn't enough in this case), and fighting with the wonder that is the ValidationResult class, you eventually reach the point where you have a reasonably concise validation method, as follows.
Forgive the jump to VB, I'm trying to be equal opportunist here.
Public Class FunkyRequiredAttribute
Inherits ValidationAttribute
Protected Overrides Function IsValid(ByVal value As Object, ByVal validationContext As System.ComponentModel.DataAnnotations.ValidationContext) As System.ComponentModel.DataAnnotations.ValidationResult
If (TypeOf (validationContext.ObjectInstance) Is TestClass) Then
Dim obj As Class1 = DirectCast(validationContext.ObjectInstance, TestClass)
If (String.IsNullOrEmpty(obj.Funky) AndAlso obj.IsFunkyRequired) Then
Return New ValidationResult("Funky must be populated when flag is set.")
End If
End If
Return ValidationResult.Success
End Function
End Class
And this surprisingly works really well, no data is committed to the database if the object is in an invalid state.... of course you don't get any error messages raised either.
Spelunking into the genned code shows that our custom attribute hasn't been applied to the genned Entity class for the client side, hence no client-side validation when tabbing off fields.
Luckily Guy Burtstein has some older info showing some ways around this. Note that the [Shared] attribute doesn't seem to exist any more, you just rely on a well named .shared.vb or .shared.cs file name.
At this point, we have a custom client+server validator that runs quite happily.
Except it doesn't work.
You see validation is applied prior to the property being set, so if we clear the Funky field on an entity the validator returns success, as the property is still set... this applies when adding data to a previously invalid item: the error can't be cleared.
Changing the validator to operate on the passed value (ie, the new value we're attempting to set) only works if we handle each property type it's applied to, for example the following works:
Except it doesn't, really. Because this is considered two separate validations by the system (because it applies to two different properties), you have piss-poor usability.
Protected Overrides Function IsValid(ByVal value As Object, ByVal validationContext As System.ComponentModel.DataAnnotations.ValidationContext) As System.ComponentModel.DataAnnotations.ValidationResult
If (TypeOf (validationContext.ObjectInstance) Is TestClass) Then
Dim obj As TestClass = DirectCast(validationContext.ObjectInstance, TestClass)
If (TypeOf (value) Is String) Then
'changed the Funky field
Dim s As String = DirectCast(value, String)
If (String.IsNullOrEmpty(s) AndAlso obj.IsFunkyRequired) Then
Return New ValidationResult("Funky must be populated when flag is set.")
End If
ElseIf (TypeOf (value) Is Boolean) Then
'changed the flag
Dim b As Boolean = DirectCast(value, Boolean)
If (String.IsNullOrEmpty(obj.Funky) AndAlso b = True) Then
Return New ValidationResult("Funky must be populated when flag is set.")
End If
End If
End If
Return ValidationResult.Success
End Function
Starting with the flag and field both populated, you clear the field, adding an error to the Funky control. You then clear the check box, placing the object in a valid state, but because the error is on a separate field, the old error remains in place on the screen. There isn't a tester known to man that would let me slip that mickey to the users.
Better yet, because the change to clear the textbox was considered an invalid move, it never really took place. The Validator.ValidateProperty method called by the genned Entity throws a ValidationException if any validation on that property fails, meaning the property setter is never called (validation occurs before the setter is called remember).
This all means when you submit changes despite there being an error on the screen, the checkbox is cleared, but the textbox still has it's original value in it.
[Side Rant]
Why or why are we throwing exceptions for what is obviously a very common path through the system? It fails both of Rico's almost rules regarding exceptions.
[/Side Rant]
I've only been playing with this for a day or so, so it's entirely possible I'm missing some really obvious stuff that would make all these go away, and to be fair it's not at a go-live stage yet, but I'm really worried this is taking us down a path we don't want to be on.
These issues all seem to stem from two basic premises (and I'm not sure which started the other, or if they were unrelated):
1. Validation is a per-property behaviour, as opposed to per-object behaviour
2. Entities are not allowed to enter an invalid state, rather than not being allowed to be saved in an invalid state
I don't have any solution to this yet, but I suspect the CSLA Light stuff might be able to be jury-rigged in here somehow.
No comments:
Post a Comment