Silverlight - RadScheduler and required field data validation
In my current project I'm having the pleasure of working with the Silverlight RadScheduler control. While you may read and feel it is a very bloated control, I argue otherwise, it is highly extensible for such a complex control. I really think the Telerik guys did a very good job.
My particular scenario is interesting. I need to:
- Extend the appointment dialog to support addresses
- The addresses aren't always required (so I can't just use RequiredAttribute)
- When fields are required, I'd like the validation to kick off and prevent me from saving that appointment
Here we go:
Updating the RadScheduler template:
This is pretty simple - find the EditAppointmentTemplate
<ControlTemplate x:Key="EditAppointmentTemplate" TargetType="telerik:AppointmentDialogWindow">
- Customize the control template, we added additional fields
- NotifyOnValidationError and ValidatesOnException are important.
..snip <input:ValidationSummary x:Name="ValidationSummary" /> <TextBlock Text="Suburb" Style="{StaticResource FormElementTextBlockStyle}"/> <TextBox Text="{Binding Suburb, Mode=TwoWay, NotifyOnValidationError=True,
ValidatesOnExceptions=True}" />
Extend the Appointment class:
- Create a ValidationEnabled property - sometimes you need to "not validate"
- Create a ValidateProperty method - this takes a property name and triggers all validate attributes on that property - Validator.ValidateProperty is the method that will trigger all kinds of fun validation exceptions for us.
- Set the appropriate validation attributes on the property e.g. Suburb - I'm using a custom validation property that I've created based on the RequiredAttribute
- In the setter, call ValidateProperty.
public class JobAppointment : Appointment { private string _suburb; public ObservableCollection<ValidationError> Errors { get; set; } public bool ValidationEnabled { get; set; } private void ValidateProperty(string propertyName, object value) { if (ValidationEnabled) { var context = new ValidationContext(this, null, null); context.MemberName = propertyName; Validator.ValidateProperty(value, context); } } [ValidationRequired] public string Suburb { get { return _suburb; } set { ValidateProperty("Suburb", value); if (_suburb != value) { _suburb = value; OnPropertyChanged("Suburb"); } } }
Add our own Validation attributes
- This Required attribute checks against our settings to see if "Suburb" happens to be a required field. If not, then skip the validation and just return success.
public class ValidationRequiredAttribute : RequiredAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (SettingsLifeTimeService.Current != null ) { if (SettingsLifeTimeService.Current.RequiredFields.Contains(validationContext.MemberName)) { // RequiredAttribute validation return base.IsValid(value, validationContext); } } return ValidationResult.Success; } }
Stop validation on Scheduler's Appointment_Saving event
- In the AppointmentSaving event, grab all the children framework elements from the appointment dialog window
- Find their binding expression (e.g., for TextBox I want the binding expression for Textbox.Text)
- If exists, I want it to push the values back into the datasource (our customized Appointment class) - this triggers the property setter (which triggers our ValidateProperty, which triggers the custom Required attribute)
- Finally, check the validation summary to see if we have any binding errors, if we do, cancel the save.
void _scheduler_AppointmentSaving(object sender, AppointmentSavingEventArgs e) { JobAppointment jobApp = e.Appointment as JobAppointment; AppointmentDialogWindow window = e.OriginalSource as AppointmentDialogWindow; var children = window.ChildrenOfType<FrameworkElement>(); if (children != null) { foreach (var element in children) { BindingExpression binding = null; if (element is TextBox) { binding = element.GetBindingExpression(TextBox.TextProperty); } if (binding != null) { // force control to update databound VM. This triggers the validation. binding.UpdateSource(); } } } ValidationSummary summary = window.FindChildByType<ValidationSummary>(); if (summary != null) { if (summary.HasErrors) { e.Cancel = true; return; } } }
Finished!