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);
}