Recently, I was working on developing custom field types and controls for SharePoint. There is some really amazing customization capability here that I am very excited to talk about in a future article, but today I want to talk about something I know many people have seen, but (so far as I know) nobody has been able to solve without a lot of code.
I am talking about a bug that occurs when you add properties to the PropertySchema element in your custom field type definition file. The way these are supposed to work is that adding these properties will give you additional settings for your field that you can access from the code of your custom field class. This is supposed to save you a lot of work by providing a UI for properties that are based on fields that already exist in SharePoint, like text or choice.
However, the properties don't work as expected. After creating a new field (either in a list or as a site column) and saving it, the data entered is indeed stored and available to you via the GetCustomProperty function of the SPField class, but when you return to make changes to the field, it's properties are not properly set and there is garbage text in the form instead.

Now, I am not really sure what is going wrong here under the hood in SharePoint. I have read some things that speculated there was a hotfix to this problem, and at least one person has speculated that such a hotfix will be rolled into Service Pack 1, whenever that is. I would've thought that the HUGE security updates from September and October that required schema updates would have rolled this fix in, but they didn't solve this issue.
Some folks have solved this issue by writing custom property type rendering controls for their fields. There is a lot of chatter on the forums about this solution and the various shortcomings with it. However, I am not a big fan of writing a bunch of code where it should not be needed, just to address some broken functionality. I wanted to be able to use the custom property schema as intended out-of-the-box to make my custom field controls configurable without a ton of extra work.
Well, I did some digging, and though it was very difficult and took me a little over a day, I found the answer to my prayers. I know Microsoft will eventually fix this issue, but in the mean time, I'm providing source code for you to download and use in your own custom field controls so that you can get around this issue. See the link at below for the code, or keep reading to find out how it works.
Note that I haven't been able to reproduce the issue reliably on every SharePoint machine. I'll leave it to Microsoft to determine the root cause and get to the bottom of it and fix it. Basically, I think you should try every other option at your disposal first, and then try this if there is nothing left that works for you
My Approach
If you attach to the debugger in Visual Studio while you are on the FldEditEx.aspx page, you will find that your own custom field type is indeed being constructed (several times in fact) as that page goes through its own lifecycle.
So, I got to thinking, "If the data in SharePoint is OK, but the data shown on the form is bad, then it must be just a rendering issue in the page and we can probably write over it as long as we do so before the page posts the 'corrupted' data back into SharePoint." I did a little digging in the debugger and I could see that at certain points, the field did indeed have the "correct" custom properties.
So far, so good. But how to get the correct data back into the web form where it belongs, and not too soon so that SharePoint doesn't then just copy over my changes? Well, you can get the page context from HttpContext.Current and actually everything you need from there is at your disposal.
I dug into the HTML source and found the text input element for my first custom property. This was easy enough since I knew the field title that appears in its label. It had an ID property like this:
ctl00_PlaceHolderMain_OptionalSettings_ctl00_ctl00_ctl01_ctl00_ctl00_TextField
After redeploying my feature, the ID actually changed slightly. It seems some of the ID properties for placeholders, templates, and such are set dynamically.
In the debugger, if you look through the Controls collections of the page object, you can see that there is a complex hierarchy of templated controls being used to render the form. Knowing that, I was able to browse the control hierarchy for the page, looking for any control that ended in "ctl00_TextField". Then I walked up the Parent properties of all the controls and was able to determine this hierarchy.
|
ID |
Type |
|
ctl00 |
ASP._layouts_application_master |
|
ctl00 |
System.Web.UI.HtmlControls.HtmlGenericControl |
|
ctl13 |
System.Web.UI.HtmlControls.HtmlForm
We can get this deep with page.Form. |
|
PlaceHolderMain |
System.Web.UI.WebControls.ContentPlaceHolder |
|
OptionalSettings ASP |
Control of dynamic type |
|
PlaceHolderControls |
System.Web.UI.WebControls.PlaceHolder |
|
Customization |
System.Web.UI.WebControls.PlaceHolder
We can get to here using a recursive FindControl command. |
|
ctl08 |
Microsoft.SharePoint.WebControls.ListFieldIterator
We can loop through the Controls for a control of this type. |
|
ctl00 |
Microsoft.SharePoint.WebControls.TemplateContainer |
|
ctl01 |
Microsoft.SharePoint.WebControls.FormField |
|
ctl00 |
Microsoft.SharePoint.WebControls.TextField |
|
ctl00 |
Microsoft.SharePoint.WebControls.TemplateContainer |
|
TextField |
System.Web.UI.WebControls.TextBox |
What a pain! I hate trying to access properties in templates! Half the time they don't even have static ID properties, so FindControl doesn't work. That is true in the case as well, but fortunately there is a predictable pattern that we can use to find what we need. I wrote a couple simple functions to help us do recursive searches for controls and a search by type. You can find examples of these in just about a million places on the web.
public class WebControlTools {
/// <summary>
/// Used to scan for controls within a hierarchy of control containers. The
/// </summary>
/// <param name="root">The top levle control at which to begin the search</param>
/// <param name="id">The ID or ClientOD property to search for</param>
/// <param name="resolveByClientId">
/// Set to true if you want to find a control whose CLientID property
/// ends with a specific string. Useful for finding a specific control
/// buried deep inside nested templates, mostly for research.</param>
/// <returns></returns>
public static Control RecurseFindControl( Control root, string id, bool resolveByClientId ) {
if (resolveByClientId) {
if (root.ClientID.EndsWith( id ))
return root;
if (root.Controls == null)
return null;
foreach (Control subCtl in root.Controls) {
Control found = RecurseFindControl( subCtl, id, resolveByClientId );
if (found != null)
return found;
}
} else {
Control ctl = root.FindControl( id );
if (ctl != null)
return ctl;
if (root.Controls == null)
return null;
foreach (Control subCtl in root.Controls) {
Control found = RecurseFindControl( subCtl, id, resolveByClientId );
if (found != null)
return found;
}
}
return null;
}
public static Control RecurseFindControl( Control root, string id ) {
return RecurseFindControl( root, id, false );
}
/// <summary>
/// Gets the first child control that matches the specified type.
/// This is useful for finding tempalted controls when you know
/// the type but the ID is determined dynamically at run time.
/// </summary>
/// <param name="parent">The parent control to search</param>
/// <param name="controlType">The type of control to search for</param>
/// <param name="skipHowMany">An optional parameter to tell the search to skip the first x numbe rof controlls of this type that it finds, default is 0</param>
/// <returns>A control of the specified type, or null if none found</returns>
public static Control GetChildControlByType( Control parent, Type controlType, int skipHowMany ) {
int i = 0;
foreach (Control ctl in parent.Controls) {
if (ctl.GetType() == controlType) {
if (i++ >= skipHowMany)
return ctl;
}
}
return null;
}
public static Control GetChildControlByType( Control parent, Type controlType ) {
return GetChildControlByType( parent, controlType, 0 );
}
}
It might've also been possible to get the field at a point where its FieldRenderingControl was already created and set up, and then use that to bootstrap our way up the stack of parent controls to get where we needed to be, but I haven't tried that yet.
So anyway, the FldEditEx.aspx page has a Placeholder control, called Customization, that holds the dynamically rendered controls for - of all things - our custom properties. Once we can get that, we can grab the ListIterator control beneath it. Within that, the FormField controls represent the editor controls for each custom property.
/// <summary>
/// Search a Page object for the ListIterator control that acts
/// as a container for all the custom property field controls for
/// that page. This will only work on SharePoint's FldEditEx.aspx page.
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private Control GetListIteratorControl( Page page ) {
/*
Control phMain = page.Form.FindControl( "PlaceHolderMain" ); if (phMain == null) return;
Control oSettings = phMain.FindControl( "OptionalSettings" ); if (oSettings == null) return;
Control phControls = oSettings.FindControl( "PlaceHolderControls" ); if (phControls == null) return;
Control custom = oSettings.FindControl( "Customization" ); if (custom == null) return null;
*/
Control custom = WebControlTools.RecurseFindControl( page.Form, "Customization" );
if (custom == null)
return null;
ListFieldIterator listIterator = WebControlTools.GetChildControlByType( custom, typeof( ListFieldIterator ) ) as ListFieldIterator;
return listIterator;
}
We can fix them all in one fell swoop by looping through all the controls in the ListIterator. For each FormField control, we do a GetCustomProperty and then just stuff that value into the form field's Value property. Since the design of FormField (and all its derived classes) is that Value is used to set the value of the underlying primitive web controls - Voilla!
private void ReplaceCorruptedFieldValues( ListFieldIterator listIterator, SPField field ) {
if (listIterator == null)
return;
if (listIterator.ControlMode == SPControlMode.New)
return;
foreach (Control propertyTemplate in listIterator.Controls) {
// there is generally always a template for the individual field property
foreach (Control lic in propertyTemplate.Controls) {
FormField ff = lic as FormField; // this is the form field for the custom property, not the actual list item field
if (ff != null) { // filter out any LiteralControls that are floating around the form field
// get the field name and do the substitution
string fieldName = ff.FieldName;
object prop = field.GetCustomProperty( fieldName );
ff.ItemFieldValue = prop; // this may not be necessary but it hasn't bitten me yet
ff.Value = prop;
}
}
}
}
As you can see from this screen, the problem is fixed!

One last caveat, since the field gets instantiated several times during the page lifecycle, we need a way to make sure our code runs at the correct time, and that it only runs once. I do this by creating a static method called DoHookup which takes the custom field as an argument. It does all the work of getting the page object and managing the event. If the passed field has not been sent to DoHookup before, it will bind a PreRender event to the page to do the rest of the work. To make sure we track the fields we have called, I just store a generic string list in the page's Items collection and check it every time the static method is called. This works much better than using a static property, since ASP.net will run multiple pages in a single thread and you don't want field names to cross over the pages in this case.
/// <summary>
/// This class contains code to workaround an issue that prevents the properties
/// of a custom field from being properly displayed in the FldEditEx.aspx page.
/// </summary>
public class CustomFieldPropertyDisplayWorkaround {
private SPField _field = null;
private Page _page = null;
/// <summary>
/// You can call this method as many times as you want. It will only
/// create an event one time per unique field that is passed to it.
/// </summary>
/// <param name="field"></param>
/// <returns></returns>
public static object DoHookup( SPField field) {
if (HttpContext.Current == null || field == null)
return null;
Page page = HttpContext.Current.Handler as Page;
if (page == null || page.IsPostBack)
return null;
// ensure that we only attach a single pre render event for each unique field name
// TODO do something to ensure that fields trigger by other parts of the form or custom
// properties do not get added here
List<string> fieldList = GetFieldList(page);
if (!fieldList.Contains( field.InternalName )) {
object work = new CustomFieldPropertyDisplayWorkaround( page, field );
fieldList.Add( field.InternalName );
return work;
}
return null;
}
private static List<string> GetFieldList(Page page) {
List<string> fieldList = page.Items["FieldList"] as List<string>;
if (fieldList == null) {
fieldList = new List<string>();
page.Items.Add("FieldList", fieldList);
}
return fieldList;
}
internal CustomFieldPropertyDisplayWorkaround( Page page, SPField field ) {
this._page = page;
this._field = field;
_page.PreRender += new EventHandler( page_PreRender );
} // SPField field
protected void page_PreRender( object sender, EventArgs e ) {
if (_field == null) return;
if (_page == null || _page.IsPostBack) return;
ListFieldIterator listIterator = GetListIteratorControl( _page ) as ListFieldIterator;
ReplaceCorruptedFieldValues( listIterator, this._field );
}
// ... other methods shown above
}
To incorporate this workaround into your own custom field classes, simply call CustomFieldPropertyDisplayWorkaround.DoHookup( this ); from the constructors of your custom field. (Note, not the field value, and not the field control - the field itself). That's all you should need to do.
Keep in mind that this solution only does the workaround on fields you created (or at least have source code for), since the out-of-the-box SharePoint fields all work as designed - it's only custom properties that are hosed. If you have custom fields with custom properties in use where the author has not provided you the source, you need to tell them about this workaround. (If you are very brave, you might try to figure out how to wire up your own code on the page to an instance of their field object using events, but I don't need to do it, so I'm not gonna.)
In case you're wondering how this will affect custom properties when they're accessed outside of a web context, it won't! If any of the controls don't line up as expected, or the HttpContext is null, the code doesn't do anything at all. Since our issue is a malfunctioning web page, we don't need the workaround anyplace but here. :-)
Download the Code
You can download the needed source code here.
References and More Information
More chit-chat about this issue is in various places in the SharePoint community: