Sunday, April 10, 2011

How to easily implement SharePoint custom list forms

In a recent project I had to create a number of custom list forms. As it turns out, custom list forms are quite easy to create, requiring only an extra attribute in the list schema.xml, a template file, and a custom control to actually render the custom form.


Basically, I needed to make a list where some fields were editable for some users at some stage in a process, while other fields were editable for other users at another stage.

Imagine a design company, where a designer is responsible for adding new ideas to a product list. The product manager then calculates production cost, after which the sales manager adds an expected retail price, and determines whether to send the idea into production. Each of these persons adds information to the same list item, but only to fields within their own area of expertice. Further more, the sales manager may not add the sales information until the product manager has completed his work.

How to register a custom form for a list template

First, we need to register the custom form in the list definition. In the schema.xml of the list template, typically at the end, there is a <Forms> tag. To register the custom form simply add a Template attribute to the <Form> elements.

In the scenario described above I need to implement some logic in the New and Edit form. For the Display form the SharePoint standard form is sufficient.
Therefore I add a template attribute to two of the forms. As it is the same logic I need to implement for the two forms I just use the same template.

<Forms>
  <Form
    Type="DisplayForm"
    Url="DispForm.aspx"
    SetupPath"pages\form.aspx"
    WebPartZoneID="Main" />
  <Form
    Type="EditForm"
    Url="EditForm.aspx"
    SetupPath"pages\form.aspx"
    Template="MyCustomForm"
    WebPartZoneID="Main" />
  <Form
    Type="NewForm"
    Url="NewForm.aspx"
    SetupPath"pages\form.aspx"
    Template="MyCustomForm"
    WebPartZoneID="Main" />
</Forms>

How to create the custom form

The form template is created using a template in the SharePoint CONTROLTEMPLATES folder. Start by creating an .ascx file in the folder, naming it whatever you like. Then open the DefaultTemplates.ascx file in the same folder, and copy all the top declarations (before the first <SharePoint:RenderingTemplate> tag) to your file. Also copy the template called 'ListForm' from DefaultTemplates.ascx and insert it in your file. Set the ID of the RenderingTemplate to the name you entered in the template attribute above (MyCustomForm in my example).

WARNING: Visual Studio by default adds an ID attribute to all server controls. You need to remove all ID's in your template that were not present in the original 'ListForm' template, as they will break the form. Go to Tools > Options > Text Editor > HTML > Miscellaneous in Visual Studio if you want to change this default behaviour.

<%@ Control Language="C#" AutoEventWireup="false" %>
<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%>
<%@Register TagPrefix="SPHttpUtility" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.Utilities"%>
<%@ Register TagPrefix="wssuc" TagName="ToolBar" src="~/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton" src="~/_controltemplates/ToolBarButton.ascx" %>
<SharePoint:RenderingTemplate ID="MyCustomForm" 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"/>
      <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>

If you now deploy the list template you will see what looks exactly like the standard SharePoint forms. Fair enough - we haven't made any changes to the template yet. But although you can't differentiate them, it will actually be your custom forms that are displayed.

How to render som fields as read-only and others as editable

The next step is to implement the logic for our custom form. We replace the control that renders the fields, the SharePoint:ListFieldIterator, with a custom control. Remember to add a @Register tag to the template file to register the custom control. In this example I implement it as a server control (no .ascx file) but you could just as well choose a user control (with .ascx file). In that case it must also be included in the CONTROLTEMPLATES folder.

<%@ Control Language="C#" AutoEventWireup="false" %>
...
<%@ Register TagPrefix="MyPrefix" Assembly="MyAssembly 4 part name" Namespace="MyNamespace" %>
...
<SharePoint:RenderingTemplate ID="MyCustomForm" runat="server">
  ...
      <SharePoint:FolderFormFields runat="server"/>
      <!--<SharePoint:ListFieldIterator runat="server"/>-->
      <MyPrefix:MyFieldIterator ID="MyControl" runat="server />
      <SharePoint:ApprovalStatus runat="server"/>
  ...
</SharePoint:RenderingTemplate>

Now, we want to control which fields are shown as read-only, as editable, or as hidden altogether. What we don't want to do is reinvent the rendering of the different SharePoint field types. Therefore we use the ListFieldIterator control to render the fields. This is what SharePoint does, so the fields will be rendered in the proper SharePoint way.

Two things to know about the ListFieldIterator:

  • The iterator will by default display all the fields that the user should see, according to the field definitions in the list. This can be overridden by setting the ExcludeFields property to the internal name(s) of the field(s) that should not be displayed (separate multiple names with ';#').
  • The iterator will determine the rendering mode (display, new or edit) from the context. This can be overridden by setting the ControlMode property.

What we are going to do is create a ListFieldIterator instance for each user group. Each instance excludes the fields that the group does not 'own'. We then set the ControlMode of each instance according to which group the current user belongs to.

using System.Web.UI;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace MyNamespace
{
  /// <summary>
  /// My implementation of the field rendering control.
  /// </summary>
  public class MyFieldIterator : UserControl
  {
    /// <summary>
    /// Creates the ListFieldIterator instances and sets their properties.
    /// </summary>
    protected override void CreateChildControls()
    {
      // Create ListFieldIterator instances,
      // and set their ExcludeFields.
      var designerIterator = new ListFieldIterator()
      {
        ControlMode = SPControlMode.Display,
        ExcludeFields = "ProductionCost;#RetailPrice;#ToProduction"
      };
      var productManagerIterator = new ListFieldIterator()
      {
        ControlMode = SPControlMode.Display,
        ExcludeFields = "Title;#Description;#RetailPrice;#ToProduction"
      };
      var salesManagerIterator = new ListFieldIterator()
      {
        ControlMode = SPControlMode.Display,
        ExcludeFields = "Title;#Description;#ProductionCost"
      };

      // Set ControlMode depending on the current user.
      var groups = SPContext.Current.Web.SiteGroups;
      var currentMode = SPContext.Current.FormContext.FormMode;
      if (groups["My Designers"].ContainsCurrentUser)
      {
        designerIterator.ControlMode = currentMode;
      }
      if (groups["My Product Managers"].ContainsCurrentUser)
      {
        productManagerIterator.ControlMode = currentMode;
      }
      if (groups["My Sales Managers"].ContainsCurrentUser)
      {
        salesManagerIterator.ControlMode = currentMode;
      }

      // Add controls.
      this.Controls.Add(designerIterator);
      this.Controls.Add(productManagerIterator);
      this.Controls.Add(salesManagerIterator);

      base.CreateChildControls();
    }
  }
}

Deploy to the global assembly cache, login as member of one of the three groups, and add, edit and view items in the list.

More advanced forms

The above form is a simple custom form. For more advanced scenarios you might want to handle the 'save'-action differently - setting extra fields, performing advanced validation, or creating related items. This could be done using an event receiver, but you could also create your own control that inherits from SharePoint:SaveButton.

An even more advanced form might render the fields in an altogether different way, drawing on data from external sources, or handling multiple list items simoultaneously. In such scenarios you would probably replace everything inside the SharePoint:RenderingTemplate with custom controls, including toolbars, information on created/modified by, approval state and more.

5 comments:

  1. Nice!!

    I was just about to try this out, and I see you have done it already!!

    Mikael L. Mortensen

    ReplyDelete
  2. Thanks Mikael. I just hope You found it useful :)

    ReplyDelete
  3. Thanks so much for the warning about removing the ID's. Spent a whole day trying to figure out what was wrong. Luckily, I happened to come across your posting!!

    ReplyDelete
  4. Great article!
    FYI, there are awesome tools that performs this kind of custom forms and more.
    Try Kaldeera Forms. It's very powerful.

    ReplyDelete