Requirements
Today I had the requirement to add custom validation logic to a SharePoint edit form. The edit form is being displayed using a ListFieldIterator. The requirement was to make sure the Date field could not be set to a date in the past.
The solution (Other solutions are available and are most probably better!) I came up with involved subclassing ListFieldIterator and attaching ASP.NET validators at run-time.
Challenge 1
How do you attach a ASP.NET validation control at run-time.
- Attach the validator at the correct stage of the page/control life-cycle. In the case of the ListFieldIterator I wanted to attach it after the control had build it’s control tree. Override CreateChildControls, call base, then attach.
- Find the FormField associated with the SharePoint field (SPField) you want to validate.I have an extension method that I use to parse the control hierarchy of a ListFieldIterator as follows:
Usage:
FormField formField = listFieldIterator.GetFormField("MyInternalFieldName");
public static class ListFieldIteratorExtensions { public static FormField GetFormField(this ListFieldIterator listFieldIterator, string fieldName) { return GetFormField(listFieldIterator, GetFormFields(listFieldIterator), fieldName); } public static FormField GetFormField(this ListFieldIterator listFieldIterator, List<FormField> formFields, string fieldName) { FormField formField = (from form in formFields where form.FieldName.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase) select form).FirstOrDefault(); if (formField == null) { throw new GeneralApplicationException("Could not find form field: " + fieldName); } return formField; } public static List<FormField> GetFormFields(this ListFieldIterator listFieldIterator) { if (listFieldIterator == null) { return null; } return FindFieldFormControls(listFieldIterator); } private static List<FormField> FindFieldFormControls(System.Web.UI.Control root) { List<FormField> baseFieldControls = new List<FormField>(); foreach (System.Web.UI.Control control in root.Controls) { if (control is FormField && control.Visible) { FormField formField = control as FormField; if (formField.Field.FieldValueType == typeof(DateTime)) { HandleDateField(formField); } baseFieldControls.Add(formField); } else { baseFieldControls.AddRange(FindFieldFormControls(control)); } } return baseFieldControls; } private static void HandleDateField(FormField formField) { if (formField.ControlMode == SPControlMode.Display) { return; } Control dateFieldControl = formField.Controls[0]; if (dateFieldControl.Controls.Count > 0) { DateTimeControl dateTimeControl = (DateTimeControl) dateFieldControl.Controls[0].Controls[1]; TextBox dateTimeTextBox = dateTimeControl.Controls[0] as TextBox; if (dateTimeTextBox != null) { if (!string.IsNullOrEmpty(dateTimeTextBox.Text)) { formField.Value = DateTime.Parse(dateTimeTextBox.Text, CultureInfo.CurrentCulture); } } } } }
- Find the Control that is rendered by the FieldControl.Field.FieldRenderingControl. In my specific case a DateTimeField will render a DateTimeControl. Now that we have the form field we grab the rendering control:
Usage:
Control renderedControl = GetControl(formField);
private static Control GetControl(FieldMetadata formField) { return formField.FindControlRecursive(x => x.GetType() == GetChildControlBasedOnFieldType(formField.Field.FieldRenderingControl)); } private static Type GetChildControlBasedOnFieldType(object field) { if (field is TextField) { return typeof(TextBox); } if (field is DropDownChoiceField) { return typeof(DropDownList); } if (field is DateTimeField) { return typeof (DateTimeControl); } return null; } public static Control FindControlRecursive(this Control control, Func<Control, bool> evaluate) { if (evaluate.Invoke(control)) { return control; } foreach (Control childControl in control.Controls) { Control foundControl = FindControlRecursive(childControl, evaluate); if (foundControl != null) { return foundControl; } } return null; }
- Now we have found the control we want to validate, we can add the ASP.NET to it’s parent’s control collection
renderedControl.Parent.Controls.AddAfter(control, validator as Control);
Uses another little extension method:
public static void AddAfter(this ControlCollection collection, Control after, Control control) { int indexFound = -1; int currentIndex = 0; foreach (Control controlToEvaluate in collection) { if (controlToEvaluate == after) { indexFound = currentIndex; break; } currentIndex = currentIndex + 1; } if (indexFound == -1) { throw new ArgumentOutOfRangeException("control", "Control not found"); } collection.AddAt(indexFound + 1, control); }
Love the code!! But if your doing a CompareValidator with 2 datetime fields the GetControl functions returns the correct control but the Ids are the same evertime. This must be a sharepoint thing. Anythoughts on how you would get a CompareValidator to work ?
For an alternative way of custom validation have a look at this post:
Cross field, cross item, cross list or even more complicated validations on SharePoint forms
http://pholpar.wordpress.com/2010/01/04/cross-field-cross-item-cross-list-or-even-more-complicated-validations-on-sharepoint-forms/
Nice post! My own FindControlRecursive helped me out many times. Especially, when I deal with MasterPages. For example, one of such usage is shown in my post here – http://dotnetfollower.com/wordpress/2010/12/sharepoint-add-onchange-attribute-to-dropdownchoicefield/.
Thank you!