Sometimes you need to perform some validation on the input of a user in EditForm.aspx or NewForm.aspx. I saw some people validating the input in the SPItemEventReceiver and throwing an ugly exception. Not sure your user will not panic by the error message... Come on guys, we can provide some more useful validation tips!
Sure you can write your own custom field (or inherits from an existing one) but this is fastidious work. What about checking whether the "EndDate" is later than "StartDate"?
I want to write my CustomValidator in a way that is reusable and extensible. My first question was: where to add my javascript/server code?
Here is a preview of the final solution:
1. Using your own New/Edit Form page
In the schema.xml of your list, there is a section where you define the pages used to render Disp/Edit/New form. Something likes this:
|
<Forms> <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" /> <Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" /> <Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" /> </Forms>
|
By default, you will find SetupPath="pages\form.aspx" that indicates to SharePoint to use the file located at 12\TEMPLATE\Pages\form.aspx. To specify your own page, replace SetupPath="pages\form.aspx" by "myform.aspx" (without the reference to the pages directory) and deploy your page in the same directory as your schema.xml. You can add directly some javascript, a different look, another CSS, … but be warned the page will be ghosted. So this is not the solution I prefer for maintenance reason.
Let's find another way…
2. Overriding DefaultTemplates.ascx
I already talked about overriding a rendering template so I won't go into details again. In the definition of your <ContentType> locate the tag <XmlDocuments>. Change the default ListForm by the name you want:
|
<XmlDocuments> <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms"> <FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms"> <Display>ListForm</Display> <Edit>NotesForPictureListForm</Edit> <New>NotesForPictureListForm</New> </FormTemplates> </XmlDocument> </XmlDocuments>
|
In this example, I just want to modify Edit & New so I leave Display unchanged.
SharePoint will look for a RenderingTemplate named NotesForPictureListForm. Add a new file in 12\TEMPLATES\CONTROLTEMPLATES such as: NotesForTemplates.ascx. Copy-Paste the definition of ListForm from DefaultTemplates.ascx and renames it to NotesForPictureListForm. You can add some JavaScript validation using this technique to retrieve field's value. But I have to do some server validation and I want to write something reusable.
This is the code I wrote initially and what I want to obtain:
|
<SharePoint:RenderingTemplate ID="NotesForReportListForm" runat="server"> <Template> <SPAN id='part1'> <SharePoint:InformationBar runat="server"/> <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator=" " runat="server"> <Template_RightButtons> <SharePoint:NextPageButton runat="server"/> <SharePoint:SaveButton runat="server"/> <SharePoint:GoBackButton runat="server"/> </Template_RightButtons> </wssuc:ToolBar> <SharePoint:FormToolBar runat="server"/> <TABLE class="ms-formtable" style="margin-top: 8px;" border=0 cellpadding=0 cellspacing=0 width=100%> <SharePoint:ChangeContentType runat="server"/> <SharePoint:FolderFormFields runat="server"/> <SharePoint:ListFieldIterator runat="server"/> <NotesFor:FieldFormValidatorAdapter FieldName="Title" runat="server"> <Template_Metadata> <NotesFor:UniqueListItemNameCustomValidator runat="server" ErrorMessage="This report already exists." /> </Template_Metadata> </NotesFor:FieldFormValidatorAdapter> <SharePoint:ApprovalStatus runat="server"/> <SharePoint:FormComponent TemplateName="AttachmentRows" runat="server"/> </TABLE> <table cellpadding=0 cellspacing=0 width=100%><tr><td class="ms-formline"><IMG SRC="/_layouts/images/blank.gif" width=1 height=1 alt=""></td></tr></table> <TABLE cellpadding=0 cellspacing=0 width=100% style="padding-top: 7px"><tr><td width=100%> <SharePoint:ItemHiddenVersion runat="server"/> <SharePoint:ParentInformationField runat="server"/> <SharePoint:InitContentType runat="server"/> <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator=" " runat="server"> <Template_Buttons> <SharePoint:CreatedModifiedInfo runat="server"/> </Template_Buttons> <Template_RightButtons> <SharePoint:SaveButton runat="server"/> <SharePoint:GoBackButton runat="server"/> </Template_RightButtons> </wssuc:ToolBar> </td></tr></TABLE> </SPAN> <SharePoint:AttachmentUpload runat="server"/> </Template> </SharePoint:RenderingTemplate>
|
This is my dream: I just declare a template inline and specify the FieldName I want to validate. To fit the context of the rendering, you must inherit from Microsoft.SharePoint.WebControls.FormComponent. Now, you have access to helper properties like Item, ControlMode, ItemContext, List,… As my target is a Field, I inherit from FieldMetadata which inherits itself from FormComponent.
The following class will look for the BaseFieldControl associated to the targeted SPField and add the Template_Metadata at the end of it.
FormContext.FieldControlCollection holds the list of BaseFieldControl of the current ContentType.
|
using System; using System.Web.UI; using Microsoft.SharePoint.WebControls; using Microsoft.SharePoint; namespace NotesFor.WebControls { /// <summary> /// An extender class that allows to insert a CustomValidator and associate it to a SPField. /// </summary> public sealed class FieldFormValidatorAdapter : FieldMetadata { protected override void CreateChildControls() { if (this.Template_Metadata != null && this.ControlMode != SPControlMode.Display) { foreach (BaseFieldControl fc in this.ItemContext.FormContext.FieldControlCollection) { if (fc.FieldName == this.FieldName) { Template_Metadata.InstantiateIn(fc.TemplateContainer); break; } } } }
/// <summary> /// Gets or sets the template to insert in the field rendering. /// </summary> public ITemplate Template_Metadata { get; set; } } }
|
3. Validation using classic Asp.Net Validator
You can use the classic asp.net validator directly in the ascx file (or their counterpart <SharePoint:InputFormXXX>. One or more validators can be defined in Template_Metadata.
Take a look at this gold post about the different validators that exist in SharePoint.
|
<NotesFor:FieldFormValidatorAdapter FieldName="Title" runat="server"> <Template_Metadata> <SharePoint:InputFormRegularExpressionValidator runat="server" ValidationExpression="^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$" ControlToValidate="TextField" ErrorMessage="This is not a valid e-mail address." /> </Template_Metadata> </NotesFor:FieldFormValidatorAdapter>
|
4. Unique ListItem Name validator
In Bonus, this is my final solution to ensure the Title of a ListItem is unique throughout a SPList.
There is another solution available on codeplex but that one throws an ugly exception from the SPItemReceiver 
|
using System; using System.Text; using System.Web.UI.WebControls; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; namespace NotesFor.WebControls { /// <summary> /// Represents a custom validator that ensures the name of a ListItem is unique in the List. /// </summary> public sealed class UniqueListItemNameCustomValidator : FormComponent { /// <summary>This flag is used to avoid 2 call to ServerValidate: one by the postback, the second caused by SaveButton</summary> private bool? isValid;
protected override void CreateChildControls() { if (base.ControlMode != SPControlMode.Display) { BaseTextField textField = (BaseTextField) this.Parent.NamingContainer;
InputFormCustomValidator nameValidator = new InputFormCustomValidator(); nameValidator.ControlToValidate = "TextField"; nameValidator.EnableClientScript = false; nameValidator.ServerValidate += nameValidator_ServerValidate; nameValidator.ErrorMessage = this.ErrorMessage ?? "This name already exists"; nameValidator.BreakAfter = this.BreakAfter; nameValidator.BreakBefore = this.BreakBefore; this.Parent.Controls.Add(nameValidator); } }
private void nameValidator_ServerValidate(object source, ServerValidateEventArgs args) { if (isValid.HasValue) { args.IsValid = isValid.Value; return; }
// Check whether a ListItem already exists in the list with the same name. SPQuery query = new SPQuery(); query.Folder = this.List.RootFolder;
StringBuilder sb = new StringBuilder(); sb.Append("<Where>"); sb.Append(" <Eq>"); sb.Append(" <FieldRef Name='Title' />"); sb.AppendFormat(" <Value Type='Text'>{0}</Value>", args.Value); sb.Append(" </Eq>"); sb.Append("</Where>"); query.Query = sb.ToString(); SPListItemCollection items = this.List.GetItems(query); if (items.Count > 0) { if (this.ControlMode == SPControlMode.Edit) args.IsValid = this.ItemId == items[0].ID; else args.IsValid = false; } isValid = args.IsValid; } /// <summary> /// Gets or sets the message to display in case a duplicate name occurs. /// </summary> public String ErrorMessage { get; set; } public bool BreakAfter { get; set; } public bool BreakBefore { get; set; } } }
|
Hope someone find this useful!
Source code: NotesForValidation.zip (21.88 kb)