In my previous post, I showed how to customize a list form without losing all the built-in SharePoint functionality. At the heart of my approach was harnessing SharePoint's own template rendering mechanism, the details of which I'm going to dive into now.
If you've ever used any of the template-based controls in ASP.NET (GridView, Repeater, DataList, etc.), then you've already touched on the core concept. For example, the Repeater control provides various templates (header, footer, item, alternating item, etc.) that allow you to control what content is rendered at different points during the rendering process. Typically you'll provide markup for the control on your page like so:
<asp:Repeater id="myRepeaterUL" runat="server"><HeaderTemplate><ul></HeaderTemplate><ItemTemplate><li><a href='<%# DataBinder.Eval(Container.DataItem, "URL") %>'><%# DataBinder.Eval(Container.DataItem, "Name") %></a></li></ItemTemplate><FooterTemplate></ul></FooterTemplate></asp:Repeater>
Each template you provide in the page markup is loaded by the ASP.NET runtime and exposed through the ITemplate interface which has a single method with the following signature:
void InstantiateIn(Control container)
With the templates loaded, all the Repeater control has to do to instantiate a new row of data is call this method passing in the appropriate container. New instances of any contained ASP.NET controls will be instantiated and processed through the usual ASP.NET control lifecycle.
Now switching back into the SharePoint world, if you look in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES folder, you will see a number of user control files (*.ascx) most of which contain very standard looking user control content. However, the largest of these files (DefaultTemplates.ascx) is a bit different and since it is at the heart of SharePoint's template rendering mechanism, it is the one I am focusing on. If you open up the file you will see that it contains nothing but instances of the <SharePoint:RenderingTemplate> control. Each RenderingTemplate control instance has a unique ID and a single child <Template> element. When SharePoint starts up, it parses all files in this folder and makes all the templates it finds easily accessible through the static GetTemplateByName method of the SPControlTemplateManager class.
In my last post, I showed a web part that exposes a Template ID setting that is used as the basis for its rendering. Instead of overriding one of the web part rendering methods like Render or RenderControl, I am overriding the CreateChildControls method. I simply create an instance of SharePoint's TemplateContainer control, provide its template by loading the template specified in the web part's Template ID setting, add the TemplateContainer instance to my web part's Controls collection. ASP.NET does the rest. Here's the snippet of code I am referring to:
// Initialize template container with our custom templatetemplateContainer = new TemplateContainer();templateContainer.Template = SPControlTemplateManager.GetTemplateByName(RenderingTemplateId);...// Add the container to the webpart control hierarchyControls.Add(templateContainer);
1: <SharePoint:RenderingTemplate ID="ListForm" runat="server">
2: <Template>
3: <SPAN id='part1'>
4: <SharePoint:InformationBar runat="server"/>
5: <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator=" " runat="server">
6: <Template_RightButtons>
7: <SharePoint:NextPageButton runat="server"/>
8: <SharePoint:SaveButton runat="server"/>
9: <SharePoint:GoBackButton runat="server"/>
10: </Template_RightButtons>
11: </wssuc:ToolBar>
12: <SharePoint:FormToolBar runat="server"/>
13: <TABLE class="ms-formtable" style="margin-top: 8px;" border=0 cellpadding=0 cellspacing=0 width="100%">
14: <SharePoint:ChangeContentType runat="server"/>
15: <SharePoint:FolderFormFields runat="server"/>
16: <SharePoint:ListFieldIterator runat="server"/>
17: <SharePoint:ApprovalStatus runat="server"/>
18: <SharePoint:FormComponent TemplateName="AttachmentRows" runat="server"/>
19: </TABLE>
20: <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>
21: <TABLE cellpadding=0 cellspacing=0 width="100%" style="padding-top: 7px"><tr><td width="100%">
22: <SharePoint:ItemHiddenVersion runat="server"/>
23: <SharePoint:ParentInformationField runat="server"/>
24: <SharePoint:InitContentType runat="server"/>
25: <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator=" " runat="server">
26: <Template_Buttons>
27: <SharePoint:CreatedModifiedInfo runat="server"/>
28: </Template_Buttons>
29: <Template_RightButtons>
30: <SharePoint:SaveButton runat="server"/>
31: <SharePoint:GoBackButton runat="server"/>
32: </Template_RightButtons>
33: </wssuc:ToolBar>
34: </td></tr></TABLE>
35: </SPAN>
36: <SharePoint:AttachmentUpload runat="server"/>
37: </Template>
38: </SharePoint:RenderingTemplate>
There's a lot in that template that I could discuss, but the part I want to focus on is the ListFieldIterator on line 16 as this is where all the field rendering occurs.
Before we dive in, you first need to understand that the context under which the page is running in SharePoint is key in defining the behavior of the ListFieldIterator. When you reassign a particular page as the "Edit" form for a list, you'll notice that the URL in the browser carries this context to the page when it loads. For example, in my "Edit Test Center" page example from my first post, the URL that SharePoint uses to launch the edit page with context looks like this:
http://vm-mossdev/sites/intranet/Test%20Security/Pages/TestCenterEdit.aspx?ID=1&List=9a2e36fb-2f44-4355-a2c6-103a6376ac8b&Source=http%3a%2f%2fvm-mossdev%2fsites%2fintranet%2fTest+Security%2fLists%2fTest+Centers%2fAllItems.aspx
SharePoint uses the ID and List parameters to establish the context for the page, and exposes this via the SPContext class. This class has a plethora of information about the current SharePoint context including the list (SPList), the current item (SPItem), and the most importantly for our purposes now, the field definitions (SPField) for the underlying list.
Using this context, the ListFieldIterator iterates through the fields, creating new TemplateContainer controls and adding them to its Controls collection. Importantly, it assigns the FieldName and ControlMode properties. The FieldName is used later in the process by SharePoint to determine (based on the corresponding SPField instance) what kind of control should actually be rendered. The ControlMode property is used to indicate whether the control is in New, Edit or Display mode, which the ListFieldIterator assigns based on the value of its own ControlMode property.
Once it has created the TemplateContainer control, the template container needs a… you guessed it… template to contain. Since the ListFieldIterator class overrides the DefaultTemplateName property to return “ListFieldIterator”, this is the template that is loaded for each field. Looking in the DefaultTemplates.ascx file, this template looks like this:
<SharePoint:RenderingTemplate ID="ListFieldIterator" runat="server">
<Template>
<TR><SharePoint:CompositeField runat="server"/></TR>
</Template>
</SharePoint:RenderingTemplate>
When this template is evaluated and constructed, the CompositeField control simply overrides DefaultTemplateName to first check whether the control mode is SPControlMode.Display and returns either “DisplayCompositeField” or “CompositeField”. The “CompositeField” template looks like this:
<SharePoint:RenderingTemplate ID="CompositeField" runat="server">
<Template>
<TD nowrap="true" valign="top" width="190px" class="ms-formlabel"><H3 class="ms-standardheader"><SharePoint:FieldLabel runat="server"/></H3></TD>
<TD valign="top" class="ms-formbody" width="400px">
<!-- FieldName="<SharePoint:FieldProperty PropertyName="Title" runat="server"/>"
FieldInternalName="<SharePoint:FieldProperty PropertyName="InternalName" runat="server"/>"
FieldType="SPField<SharePoint:FieldProperty PropertyName="Type" runat="server"/>"
-->
<SharePoint:FormField runat="server"/>
<SharePoint:FieldDescription runat="server"/>
<SharePoint:AppendOnlyHistory runat="server"/>
</TD>
</Template>
</SharePoint:RenderingTemplate>
When the FieldLabel control is created (which also inherits from FieldMetadata), it obtains the current SPField through the inherited Field property (which again in this case climbs the control hierarchy to the CompositeField, then to the TemplateContainer control as described above). Then depending on the ControlMode (whether for Display or not) and the field’s Required property in the list definition, it uses one of three templates: FieldLabelForDisplay, FieldLabelRequired, or FieldLabelDefault.
The most interesting of these three is the FieldLabelRequired template which includes the markup necessary for rendering the familiar little red asterisk next to the label. It looks like this:
<SharePoint:RenderingTemplate ID="FieldLabelRequired" runat="server"><Template><nobr><SharePoint:FieldProperty PropertyName="Title" runat="server"/><span class="ms-formvalidation"> *</span></nobr></Template></SharePoint:RenderingTemplate>
When the <SharePoint:FieldProperty> control is rendered by ASP.NET, it pulls the "Title" field value (as defined in the PropertyName attribute of the markup) from the current SPItem and renders it. (NOTE: If you look in the DefaultTemplates.ascx file you will see that the FieldProperty control is used quite frequently to render various properties of fields. You can see an example of this in the HTML comments generated by the CompositeField template above).
Let’s move back now to the second part of the CompositeField template that I want to highlight: the <SharePoint:FormField> control. In absence of an explicit template or template name, the FormField control uses its associated Field's FieldRenderingControl property to retrieve a template-based BaseFieldControl instance that is capable of rendering UI elements specifically for viewing or editing data for the current field type. It then adds this control to its Controls collection.
For an example, if the current field being rendered is a text field, the Field property will actually return an instance of an SPFieldText which inherits from SPField and overrides the FieldRenderingControl property to return an instance of the TextField template-based control. Since the TextField control overrides the DefaultTemplateName property to return "TextField", the following template is generally used (but not always, as in the case of "Choice" fields):
<SharePoint:RenderingTemplate ID="TextField" runat="server">
<Template>
<asp:TextBox ID="TextField" MaxLength="255" runat="server"/><br>
</Template>
</SharePoint:RenderingTemplate>
So finally, when the FormField control is rendered, its child control (the TextField) is rendered, which results in the template listed above being processed and at the end of this very long chain of SharePoint infrastructure… out comes a plain old vanilla ASP.NET TextBox control. Phew!
Now I realize that this is an awful lot to take in, and this post is largely based on notes that I took for myself as I went and traced through all the logic so that I could understand what was going on under the covers. The upshot of it is that once you understand the rendering mechanism SharePoint uses, you can begin to customize your forms using custom templates in ways that used to require a complete departure from the out-of-the-box SharePoint functionality.