What Do We Want in Silverlight Validation?


Posted by Shawn Wildermuth on Oct 05, 2009 on 10:26AM

Url: http://niagara.codeplex.com

Niagara Project

As I've been on the mend lately, i've been looking deep into how Validation should work in Silverlight. As I am trying to expand some of hte validation scenarios in Silverlight (in my Niagara project), I'd like to see how you are feeling about Validaiton.

Currently (as my previous post mentioned), the current validation stack is pretty tightly coupled to RIA Services (though with some work you can get it working with other data stacks).  But the question is really which validation stack does Silverlight really need. Let's discuss the two I've been looking at: DataAnnotations and Enterprise Libraries' Validation Application Block.

DataAnnotations

Let's talk history. Back in .NET 3.5 SP1 (ok, I know many of you are still in .NET 2.0, my apologies), the DataAnnotations assembly was shipped. My assumption that this was to accomodate the newly christened Dynamic Data Web projects. Dynamic Data needed a way to communicate information it couldn't discern (like required fields, string length, etc.) The way that it did this was via a set of attributes in the System.ComponentModel.DataAnnotations. For example:

public class GameInfo
{
  [Required]
  [StringLength(100)]
  public string Name { get; set; }

  [Range(0d, 1000d)]
  public decimal Price { get; set; }
}

The problem with this approach was that in most cases, the classes were generated via an ORM (LINQ to SQL, Entity Framework, etc.) and adding these attributes on generated code just doesn't work. So someone came up with a workaround (or a hack if you prefer): the Metadata Type.

The idea behind a metadata type was that with partial types, users can add attributes to a class (but not a property) so by adding an attribute to each entity class that said, the metadata (e.g. validation attributes) for this class are stored in this metadata type that is never used except as a payload for the metadata about our entity.  For example:

[MetadataType(typeof(Game.GameMetadata))]
public partial class Game
{
  internal sealed class GameMetadata
  {
    // Metadata classes are not meant to be instantiated.
    private GameMetadata()
    {
    }

    public string Description;

    public string Developer;

    public EntityState EntityState;

    public int GameID;

    public Genre TheGenre;

    public string ImageUrl;

    [Required]
    [StringLength(100)]
    public string Name;

    [Range(0d, 2000d)]
    public Nullable Price;

    public string Publisher;

    public Rating TheRating;

    public Nullable ReleaseDate;
  }
}

Early on, using the metadata classes to add these bits of data may have made sense because the sites you were creating were quick and dirty data editing sites. The problem is that this stack was adopted by RIA Services to allow for better validation in the Silverlight space (and eventually other platforms).

The DataAnnotations in RIA Services are a bit fractured as well. There are no less than three versions of the DataAnnotations that can cause confusion. The atttributes lists are different in these three versions:

  • .NET 3.5 SP1's System.ComponentModel.DataAnnotations
  • RIA Services's System.ComponentModel.DataAnnotations
  • Silverlight's System.ComponentModel.DataAnnotations

The three versions can cause some confusion as the attributes that you can use are duplicative (See .NET 3.5 SP1's Description attribute versus RIA Service's as an example).

The current Silverlight validation stack relies on exceptions to be thrown during set property setters (or you can validate the whole objects yourself). While this works, it does seem at odd with the work at hand and having a simple Validation model that returned results would be better IMHO. I understand that RIA Services was trying to fit into an existing Silverlight model but that doesn't mean I have to like it.

This stack may be good enough, especially with the ability to do custom validation with RIA Services but I sense that it may be too shallow for real validation.

Enterprise Library's Validation Application Block

On the other side of the coin is the Patterns and Practice team's Validation Application Block (VAB). This is a set of code that allows you to specify validation using either attributes or configuration files. While the Validation Application Block doesn't support Silverlight directly, I've been investigating it as an alternative design.

The Validation Application Block's design is focused on some of the same principals as the DataAnnotations but they were trying to solve some different problems.  Some of the features of that design that may be of use in Silverlight are:

  • Multi-level validation (e.g. Validates with And/or clauses: "NULL or StringLength 5-25")
  • Configuration doesn't rely on attributes but can use them
  • Uses a provider model for specifying the validation so other sources of validation metadata are possible
  • Large number of built-in validations
  • Support for multiple sets of validation (RuleSets)
  • Validation is object-based to allow for cross validation (Either Name or CompanyName but not be null).
  • Fail-over validation detection (e.g. look for attributes, if not found look up configuration, etc.)
  • Validation returns simple ValidationResults which can be parsed or shown to the user. No Exceptions requried.

For example a typical attribute scenario for validation looks a lot like the RIA Services' approach:

[StringLengthValidator(1, 50, 
  Ruleset="RuleSetA", 
  MessageTemplate="First Name must be between 1 and 50 characters")]
public string FirstName
{
    get { return firstName; }
    set { firstName = value;  }
}

[StringLengthValidator(1, 50, 
  Ruleset = "RuleSetA", 
  MessageTemplate = "Last Name must be between 1 and 50 characters")]
public string LastName
{
    get { return lastName; }
    set { lastName = value; }
}

[RelativeDateTimeValidator(-120, 
  DateTimeUnit.Year, -18, DateTimeUnit.Year, 
  Ruleset="RuleSetA", 
  MessageTemplate="Must be 18 years or older.")]
public DateTime DateOfBirth
{
    get { return dateOfBirth; }
    set { dateOfBirth = value; }
}

[RegexValidator(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", 
  MessageTemplate="Invalid e-mail address", 
  Ruleset = "RuleSetA")]
public string Email
{
    get { return email; }
    set { email = value; }
}

[ObjectValidator("RuleSetA", Ruleset = "RuleSetA")]
public Address Address
{
    get { return address; }
    set { address = value; }
}

[RangeValidator(0, RangeBoundaryType.Inclusive, 
  1000000, 
  RangeBoundaryType.Inclusive, 
  Ruleset = "RuleSetA", 
  MessageTemplate="Rewards points cannot exceed 1,000,000")]
public int RewardPoints
{
    get { return rewardPoints; }
    set { rewardPoints = value; }
}

In addition it supports a configuration-based solution:

 <validation>
    <type assemblyName="ValidationQuickStart.BusinessEntities, 
                        Version=1.0.0.0, 
                        Culture=neutral, PublicKeyToken=null"
          name="ValidationQuickStart.BusinessEntities.Address">
      <ruleset name="RuleSetB">
        <properties>
          <property name="City">
            <validator lowerBound="1"
                       lowerBoundType="Inclusive"
                       upperBound="30"
                       upperBoundType="Inclusive"
                       negated="false"
                       messageTemplate=""
                       messageTemplateResourceName=""
                       messageTemplateResourceType=""
                       tag=""
                       type="Microsoft.Practices.EnterpriseLibrary.
                         Validation.Validators.StringLengthValidator, 
                         Microsoft.Practices.EnterpriseLibrary.Validation"
                       name="String Length Validator" />
            <validator characterSet="1234567890!@#$%^&*()-="
                       containsCharacter="Any"
                       negated="true"
                       messageTemplate="City may not contain numbers"
                       messageTemplateResourceName=""
                       messageTemplateResourceType=""
                       tag=""
                       type="Microsoft.Practices.EnterpriseLibrary.Validation.
                         Validators.ContainsCharactersValidator, 
                         Microsoft.Practices.EnterpriseLibrary.Validation"
                       name="Contains Characters Validator" />
            <validator negated="false"
                       messageTemplate=""
                       messageTemplateResourceName=""
                       messageTemplateResourceType=""
                       tag=""
                       type="Microsoft.Practices.EnterpriseLibrary.Validation.
                         Validators.NotNullValidator, 
                         Microsoft.Practices.EnterpriseLibrary.Validation"
                       name="Not Null Validator" />
          </property>
      </ruleset>
    </type>
  </ruleset>
</validation>

This syntax while it works, its pretty terse and unreadable (as configuration tends to be). But it is decoupled which is nice.

While many of these features are laudable, it feels a bit over-engineered and complex.  The VAB uses Validators for a number of cases where its just a grouping technique so debugging individual validations can be complex.

"Just Right?"

So here is where I put it to you, my readers:

"What do you need in Silverlight?"

  • Is the RIA Stack good enough?
  • Do you want validation regardless of what the transport is (ADO.NET Data Services, WCF, REST)?
  • Is using  RIA Services' CustomValidation with method name good enough for custom scenarios?
  • Do you need complex validation (where AND/OR validation is necessary)?
  • How important is decoupling your validation metadata from the model?
  • Do you detest metadata types or is it just a necessary evil?

Please comment here so we can start a dialogue to help me understand what the development community is thinking about validation.

 




Application Name WilderBlog Environment Name Production
Application Ver 1.0.0.0 Runtime Framework .NETCoreApp,Version=v1.0
App Path D:\home\site\wwwroot Runtime Version .NET Core 4.0.0.0
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86