Customizing the Rendering of a Custom SPField

I was recently asked by a customer how to use a PropertySchema field value within a RenderPatten's CAML to control how a field is rendered. The idea is to create a new instance of the field and have some method to control how that field will be rendered at the time the column is instantiated. Unfortunately field values from the PropertySchema are not available via a <Property Select='myValue'/> within a ReanderPattern's CAML and nor is it possible to call a custom getter off the SPField derived class of your custom SPField. In most cases when a field is rendered it is done so within native (non-managed) code so the getter on your .net assembly will not get called. The field schema stored in the DB is what gets read to help generate the rendered output. Fortunately this schema can be accessed by SPFIeld.SchemaXml. Adding properties or attributes to the root element of this XML will allow the RenderPattern CAML access.

So let's take a look at a sample. For my scenario I want URL field however I want to control how that field is rendered. By default a URL field just renders the URL without wrapping that URL in an <a> tag. In our example we want to create a true hyperlink so we are going to generate a RenderingPattern which will wrap an <a> tag around the field value. In addition we want the creator of the field to choose what happens when that link is selected by the user. That is, will the navigation happen within the same window or will the browser open the URL in a new window. I call these options "Self" and "New" respectively.

Now that we have our scenario let's take a look at what it will take to wire up the new field. First I need to define our field via fldTypes XML. Code 1 shows what this looks like. As you can see the Parent is "URL" because we want to use the same storage and rendering of the built-in URL however we are going to tweak it a bit. The FieldTypeClass will be outlined in Code 2. The PropertySchema field is used to collect the user's intent on how the URL will behave within the client's browser and finally we have RenderingPattern CAML which will control how the field will be rendered within the browser. Note I have a FieldSwitch on a Property called 'HowOpenUrl', this value will not be chosen from the PropertySchema's HowOpenUrl directly however once you take a look at Code 2 you will see how this is done. The reminder of the RenderPattern uses the FieldSwitch to determine how to render the URL. The default rendering is an <a> tag with the target set to _self so the URL will be navigated to within the same window.

Code 1 – fldtypes_sample.xml

<?xml version="1.0" encoding="utf-8" ?>

<FieldTypes>

    <FieldType>

        <Field Name="TypeName">ConfigurableURL</Field>

        <Field Name="ParentType">URL</Field>

        <Field Name="TypeDisplayName">Configurable URL</Field>

        <Field Name="TypeShortDescription">Configurable URL</Field>

        <Field Name="UserCreatable">TRUE</Field>

        <Field Name="ShowInListCreate">TRUE</Field>

        <Field Name="ShowInSurveyCreate">TRUE</Field>

        <Field Name="ShowInDocumentLibraryCreate">TRUE</Field>

        <Field Name="ShowInColumnTemplateCreate">TRUE</Field>

        <Field Name="FieldTypeClass">SampleConfigurableField.ConfigurableUrlField, SampleConfigurableField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3de2414d286dff3d</Field>

        <PropertySchema>

            <Fields>

                <Field Name="HowOpenUrl" DisplayName="Choose how to open the URL ('Self' = within same window, 'New' = New Window" Type="Text">

                    <Default>Self</Default>

                </Field>

            </Fields>

        </PropertySchema>

        <RenderPattern Name="DisplayPattern">

            <FieldSwitch>

                <Expr>

                    <Property Select='HowOpenUrl'/>

                </Expr>

                

                <Case Value="Self">

                    <HTML><![CDATA[<a target="_self" href="]]></HTML>

                    <Column HTMLEncode="TRUE" />

                    <HTML><![CDATA[">]]></HTML>

                    <Column HTMLEncode="TRUE" />

                    <HTML><![CDATA[</a>]]></HTML>

                </Case>

                <Case Value="New">

                    <HTML><![CDATA[<a target="_blank" href="]]></HTML>

                    <Column HTMLEncode="TRUE" />

                    <HTML><![CDATA[">]]></HTML>

                    <Column HTMLEncode="TRUE" />

                    <HTML><![CDATA[</a>]]></HTML>

                </Case>

                <Default>

                    <Column HTMLEncode="TRUE" />

                </Default>

            </FieldSwitch>

        </RenderPattern>

    </FieldType>

</FieldTypes>

After creating a Class Library project and adding the proper reference to Microsoft.SharePoint I added the following code to define my ConfigurableUrlField. This field derives from the SPFieldURL class which is important because we defined in Code 1 that our ParentType is "URL". Like all custom fields we need to create the two proper constructors which not only allow our type to be constructed but properly instantiates the base type. Next we need to override the OnAdded() and OnUpdated() methods which each get called whenever this field definition is added to a new site column or when the site column definition is updated, for example when someone wants to change from a rendering of this field from "Self" to "New". The magic all happens in ConfigureSchemaXml(), this is where we access the base.SchemaXml and update it with our custom property's value as configured by the user. This is the only chance we get to manipulate the schema before being stored in the DB and used to render the field. Note the name of the attribute being added/updated needs to match the name being used within the FieldSwitch: <Property Select='HowOpenUrl'/>

Code 2 – ConfigurableUrlField.cs

namespace SampleConfigurableField

{

using System;

using Microsoft.SharePoint;

using Microsoft.SharePoint.WebControls;

using System.Xml;

public class ConfigurableUrlField : SPFieldUrl

{

bool _updating = false;

public ConfigurableUrlField(SPFieldCollection fields, string fieldName)

: base(fields, fieldName) { }

public ConfigurableUrlField(SPFieldCollection fields, string typeName, string displayName)

: base(fields, typeName, displayName) { }

public override void OnUpdated()

{

//ConfigureSchemaXml() will cause the OnUpdated event to be raised,

//to keep out of a stack overflow condition we bail early when updating

if (_updating)

return;

_updating = true;

{

base.OnUpdated();

ConfigureSchemaXml();

}

_updating = false;

}

public override void OnAdded(SPAddFieldOptions op)

{

base.OnAdded(op);

ConfigureSchemaXml();

}

void ConfigureSchemaXml()

{

string howOpenUrl = (string)base.GetCustomProperty("HowOpenUrl");

howOpenUrl = AreStringsEqual("New", howOpenUrl) ? "New" : "Self";

XmlDocument doc = new XmlDocument();

doc.LoadXml(base.SchemaXml);

if (doc.FirstChild.Attributes["HowOpenUrl"] == null)

{

XmlAttribute attrib = doc.CreateAttribute("HowOpenUrl");

attrib.Value = howOpenUrl;

doc.FirstChild.Attributes.Append(attrib);

}

else

{

doc.FirstChild.Attributes["HowOpenUrl"].Value = howOpenUrl;

}

base.SchemaXml = doc.OuterXml;

}

private bool AreStringsEqual(string s1, string s2)

{

return (String.Compare(s1, s2, true) == 0);

}

public override BaseFieldControl FieldRenderingControl

{

get

{

BaseFieldControl fldControl = new UrlField();

fldControl.FieldName = InternalName;

return fldControl;

}

}

}

}

For completeness I have included the manifest.xml used for deployment of the solution. I will not go over that here since the concept is covered in so many other places.

Code 3 – manifest.xml

<?xml version="1.0" encoding="utf-8" ?>

<Solution xmlns="https://schemas.microsoft.com/sharepoint/"

SolutionId="733641DA-95D9-446E-812E-6070947171B2"

DeploymentServerType="WebFrontEnd"

ResetWebServer="TRUE">

    <Assemblies>

        <Assembly DeploymentTarget="GlobalAssemblyCache" Location="SampleConfigurableField.dll">

            <SafeControls>

                <SafeControl Namespace="SampleConfigurableField" TypeName="*" Safe="True" />

            </SafeControls>

        </Assembly>

    </Assemblies>

    <TemplateFiles>

        <TemplateFile Location="XML\fldtypes_Sample.xml"/>

    </TemplateFiles>

</Solution>

Once we have our solution added and deployed within our farm it is time to use the field. I created a test list and added a new column based on the Configurable URL field I just deployed. Note the open to set how the URL will open within the browser.

 

Now back at my list I create a new list item – note that we did not create a custom field control but rather are using the SPFieldUrl's field control for editing of this field.

Finally we can see the URL in action. Obviously an image does not do it justice but if I look at the HTML source I can find what actually got rendered:

<a target="_self" href="https://www.msn.com" href="https://www.msn.com">https://www.msn.com</a>