Wednesday, April 16, 2008

Customizing SharePoint 2007 Forms Without Losing Attachments (et al.) Functionality

I recently had some time on my hands, so I decided to explore different ways of customizing SharePoint forms in an effort to find the "best" way.  It seemed to me that to truly reap the benefits of using SharePoint as a development platform, the key was to reuse as much out-of-box functionality as possible (i.e. attachments, delete logic that is sensitive to the recycle bin's state, optimistic conflict detection on updates, etc).  However, once I started digging into the options for customizing forms, I was left wanting. 

First, I tried my hand at writing custom web parts and ended up with a lot of tedious coding (compared with the classic ASP.NET designer experience).  While I did wrap up some of the rendering logic into reusable base class methods to maintain the SharePoint look and feel, I was not satisfied with the experience and continued my search.

This led me to find Jan Tielens' excellent work on the SmartPart which is essentially a web part that hosts an ASP.NET user control.  It's great for enabling an ASP.NET designer experience, but doesn't really help accomplish the goal of reusing existing SharePoint functionality (additionally it has the drawback of making it a bit clumsy for users to configure your web parts themselves if that is applicable in your scenario).

At this point, my history in using code generation for data layers going back into the 90's came to the surface, and I briefly considered implementing code templates that would inspect the SharePoint list definitions and do all the initial gruntwork of setting up a page.  But then again, I'd be in the position of essentially reimplementing or figuring out how to integrate all the out-of-the-box SharePoint goodness described above.  Not the business I wanted to be in.

Still not satisfied I found some information on using SharePoint Designer to insert a custom list form, but I (and obviously a few others) found it to be problematic since it broke the functionality for attachments (or "attachements" if you ask the Microsoft developer who wrote the JavaScript).  Also, I was well outside of the world I wanted to be in for further customizations (some pretty heinous looking XSLT and JavaScript... yeech).

And so the little bird looked on...

image

I had recently become aware of the existence of all the templates in the the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES folder, and I decided that to stand the best chance of reusing SharePoint functionality, I needed to understand how SharePoint renders its own content.

So I rolled up my sleeves, whipped out my trusty .NET Reflector and went to work.  If there is interest, I will post a follow up on the details of my exploration through SharePoint's template rendering logic.

For the rest of you who just want to customize a form and retain attachments functionality, here's the skinny.

Solution

I have a list in my site that has some test data on Test Centers which I'll be using for this example.

image

Below is a screen shot of the out-of-the-box Edit form which I want to customize.  For this example, let's assume that I want to make the Test Center Number and address components read-only, and I don't want either the phone or fax number to be displayed at all.  When I'm done, I want all the existing out-of-the-box SharePoint functionality to be preserved (Attach File, Delete Item, Spelling, optimistic Updates, created/modified by information, etc.).

image

When I put this solution together, I used Jan Tielens' Smart Templates for VS 2008 which made the process of building and deploying the web part an absolute breeze.  It creates a Setup folder that contains an installer (Setup.exe) which you'll need to run.

Once installed and deployed, activate it in the Site Collection Features page:

image

Before attempting to use the web part, you'll also need to install the CustomListForms.dll assembly (which should now be located in your web site's bin folder) into the GAC.  This can be done by dragging the file into the C:\Windows\assembly folder in Windows Explorer, or from a command prompt using the following command:

gacutil.exe /i "<assemblyFilePath>"

Next, add a new web part page to your site (I called mine "TestCenterEdit") and add an instance of the Custom List Form Web Part, which can be found in the "Custom List Forms" section.

image

If you scroll all the way to the bottom in the properties pane on the right side of the page, you'll see the "Custom List Properties" section.  Expanding that shows the following settings (yours will be blank initially):

image

The Template ID identifies the ID of the template that SharePoint is going to use to render the web part (I'll show you how to add that template shortly).  The Field List allows you define the fields (as a semicolon delimited list) that you are targeting for inclusion/exclusion on the form based on the selection in Field List Type.  The Read-only Fields setting allows you to identify any fields that should be rendered for display only.  Finally, the Control Mode value tells SharePoint whether the controls should be rendered in New, Edit or Display mode.  Basically, this setting should match the function of the form you are customizing.  Since I'm customizing my Edit form, I've set mine to "Edit".  Once you've set the values the way you want them, click Apply.

The web part will still appear blank because we still need to provide the custom template that SharePoint will use to render our custom Edit form.  Create a new file called "CustomForms.ascx" in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\CONTROLTEMPLATES folder.  SharePoint loads all the user control files in this folder when the application pool starts in IIS, enabling each "RenderingTemplate" to be referenced by ID.  Add the following markup to the new file:

<%@ Control Language="C#"   AutoEventWireup="false" %>
<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@Register TagPrefix="Custom" Assembly="CustomListForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7597e2c2cd876846" namespace="CustomListForms" %>
<%@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="CustomListForm" runat="server">
<Template>
<SPAN id='part1'>
<wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator="&nbsp;" runat="server">
<Template_RightButtons>
<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%>
<TR><SharePoint:CompositeField FieldName="Name" ControlMode="Display" runat="server"/></TR>
<Custom:CustomListFormFieldIterator 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"/>
<wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator="&nbsp;" 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're wondering how I came up with this template, open up the DefaultTemplates.ascx file  in the CONTROLTEMPLATES folder and search for the "UserListForm" template.  It's basically identical, except I switched out the <SharePoint:ListFieldIterator> element for my <Custom:CustomListFormFieldIterator>.  As an interesting learning exercise, search the file for all usages of the ListFieldIterator and see where else SharePoint uses this control.



Once the template is in place, restart IIS (or recycle the application pool) so that SharePoint will see the new template.  The last thing you need to do is associate your new web part page as the Edit form for your list.  The easiest way to do this is with SharePoint Designer (but could also be done through a feature):




  1. Connect to your site in SharePoint Designer and expand the "Lists" node. 


  2. Right-click on the tree view item for your list and select "Properties".


  3. Click on the "Supporting Files" tab.


  4. Change the "Content type specific forms" to "Item".


  5. On the "Edit item form" setting, click the "Browse..." button and locate your web part page.


  6. Click OK to apply the changes.



Here is how my dialog looks after I've configured custom forms for the "New" and "Edit" functionality:



image



Go back to your web browser and navigate to the list and edit an item.  Voilà!



image



Attach File works!



image



Before saving...



image



After saving...



image



Delete works! (And it knows whether or not the Recycle Bin is enabled)



image



Optimistic updates work!



image



Spelling works!



(You get the idea)



The template I've given here should work for most needs, so the only work needed to customize additional forms is basically to add a new web part page, add the web part and change the properties appropriately, and configure the SharePoint list to use the new form for new/edit/display functionality.



If there is interest, and time allowing, I'll post a follow up going over the details of the web part implementation and explore other possibilities beyond just rendering the usual templated SharePoint controls (i.e. inserting a map immediately after the address).  You are, after all, operating within the ASP.NET infrastructure so a lot more customization is possible.



Get the source and Setup executable here.



 



UPDATE: Based on a response I got on the TechNet thread on using SharePoint Designer for this, I added another property to the web part: FieldTemplateOverrides.  This allows you to specify a different template to use when rendering a particular field.  I changed my sample usage from above to exclude the State/Province and the ZIP/Postal Code columns, and then told the web part to use a different template for the "City" column (by setting the FieldTemplatesOverrides property to "City=CityStateZIPField".  Here's what the two templates that I added to my "CustomTemplates.ascx" file look like:



<SharePoint:RenderingTemplate ID="CityStateZIPField" runat="server">
<Template>
<TD nowrap="true" valign="top" width="190px" class="ms-formlabel">
<H3 class="ms-standardheader">
<nobr>City/State/ZIP<span class="ms-formvalidation"> *</span></nobr>
</H3>
</TD>
<TD valign="top" class="ms-formbody" width="400px">
<SharePoint:TextField runat="Server" FieldName="City" DisplaySize="15" TemplateName="TextFieldNoBr"/>, <SharePoint:TextField runat="Server" FieldName="State/Province" DisplaySize="2" TemplateName="TextFieldNoBr"/>
<SharePoint:TextField runat="Server" FieldName="ZIP/Postal Code" DisplaySize="10" TemplateName="TextFieldNoBr"/>
</TD>
</Template>
</SharePoint:RenderingTemplate>

<SharePoint:RenderingTemplate ID="TextFieldNoBr" runat="server">
<Template>
<asp:TextBox ID="TextField" MaxLength="255" runat="server"/>
</Template>
</SharePoint:RenderingTemplate>




I had to add a special template for the TextField because the out-of-the-box template appends a <br> tag at the end.  Here's how the web part renders now:



image



I didn't handle the "Display" or "Required" cases, but I could augment the property to take multiple templates for each (kind of like how Excel accepts various formats for positive and negative values), but at that point I should probably look to creating a custom UI so that it doesn't get overly convoluted to use...

28 comments:

Anonymous said...

Hi,

the idea is great.
But since now i couldn't get the webpart to work or even to be displayed.
After installing and copying the .dll, i can select this webpart to be inserted, but then get an error

"An unhandled exception has occureed.
Request for the permission type 'Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' failed."

Do you have any idea about this?

Jo

Geoff McElhanon said...

Are you sure that the assembly made it into the GAC? I got this when I didn't install it in the GAC.

If you are sure it's in the GAC (you can see it in the list when you're looking at the C:\Windows\assembly folder), then I'd check to make sure that the "SafeControls" section of SharePoint's web.config includes an entry for the CustomListForms assembly.

If neither of those work, let me know...

Unknown said...

Hi,
Thanks for posting this great tool.
However, I can not get it to show up in the list as a feature.
I ran through the setup you provided and it shows as being deployed. I have restarted the SP server and it still does not appear. It has placed all the files in the 12 directory.
Also, I am running 2008 and it will not allow me to register the dll by dragging it. It says access denied.

Thanks!

Unknown said...

OK, I resolved my issue. I was looking at the Site features and not the Site collection features.
Also, with Windows 2008 you must disable UAC.
Msconfig-Tools-Disable UAC, reboot your computer.
You can then drop the dll into the folder.

Bob said...

Hi,

I would like to use the capability described in your article, but I don't see anywhere to download anything. Is there a download avaialble?

Geoff McElhanon said...

There is a link for the source code and the setup just above the red UPDATE towards the bottom of the post.

spmikey said...

Hi Geoff,

Thanks for a great solution.

Unfortunately I am unable to deploy your .ascx code (suitably modified) as a separate .ascx file in SharePoint’s CONTROLTEMPLATES folder. The LOG file shows the following error:

Load control template file /_controltemplates/CustomEditForm.ascx failed: There can be only one 'control' directive.

It works fine when the code is pasted into DefaultTemplates.ascx, but this is obviously unsatisfactory.

Do you have any notion as to what [There can be only one 'control' directive] actually means?

Geoff McElhanon said...

Each ascx file contains a <%@ Control ... > directive (just as each ASP.NET page contains a <%@ Page ... > directive).

Are you sure you don't accidentally have two <%@ control ... > tags in your control (ascx) file?

Geoff McElhanon said...

As I thought about this a minute longer, I realized that you may have added multiple templates to your control file, but copied all the directives with each one?

Make sure that you only have the directives once (the tags that look like <%@ Xxxxx ... <). For each new template you want to add to the control file, you simply need to wrap it in a RenderingTemplate element, like so:

<SharePoint:RenderingTemplate ID="CityStateZIPField" runat="server">
...STUFF...
</SharePoint:RenderingTemplate>

Unknown said...

hey man,
great work...
really a dream came true that helps alot...
i followed your points, but how do u associate the list with the webpart? i cnt seem to have it display my custom list and edit the view i need..
i think am missing something here..

Geoff McElhanon said...

@tamer

You need to create a new web part page, add the web part to the page, and then use SharePoint Designer to associate the page to the list for one of the operations (New, Edit, Display). It's covered in the middle of the post and there is a screen shot of what the SharePoint Designer dialog looks like...

If you don't have SharePoint Designer, you can do it with a "feature" but I don't remember the details of that off hand.

JohnJay said...
This comment has been removed by the author.
Erik Burger said...

Hi,

Creating a custom template and loading it into a webpart using SPControlTemplateManager works great. Thanks for that!

What I would like to do now is to access controls within the custom template in order to change some properties (more specifically, the Url for the FormToolBar buttons. Would you have any idea how to go about this? I reckon I'll need to create a UserControl and hook the RenderingTemplate into it but I cannot get it to work.

Any ideas would be very welcome.

Regards,
Erik

Anonymous said...

The Webpart seems to work sometimes, sometimes it blocks access to display and new actions but displays the edit action correctly. When inserting the rendering template into the properties, the web part renders the following error:

The following exception occurred while trying to initialize the rendering template:
System.FormatException: Die Eingabezeichenfolge hat das falsche Format.
bei System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
bei System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
bei System.Convert.ToInt32(String value, IFormatProvider provider)
bei Microsoft.SharePoint.WebControls.ItemHiddenVersion.OnLoad(EventArgs e)
bei System.Web.UI.Control.LoadRecursive()
bei System.Web.UI.Control.LoadRecursive()
bei System.Web.UI.Control.LoadRecursive()
bei System.Web.UI.Control.AddedControl(Control control, Int32 index)
bei System.Web.UI.ControlCollection.Add(Control child)
bei
CustomListForms.CustomListFormWebPart.CreateChildControls()

"Die Eingabezeichenfolge hat das falsche Format." -> The input string has the wrong format.

Also, the Webpart looks normal after publishing the page, but won´t display list data. Insted the server renders an error that the item is not available and that it was possibly deleted or renamed by an other user.

What could be the reason for this behaviour?

Bob said...

Hi, I am trying to implement the custom list. Everything went according to plan until I added the web part to the page. Immediately after selecting it and clicking Ok, I get this error:

Error
An unexpected error has occurred.

As far as I can see, everything is as it should be. I intalled the dll in the GAC and verified that it's there. I also checked the "SafeControls" section of web.config and it's there too. I'm running this on a VMWare guest with Windows 2008. The host OS is Vista 64. Any ideas would be gratefully appreciated as I'm supposed to have this running my Sept 1.

Geoff McElhanon said...

I don't know what your problem could be, but I would look at the following post on how to get more information about what the real problem is:

http://www.sharepointblogs.com/rhulsman/archive/2007/01/22/get-more-error-information-when-quot-an-unexpected-error-has-occurred-quot.aspx

Bob said...

Thanks for your prompt reply Geoff. My "unexpected error" resolved itself after about an hour. I think there was some internal refresh action happening. Anyway, I was trying to solve the problem of not only which fields to include on the form, but what order they should appear in. I discovered that with a very minor tweak to your code I could also solve this problem. I have included a code snippet below, but the basic solution is that if we are "including" fields, they are displayed in the order entered in the list. Hope this helps someone else.

Bob

if (listIncludesFields)
{
for (int i = 0; i < fieldList.Count; i++)
{
try // protect against a bad field entry
{
SPField field = Fields[fieldList[i]];
TemplateContainer child = new TemplateContainer();
Controls.Add(child);

SetFieldName(child, field.InternalName);
SetControlMode(child, IsFieldReadOnly(field) ? SPControlMode.Display : ControlMode);

ControlTemplate.InstantiateIn(child);
}
catch (Exception)
{
}
}
}
else

Erik Burger said...

Hi Geoff,

Have you ever tried to install one of the language packs with your solution? We have created a custom list form which works perfectly fine in English but after installing the Dutch or French language pack it blows up with a NullReference on CompositeField. Commenting out both the CompositeField line and the ListFieldIterator (which uses the CompositeField) makes the exception go away, but naturally we'd very much like to be able to keep using the ListFieldIterator :)

Can you shed some light??

Kind Regards,
Erik

Geoff McElhanon said...

@novelist,

I have not tried installing other language packs, and cannot begin to guess what might be the cause of the problem. Honestly, I've been out of the SharePoint space for about 4 months now and my memory of the details is getting a little foggy.

I wonder if the new language packs are using a different folder for language-specific templates that override the default ones and that has something to do with the null CompositeField? Without rolling up the sleeves and digging in myself, I really don't know what else to suggest.

Mehul K Bhuva said...

Hi Geoff,

Great post. I have read it all.

I wish to edit the Display form.aspx of my site collection, i wish to show my users only the data in the title & description columns (without the column names), it means that i have to do away with all the mark-up that the default DispForm.aspx emits in a tabular format, how do i achieve it, can you help me out...

Geoff McElhanon said...

Ok Mehul... I understand now and I've looked it to refresh my memory. It sounds to me like you'll need to decide just where you want to branch off the out-of-the-box templates for rendering. The CustomListForm template that I put together swaps out the ListFieldIterator with my CustomListFieldIterator control. However, mine it still wrapped by a TABLE and I use the templating infrastucture to render TRs and TDs.

If you don't want the table at all, you'll need modify the rendering at a higher level template (i.e. the ListForm level). If you look at the Update to the post on the CustomListForm (near the bottom of the post), you can see how I redirected to non-default rendering templates by explicitly identifying them in the attributes like so:

<SharePoint:TextField runat="Server" FieldName="City" DisplaySize="15" TemplateName="TextFieldNoBr"/>

Using that attribute, I was able to change which template was used to render the field to something I controlled. There would be nothing stopping from you from providing a chain of custom templates and rendering your own content for this particular display form outside of a table.

Does this help?

Unknown said...

Great work. I just have one problem...when rendering my custom edit.aspx form, the text is larger than it should be and i get labels over top the field textboxes. not sure how i could have messed up any style sheets, hopefully you have seen this before and can point me in the right direction.

Gunjan said...

Great Post. I've been searching around for an example like this and finally found one.

I'm new to sharepoint, and need to create a sub-site (new site collection) for clients to login to and view only "list items" for that specific client.

I've been able to create a custom .aspx page containing a listfielditerator to display the form as needed. I can't seem to implement this in a webpart though. Any suggestions as to how to complete this or info on an easier method would be very appreciated.

Thanks.

Geoff McElhanon said...

@Gunjan... I'm really glad you found the post helpful. I wrote that about a month before I left a job where I was working in SharePoint regularily. I actually haven't touched the platform since about May 2008, and so my memory of the details of surrounding issues related to this post have faded.

I do remember at the SmartPart for SharePoint on Codeplex (http://www.codeplex.com/smartpart) which provides a wrapper around webparts to give you a more classic ASP.NET dev experience. I don't know whether or not I'd recommend that approach over a purely code-based one (for web part development), but you might look into it and see it might be what you're looking for.

I'm afraid I don't have any more helpful information to offer you...

Gunjan said...

I'll check it out. Thanks for getting back so fast.

BK-25 said...

Very Informative blog thank you for sharing. Keep sharing.

Best software training institute in Chennai. Make your career development the best by learning software courses.

best informatica training in chennai
android training in chennai
power bi training in chennai
Docker Training in Chennai
ios training in chennai
Xamarin Training in Chennai
msbi training in chennai

Block said...
This comment has been removed by the author.
Unknown said...



Nice blog thank you .For your Sharing It's a pleasure to read your post.It's full of information I'm looking for and I'd like to express that "The content of your post is awesome"C#.NET Training In Chennai
hadoop training in chennai
software testing training in chennai