In our last post, we learned how ValidationContext can be used to provide and consume state and services for validation methods to use.  Now, let’s take a quick look at how validation methods can actually use this information.  I’m going to include a bunch of code in this post, and I will rely on the code to be rather self-explanatory.

Property-Level Validation

In this property-level validation example, we need to validate that the location entered for a meeting is a valid location from our database.  We want this to work both on the server and on the client, but the data is available differently on the two tiers.  To abstract the data access away, we’ll create an IMeetingDataProvider interface and use that as our means for getting to the list of locations.

    public interface IMeetingDataProvider
    {
       
IEnumerable<Meeting> Meetings { get
; }
       
IEnumerable<Location> Locations { get
; }
    }

 

Now that we have the interface, both the DomainService and the DomainContext need to inject an implementation of IMeetingDataProvider into our ValidationContext.  Here’s the DomainService code:

    public class MeetingService : LinqToEntitiesDomainService<MeetingStoreEntities>, IMeetingDataProvider
    {
       
/// <summary>
        /// Initialize the <see cref="DomainService"/>, setting the
        /// <see cref="ValidationContext"/> with custom state and services.
        /// </summary>
        /// <param name="context">
        /// Represents the execution environment for the operations performed
        /// by a System.ServiceModel.DomainServices.Server.DomainService.
        /// </param>
        public override void Initialize(DomainServiceContext
context)
        {
           
var contextItems = new Dictionary<object, object
>
            {
                {
"AllowOverBooking", HttpContext.Current.Session["AllowOverBooking"] ?? false
}
            };

           
this.ValidationContext = new ValidationContext(this
, context, contextItems);
           
this.ValidationContext.ServiceContainer.AddService(typeof(IMeetingDataProvider), this
);

           
base
.Initialize(context);
        }
        ...
        public IEnumerable<Meeting> Meetings
        {
           
get { return this
.ObjectContext.Meetings; }
        }

       
public IEnumerable<Location
> Locations
        {
           
get { return this
.ObjectContext.Locations; }
        }
    }

And here’s the DomainContext code.  Notice that we’re taking advantage of the fact that every DomainContext is generated as a partial class, so we can easily add interface implementations.

    public sealed partial class MeetingContext : IMeetingDataProvider
    {
       
IEnumerable<Meeting> IMeetingDataProvider
.Meetings
        {
           
get { return this
.Meetings; }
        }

       
IEnumerable<Location> IMeetingDataProvider
.Locations
        {
           
get { return this
.Locations; }
        }
    }

 

Consumers of the DomainContext must create the custom ValidationContext to use for validation:

    var contextItems = new Dictionary<object, object>
    {
        {
"AllowOverBooking", this.allowOverBooking.IsChecked ?? false
}
    };

   
var contextServiceProvider = new SimpleServiceProvider
();
    contextServiceProvider.AddService<
IMeetingDataProvider>(this
.meetingContext);

   
this.meetingContext.ValidationContext = new ValidationContext
(
       
this.meetingContext, contextServiceProvider, contextItems);

 

The plumbing is now in place:

  1. Both the DomainService and the DomainContext provide our Meetings and Locations to be consumed through an IMeetingDataProvider interface;
  2. The interface is registered as a service within the ValidationContext, making it available through ValidationContext.GetService(typeof(IMeetingDataProvider)).

We can now create a new validation method that will verify the location entered is valid.  We’ll create it in the MeetingValidators class that is a .shared.cs file, making it available to both the server and the client.

    public static ValidationResult IsValidLocation(string location, ValidationContext validationContext)
    {
       
if (!string
.IsNullOrEmpty(location))
        {
           
var meetingData = validationContext.GetService(typeof(IMeetingDataProvider
))
               
as IMeetingDataProvider
;

           
if (meetingData != null
)
            {
               
if
(!meetingData.Locations.Any(l => l.LocationName == location))
                {
                   
return new ValidationResult
(
                       
"That is not a valid location"
,
                       
new
[] { validationContext.MemberName });
                }
            }
        }

       
return ValidationResult
.Success;
    }

 

We apply this validation rule to our Meeting entity by adding another attribute to the Location property.

    [Required]
    [
RegularExpression(@"\d{1,3}/\d{4}"
,
        ErrorMessage =
"{0} must be in the format of 'Building/Room'"
)]
    [
CustomValidation(typeof(MeetingValidators), "IsValidLocation"
)]
    [
Display
(Order = 3)]
   
public string Location { get; set; }

 

The last thing to do is to make sure the Location list is loaded into our ValidationContext before users start entering new meetings.  This can be done through a standard Load call, when the screen is first being loaded.

    this.meetingContext.Load(this.meetingContext.GetLocationsQuery());

 

When we run the application, we can now enter an invalid meeting location and get an error message that integrates directly into our UI just like every other validator.

That is not a valid location

 

Entity-Level Validation (Cross-Entity)

Just as our property-level validators can become more powerful using ValidationContext, our entity-level validators can gain capabilities too.  We can perform cross-entity validation.  In this example, we’ll build upon what we’ve wired up above to ensure that locations are not double-booked.  But we’ll also leverage another capability of our ValidationContext, showing how the Items dictionary can be used to perform context-sensitive validation.  In this case, we’ll store a flag for whether or not the user can over-book a location.

We’ll simply add a ToggleButton to the view that toggles our “Allow Over-Booking” mode.  When this ToggleButton is clicked, we need to alter the client-side ValidationContext, but we also need to inform the server that the state has changed.  We can create a RIA Services Invoke method within our DomainService to allow this.

    [Invoke]
   
public void SetOverBookingSetting(bool
allowOverBooking)
    {
       
HttpContext.Current.Session["AllowOverBooking"
] = allowOverBooking;
    }

 

Now when our ToggleButton is clicked, we can execute two simple lines of code to toggle our validation state.

    this.meetingContext.ValidationContext.Items["AllowOverBooking"] = this.allowOverBooking.IsChecked ?? false;
   
this.meetingContext.SetOverBookingSetting(this.allowOverBooking.IsChecked ?? false);

 

In our DomainService code above, you might have noticed that our Initialize method checked the AllowOverBooking session value to set the corresponding ValidationContext dictionary item.  The matching code for the DomainContext also seeded this item by checking the state of the ToggleButton when the ValidationContext was being created.

Also in our DomainService and DomainClient ValidationContext creation code, we made the Meetings collection available through an IMeetingDataProvider interface.  That means validation methods can access the list of meetings available.  On the client, we will validate that there aren’t any conflicts with other meetings loaded on the client, and on the server, we’ll be validating that there are no meetings that conflict, since we’ll have access to the full database.

Let’s take a look at the validation method.

    public static ValidationResult PreventDoubleBooking(Meeting meeting, ValidationContext validationContext)
    {
       
if (validationContext.Items.ContainsKey("AllowOverBooking"
))
        {
           
bool allowOverBooking = (bool)validationContext.Items["AllowOverBooking"
];

           
if
(!allowOverBooking)
            {
               
var meetingData = validationContext.GetService(typeof(IMeetingDataProvider
))
                   
as IMeetingDataProvider
;

               
if (meetingData != null
)
                {
                   
var conflicts = from other in meetingData.Meetings.Except(new
[] { meeting })
                                   
where
other.Location == meeting.Location
                                   
// Check for conflicts by seeing if the times overlap in any way
                                    && (
                                        (other.Start >= meeting.Start && other.Start <= meeting.End) ||
                                        (meeting.Start >= other.Start && meeting.Start <= other.End) ||
                                        (other.End >= meeting.Start && other.End <= meeting.End) ||
                                        (meeting.End >= other.Start && meeting.End <= other.End)
                                        )
                                   
select
other;

                   
if
(conflicts.Any())
                    {
                       
return new ValidationResult
(
                           
"The location selected is already booked at this time."
,
                           
new[] { "Location", "Start", "End"
});
                    }
                }
            }
        }

       
return ValidationResult
.Success;
    }

 

We apply this to our Meeting entity with an attribute on the class easily enough:

    [CustomValidation(typeof(MeetingValidators), "PreventExpensiveMeetings")]
    [
CustomValidation(typeof(MeetingValidators), "PreventDoubleBooking"
)]
    [
MetadataType(typeof(Meeting.MeetingMetadata
))]
   
public partial class Meeting

 

And now, when we attempt to save a meeting that would result in a meeting being double-booked, we get an integrated validation error.

image

But if the user clicks the “Allow Over Booking” button, then the validation rule is turned off, and the meeting can be saved.

RIA Services Validation Recap

This code-heavy post shows examples of how you can leverage ValidationContext to perform intelligent property-level and entity-level validation, including cross-entity validation.  We largely built on top of the previous post that explained how ValidationContext can be set up to provide state and services.  And this has all been part of an in-depth series on RIA Services validation.  Here’s the full series:

    1. Standard Validators
    2. Custom Validation Methods
    3. Custom Reusable Validators
    4. Attribute Propagation
    5. Validation Triggers
    6. Cross-Field Validation
    7. Entity-Level Validation
    8. Providing ValidationContext
    9. Using ValidationContext (Cross-Entity Validation)
    10. ViewModel Validation with Entity Rules

[9/6/2011] The source code for everything shown during the series is available on GitHub.

Digging Deeper

We still have more to cover; I’ll soon provide a validator factory that allows validation rules from entities to be inherited into a ViewModel.