Extending the TaxonomyField (SharePoint 2010)

Hello! Today I’ll try to cover the way how I implemented the extension of TaxonomyField in SharePoint 2010. (Not tested out with version 2013)

What makes it different from extending the other OOTB SharePoint fields – it’s class is sealed, so we don’t get to extend it. Instead, I’m instantiating it inside my custom class, and filling it with parameters. The functionality in my field is pre-filling the field with values in NewForm.aspx based on some properties in current user’s user profile.

This said, let’s see what absolute minimum steps we will need:
1. Empty SharePoint 2010 project;
2. Create mapped folders to 14 hive to these folders: XML, CONTROLTEMPLATES;
3. Create fldtypes_XXX.xml inside XML folder, where XXX is the name we will give. In my example I’m using PreDefinedTaxonomyField. Inside this file we’ll declare the custom field;
4. Create two ASCX (User Control) files inside the CONTROLTEMPLATES folder. One for properties section of custom field, another for declaring the rendering template;
5. Create code classes for field declaration (derive from SPField) and for field rendering (can derive from BaseFieldControl)

OK, so steps 1 and 2 are fairly straightforward, so I’ll not cover them here.
Let’s see the contents of 3. fldtypes_PreDefinedTaxonomyField.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<FieldTypes>
  <FieldType>
    <Field Name=“TypeName”>PreDefinedTaxonomyField</Field>
    <Field Name=“ParentType”>Note</Field>
    <Field Name=“TypeDisplayName”>Pre-Defined Taxonomy Field</Field>
    <Field Name=“UserCreatable”>TRUE</Field>
    <Field Name=“ShowInListCreate”>TRUE</Field>
    <Field Name=“FieldTypeClass”>CustomFields.PreDefinedTaxonomyField.PreDefinedTaxonomyField,$SharePoint.Project.AssemblyFullName$</Field>
    <Field Name=“FieldEditorUserControl”>/_controltemplates/PreDefinedTaxonomyFieldEditorControl.ascx</Field>
    <Field Name=“RichText”>FALSE</Field>
    <Field Name=“RichTextMode”>Text</Field>
    <Field Name=“AllowBaseTypeRendering”>TRUE</Field>
    <Field Name=“CAMLRendering”>TRUE</Field>
    <PropertySchema>
      <Fields>
        <Field Name=“UserProfileProperty” DisplayName=“UserProfileProperty” Type=“Text” Hidden=“TRUE” />
        <Field Name=“TermStoreId” DisplayName=“TermStoreId” Type=“Text” Hidden=“TRUE” />
        <Field Name=“TermSetId” DisplayName=“UtuTermSetId” Type=“Text” Hidden=“TRUE” />
        <Field Name=“AllowMultipleValues” DisplayName=“AllowMultipleValues” Type=“Text” Hidden=“TRUE” />
        <Field Name=“AllowFillInValues” DisplayName=“AllowFillInValues” Type=“Text” Hidden=“TRUE” />
      </Fields>
    </PropertySchema>
  </FieldType>
</FieldTypes>

Firstly, ParentType is note, that’s because I want to store multiple values inside of a text field, and ParentType Text would have a pretty small character length limit, so Note is preferred. Another thing to mention, I’m using PropertySchema element to define my custom properties, as we’d do with SharePoint 2007, and later, in my code which inherits from SPField I’ll reference them in the SharePoint 2007 way (with Thread.GetData and Thread.GetNamedDataSlot methods).

Next, let’s cover the class that defines the field itself (the one which derives from SPField class). In my case it’s called PreDefinedTaxonomyField: (Short code explanation is below the code block)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
using System.Threading;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web;

namespace CustomFields.PreDefinedTaxonomyField
{
    /// <summary>
    /// Class responsible for current custom field representation in the SharePoint UI
    /// </summary>
    public class PreDefinedTaxonomyField : SPFieldMultiLineText
    {
        #region Contructors
        /// <summary>
        /// Constructor accepting two parameters
        /// </summary>
        /// <param name=”fields”>Field collection</param>
        /// <param name=”fieldName”>Field internal name</param>
        public PreDefinedTaxonomyField(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName)
        {
            this.Init();
        }

        /// <summary>
        /// Constructor accepting three parameters
        /// </summary>
        /// <param name=”fields”>Field collection</param>
        /// <param name=”typeName”>Field internal name</param>
        /// <param name=”displayName”>Field display name</param>
        public PreDefinedTaxonomyField(SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName)
        {
            this.Init();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Private member corresponding to UserProfileProperty
        /// </summary>
        private string UserProfileProperty;
        /// <summary>
        /// Used to host the value of UserProfileProperty
        /// </summary>
        public string UserProfileProperty
        {
            get
            {
                if (string.IsNullOrEmpty(this.UserProfileProperty))
                {
                    this.UserProfileProperty = GetFieldThreadDataValue(“UserProfileProperty”false);
                }
                return this.UserProfileProperty;
            }
            set
            {
                this.UserProfileProperty = value;
                SetFieldThreadDataValue(“UserProfileProperty”, value);
                UpdateFieldProperties();
            }
        }

        /// <summary>
        /// Private member corresponding to TermSetId
        /// </summary>
        private string TermSetId;
        /// <summary>
        /// Used to host the value of TermSetId
        /// </summary>
        public string TermSetId
        {
            get
            {
                if (string.IsNullOrEmpty(this.TermSetId))
                {
                    this.TermSetId = GetFieldThreadDataValue(“TermSetId”false);
                }
                return this.TermSetId;
            }
            set
            {
                this.TermSetId = value;
                SetFieldThreadDataValue(“TermSetId”, value);
                UpdateFieldProperties();
            }
        }

        /// <summary>
        /// Private member corresponding to TermStoreId
        /// </summary>
        private string TermStoreId;
        /// <summary>
        /// Used to host the value of TermStoreId
        /// </summary>
        public string TermStoreId
        {
            get
            {
                if (string.IsNullOrEmpty(this.TermStoreId))
                {
                    this.TermStoreId = GetFieldThreadDataValue(“TermStoreId”false);
                }
                return this.TermStoreId;
            }
            set
            {
                this.TermStoreId = value;
                SetFieldThreadDataValue(“TermStoreId”, value);
                UpdateFieldProperties();
            }
        }

        /// <summary>
        /// Private member corresponding to AllowMultipleValues
        /// </summary>
        private string AllowMultipleValues;
        /// <summary>
        /// Used to host the value of AllowMultipleValues
        /// </summary>
        public string AllowMultipleValues
        {
            get
            {
                if (string.IsNullOrEmpty(this.AllowMultipleValues))
                {
                    this.AllowMultipleValues = GetFieldThreadDataValue(“AllowMultipleValues”false);
                }
                return this.AllowMultipleValues;
            }
            set
            {
                this.AllowMultipleValues = value;
                SetFieldThreadDataValue(“AllowMultipleValues”, value);
                UpdateFieldProperties();
            }
        }

        /// <summary>
        /// Private member corresponding to AllowFillInValues
        /// </summary>
        private string AllowFillInValues;
        /// <summary>
        /// Used to host the value of AllowFillInValues
        /// </summary>
        public string AllowFillInValues
        {
            get
            {
                if (string.IsNullOrEmpty(this.AllowFillInValues))
                {
                    this.AllowFillInValues = GetFieldThreadDataValue(“AllowFillInValues”false);
                }
                return this.AllowFillInValues;
            }
            set
            {
                this.AllowFillInValues = value;
                SetFieldThreadDataValue(“AllowFillInValues”, value);
                UpdateFieldProperties();
            }
        }

        /// <summary>
        /// Gets the corresponding field rendering control
        /// </summary>
        public override BaseFieldControl FieldRenderingControl
        {
            get
            {
                BaseFieldControl control = new PreDefinedTaxonomyFieldControl();
                control.FieldName = this.InternalName;
                return control;
            }
        }
        #endregion

        #region FieldThreadDataValue methods
        /// <summary>
        /// Part of method group responsible for workaround for a SP bug with custom property persisting
        /// Tries to get the value of the custom property
        /// </summary>
        /// <param name=”propertyName”></param>
        /// <param name=”ignoreEmptyValue”></param>
        /// <returns></returns>
        private string GetFieldThreadDataValue(string propertyName, bool ignoreEmptyValue)
        {
            string _d = (string)Thread.GetData(Thread.GetNamedDataSlot(propertyName));
            if (string.IsNullOrEmpty(_d) && !ignoreEmptyValue)
            {
                _d = (string)base.GetCustomProperty(propertyName);
            }
            return _d;
        }

        /// <summary>
        /// Part of method group responsible for workaround for a SP bug with custom property persisting
        /// Sets one of thread data slots with value
        /// </summary>
        /// <param name=”propertyName”></param>
        /// <param name=”value”></param>
        private void SetFieldThreadDataValue(string propertyName, string value)
        {
            Thread.SetData(Thread.GetNamedDataSlot(propertyName), value);
        }

        /// <summary>
        /// Part of method group responsible for workaround for a SP bug with custom property persisting
        /// Cleans up thread data slots
        /// </summary>
        private void CleanUpThreadData()
        {
            Thread.FreeNamedDataSlot(“UserProfileProperty”);
            Thread.FreeNamedDataSlot(“TermStoreId”);
            Thread.FreeNamedDataSlot(“TermSetId”);
            Thread.FreeNamedDataSlot(“AllowMultipleValues”);
            Thread.FreeNamedDataSlot(“AllowFillInValues”);
        }
        #endregion

        #region UpdateFieldProperties method
        /// <summary>
        /// Updates custom properties of the current custom
        /// </summary>
        private void UpdateFieldProperties()
        {
            string sUserProfileProperty = this.UserProfileProperty;
            base.SetCustomProperty(“UserProfileProperty”, sUserProfileProperty);
            string sTermSetId = this.TermSetId;
            base.SetCustomProperty(“TermSetId”, sTermSetId);
            string sTermStoreId = this.TermStoreId;
            base.SetCustomProperty(“TermStoreId”, sTermStoreId);
            string sAllowMultipleValues = this.AllowMultipleValues;
            base.SetCustomProperty(“AllowMultipleValues”, sAllowMultipleValues);
            string sAllowFillInValues = this.AllowFillInValues;
            base.SetCustomProperty(“AllowFillInValues”, sAllowFillInValues);

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(base.SchemaXml);

            UpdateSchemaXmlAttribute(xmlDoc, “UserProfileProperty”, sUserProfileProperty);
            UpdateSchemaXmlAttribute(xmlDoc, “TermStoreId”, sTermSetId);
            UpdateSchemaXmlAttribute(xmlDoc, “TermSetId”, sTermStoreId);
            UpdateSchemaXmlAttribute(xmlDoc, “AllowMultipleValues”, sAllowMultipleValues);
            UpdateSchemaXmlAttribute(xmlDoc, “AllowFillInValues”, sAllowFillInValues);
            base.SchemaXml = xmlDoc.OuterXml;
        }

        #region SchemaXML functionality
        /// <summary>
        /// Updates the SchemaXml of the current custom field
        /// </summary>
        /// <param name=”doc”></param>
        /// <param name=”name”></param>
        /// <param name=”value”></param>
        private void UpdateSchemaXmlAttribute(XmlDocument doc, string name, string value)
        {
            XmlAttribute attribute = doc.DocumentElement.Attributes[name];
            if (attribute == null)
            {
                attribute = doc.CreateAttribute(name);
                doc.DocumentElement.Attributes.Append(attribute);
            }
            doc.DocumentElement.Attributes[name].Value = value;
        }
        #endregion
        #endregion

        /// <summary>
        /// Performs the property initializing logic
        /// </summary>
        private void Init()
        {
            this.UserProfileProperty = (string)this.GetCustomProperty(“UserProfileProperty”);
            this.TermStoreId = (string)this.GetCustomProperty(“TermStoreId”);
            this.TermSetId = (string)this.GetCustomProperty(“TermSetId”);
            this.AllowMultipleValues = (string)this.GetCustomProperty(“AllowMultipleValues”);
            this.AllowFillInValues = (string)this.GetCustomProperty(“AllowFillInValues”);
        }

        /// <summary>
        /// Transforms the value of the field into readable string
        /// </summary>
        /// <param name=”value”></param>
        /// <returns></returns>
        public override string GetFieldValueAsHtml(object value)
        {
            return FormatValueForViewing(value as string);
        }

        /// <summary>
        /// Transforms the value of the field into readable string
        /// </summary>
        /// <param name=”value”></param>
        /// <returns></returns>
        public override string GetFieldValueAsText(object value)
        {
            return FormatValueForViewing(value as string);
        }

        /// <summary>
        /// Transforms the value of the field into readable string
        /// </summary>
        /// <param name=”value”></param>
        /// <returns></returns>
        private string FormatValueForViewing(string value)
        {
            string temp = HttpUtility.HtmlDecode(value);
            XmlDocument xdoc = new XmlDocument();
            try
            {
                xdoc.LoadXml(temp);
                temp = xdoc.InnerText;
            }
            catch
            {
            }

            string[] splitTerms = temp.Split(new char[] { ‘;’ }System.StringSplitOptions.RemoveEmptyEntries);
            string result = string.Empty;

            foreach (string splitTerm in splitTerms)
            {
                result += splitTerm.Split(new char[] { ‘|’ }System.StringSplitOptions.None)[0] + “; “;
            }
            return result;
        }

        /// <summary>
        /// Performs the update logic
        /// </summary>
        public override void Update()
        {
            UpdateFieldProperties();
            base.Update();
            CleanUpThreadData();
        }
    }
}

Basically, I’m using Thread data slots mechanism to store my custom values, hence all the FieldThreadDataValue and SchemaXML region methods. Other than that it’s an ordinary SPField declaration, which we will use later in FieldEditorControl (for settings/properties setting via SharePoint UI) and in FieldBaseControl (for field rendering in forms)

Moving on, let’s see the 4. CONTROLTEMPLATES folder. Firstly, the FieldEditorControl. It’s used to display field’s settings/properties when creating or editing a field from SharePoint UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<%@ Control Language=“C#” AutoEventWireup=“true” CodeBehind=“PreDefinedTaxonomyFieldEditorControl.ascx.cs”
    Inherits=“CustomFields.PreDefinedTaxonomyField.PreDefinedTaxonomyFieldEditorControl, $SharePoint.Project.AssemblyFullName$” %>
<%@ Assembly Name=“$SharePoint.Project.AssemblyFullName$” %>
<%@ Register TagPrefix=“Utilities” Namespace=“Microsoft.SharePoint.Utilities” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register TagPrefix=“asp” Namespace=“System.Web.UI” Assembly=“System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35” %>
<%@ Import Namespace=“Microsoft.SharePoint” %>
<%@ Register TagPrefix=“wssuc” TagName=“InputFormControl” Src=“InputFormControl.ascx” %>
<%@ Register TagPrefix=“wssuc” TagName=“InputFormSection” Src=“InputFormSection.ascx” %>
<%@ Assembly Name=“Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register TagPrefix=“SharePoint” Namespace=“Microsoft.SharePoint.WebControls” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register TagPrefix=“Taxonomy” Namespace=“Microsoft.SharePoint.Taxonomy” Assembly=“Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”%>
<%@ Register TagPrefix=“Osrv” Namespace=“Microsoft.Office.Server.WebControls” Assembly=“Microsoft.Office.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register TagPrefix=“wssawc” Namespace=“Microsoft.SharePoint.WebControls” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<wssuc:InputFormSection runat=”server” id=”UTUSection” Title=”<%$Resources:UTUCustomFields, lblCustomParameters%>“>
    <template_inputformcontrols>
             <wssuc:InputFormControl runat=”server”>
                    <Template_Control>
                           <table>
                                <tr>
                                    <td>
                                        <asp:Literal ID=”lblTermSet” runat=”server” Text=”<%$Resources:CustomFields, lblTermSet%>” />:
                                    </td>
                                    <td>
                                        <asp:DropDownList ID=”ddlTermSet” runat=”server” />
                                    </td>
                                </tr>
                                 <tr>
                                    <td><asp:Literal ID=”lblUserProfileProperty” runat=”server” Text=”<%$Resources:CustomFields, lblUserProfileProperty%>” />:</td>
                                    <td>
                                        <asp:TextBox ID=”tbUserProfileProperty” runat=”server” CssClass=”ms-input” />
                                    </td>
                                </tr>
                                <tr>
                                    <td><asp:Literal ID=”lblAllowMultipleValues” runat=”server” Text=”<%$Resources:CustomFields, lblAllowMultipleValues%>” />:</td>
                                    <td>
                                        <asp:CheckBox ID=”cbAllowMultipleValues” CssClass=”ms-input” runat=”server” />
                                    </td>
                                </tr>
                                <tr>
                                    <td><asp:Literal ID=”lblAllowFillInValues” runat=”server” Text=”<%$Resources:CustomFields, lblAllowFillInValues%>” />:</td>
                                    <td>
                                        <asp:CheckBox ID=”cbAllowFillInValues” CssClass=”ms-input” runat=”server” />
                                    </td>
                                </tr>
                             </table>
                    </Template_Control>
             </wssuc:InputFormControl>
       </template_inputformcontrols>
</wssuc:InputFormSection>

This control has both ASP markup and code-behind. In the markup we see it’s pretty simple – a dropdown box for listing available term sets, a textbox where user will input the internal name of the user profile property (based on the value of which the field will pre-fill itself in NewForm.aspx) and checkboxes for toggling filling-in terms / allowing multiple terms.

The code-behind section goes like this (some methods are omitted):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Taxonomy;
using Microsoft.SharePoint.WebControls;

namespace CustomFields.PreDefinedTaxonomyField
{
    public partial class PreDefinedTaxonomyFieldEditorControl : UserControl, IFieldEditor
    {
        private TaxonomySession session = null;

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            session = new TaxonomySession(SPContext.Current.Site);
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            this.cbAllowMultipleValues.Style.Add(HtmlTextWriterStyle.Padding“0px”);
            if (!Page.IsPostBack)
            {
                PopulateTermSetDropDown(this.sessionthis.ddlTermSet);
            }
        }

        public bool DisplayAsNewSection
        {
            get { return true; }
        }

        public void InitializeWithField(SPField field)
        {
            if (field == null)
            {
                return;
            }
            else
            {
                if (!Page.IsPostBack)
                {
                    EnsureChildControls();
                    PreDefinedTaxonomyField customField = (PreDefinedTaxonomyField)field;

                    // Init with values
                    if (!string.IsNullOrEmpty(customField.TermSetId))
                    {
                        this.ddlTermSet.ClearSelection();
                        try
                        {
                            this.ddlTermSet.Items.FindByValue(customField.TermSetId).Selected = true;
                        }
                        catch
                        { }
                    }
                    if (!string.IsNullOrEmpty(customField.UserProfileProperty))
                    {
                        this.tbUserProfileProperty.Text = customField.UserProfileProperty;
                    }
                    this.cbAllowMultipleValues.Checked = false;
                    if (!string.IsNullOrEmpty(customField.AllowMultipleValues))
                    {
                        this.cbAllowMultipleValues.Checked = customField.AllowMultipleValues == “1” ? true : false;
                    }

                    this.cbAllowFillInValues.Checked = false;
                    if (!string.IsNullOrEmpty(customField.AllowFillInValues))
                    {
                        this.cbAllowFillInValues.Checked = customField.AllowFillInValues == “1” ? true : false;
                    }
                }
            }
        }

        public void OnSaveChange(SPField field, bool isNewField)
        {
            PreDefinedTaxonomyField customField = (PreDefinedTaxonomyField)field;

            Guid termSetId = Guid.Empty;
            if (this.ddlTermSet.SelectedValue != null && this.ddlTermSet.SelectedValue != string.Empty)
            {
                termSetId = new Guid(this.ddlTermSet.SelectedValue);
            }
            TermSet currentTermSet = GetTermSetById(this.session, termSetId);
            if (currentTermSet != null)
            {
                customField.UtuTermStoreId = currentTermSet.TermStore.Id.ToString();
                customField.UtuTermSetId = currentTermSet.Id.ToString();
            }
            
            customField.UserProfileProperty = this.tbUserProfileProperty.Text;
            customField.AllowMultipleValues = this.cbAllowMultipleValues.Checked ? “1” : “0”;
            customField.AllowFillInValues = this.cbAllowFillInValues.Checked ? “1” : “0”;
        }
        

        /// <summary>
        /// Performs the update logic
        /// </summary>
        public override void Update()
        {
            UpdateFieldProperties();
            base.Update();
            CleanUpThreadData();
        }
    }
}

What this code does is
A. Displays current user control as a separate section in the field settings/properties while in SharePoint UI
B. On load populates the dropdown with the available term sets (omitted in code snippet)
C. If the field has already been created – populates UI elements with the value field already has
D. On OK button press saves the values

Finally, we need to cover the template and class (which is derived from BaseFieldControl) which are used to render the field. Let’s start from the ASCX template. This particular ASCX file should not have code behind, as there’s another method how SharePoint is binding RenderingTemplate markup to our code files. More on that after the code block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ Assembly Name=“$SharePoint.Project.AssemblyFullName$” %>
<%@ Assembly Name=“Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register Tagprefix=“SharePoint” Namespace=“Microsoft.SharePoint.WebControls” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register Tagprefix=“Utilities” Namespace=“Microsoft.SharePoint.Utilities” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register Tagprefix=“asp” Namespace=“System.Web.UI” Assembly=“System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35” %>
<%@ Import Namespace=“Microsoft.SharePoint” %> 
<%@ Register Tagprefix=“WebPartPages” Namespace=“Microsoft.SharePoint.WebPartPages” Assembly=“Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Register Tagprefix=“Taxonomy” Namespace=“Microsoft.SharePoint.Taxonomy” Assembly=“Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>
<%@ Control Language=“C#” AutoEventWireup=“true” %>

<SharePoint:RenderingTemplate ID=”PreDefinedTaxonomyFieldRenderingTmpl” runat=”server” EnableViewState=”true”>
<Template>
     <Taxonomy:TaxonomyWebTaggingControl runat=”server” ID=”taxonomyWebControl” />
     <asp:PlaceHolder runat=”server” ID=”phMain” />
</Template>
</SharePoint:RenderingTemplate>

Notice the PreDefinedTaxonomyFieldRenderingTmpl ID parameter of SharePoint:RenderingTemplate element. This is the key thing in binding together this template and code file which defines rendering of the field.
Let’s look at the binding from code side:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Taxonomy;
using Microsoft.SharePoint.WebControls;

namespace CustomFields.PreDefinedTaxonomyField
{
    /// <summary>
    /// Control, responsible for rendering this custom field to forms
    /// </summary>
    public class PreDefinedTaxonomyFieldControl : BaseFieldControl
    {
        /// <summary>
        /// Represents a Managed Metadata SharePoint control
        /// </summary>
        private TaxonomyWebTaggingControl taxonomyWebControl = null;

        /// <summary>
        /// Overrides the default templates
        /// </summary>
        protected override string DefaultTemplateName
        {
            get
            {
                return “PreDefinedTaxonomyFieldRenderingTmpl”;
            }
        }

        /// <summary>
        /// Overrides the display template
        /// </summary>
        public override string DisplayTemplateName
        {
            get
            {
                return “PreDefinedTaxonomyFieldRenderingTmpl”;
            }
            set
            {
                base.DisplayTemplateName = value;
            }
        }

        /// <summary>
        /// A reference to current field instance
        /// </summary>
        protected PreDefinedTaxonomyField MyField
        {
            get
            {
                return Field as PreDefinedTaxonomyField;
            }
        }

        /// <summary>
        /// Specifies if current field instance allows multiple values
        /// </summary>
        protected bool AllowMultipleValues
        {
            get
            {
                bool result = false;
                string res = this.MyField.AllowMultipleValues;
                if (!string.IsNullOrEmpty(res))
                {
                    result = res == “1” ? true : false;
                }
                return result;
            }
        }

        /// <summary>
        /// Specifies if current field instance allows fill-in values
        /// </summary>
        protected bool AllowFillInValues
        {
            get
            {
                bool result = false;
                string res = this.MyField.AllowFillInValues;
                if (!string.IsNullOrEmpty(res))
                {
                    result = res == “1” ? true : false;
                }
                return result;
            }
        }

        /// <summary>
        /// Used for setting/getting the value of custom field
        /// </summary>
        public override object Value
        {
            get
            {
                // Go through the values (this.taxonomyWebControl.Text) and add newly typed terms to termset if needed
                string parsedValue = AddNewTermsToTermSet();
                return parsedValue;
            }
            set
            {
                base.Value = value;
            }
        }

        /// <summary>
        /// Performs control creation logic
        /// </summary>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            this.taxonomyWebControl = (TaxonomyWebTaggingControl)this.TemplateContainer.FindControl(“taxonomyWebControl”);

            if (SPContext.Current.FormContext.FormMode == SPControlMode.New ||
                SPContext.Current.FormContext.FormMode == SPControlMode.Edit)
            {
                this.taxonomyWebControl.IsMulti = this.AllowMultipleValues;
                string termStoreIdString = this.MyField.TermStoreId;
                string termSetIdString = this.MyField.TermSetId;

                if (string.IsNullOrEmpty(termStoreIdString) || string.IsNullOrEmpty(termSetIdString))
                {
                    this.taxonomyWebControl.Enabled = false;
                }
                else
                {
                    Guid termStoreId = new Guid(termStoreIdString);
                    Guid termSetId = new Guid(termSetIdString);

                    this.taxonomyWebControl.SspId.Clear();
                    this.taxonomyWebControl.SspId.Add(termStoreId);
                    this.taxonomyWebControl.TermSetId.Clear();
                    this.taxonomyWebControl.TermSetId.Add(termSetId);
                    this.taxonomyWebControl.IsAddTerms = AllowFillInValues;

                    string userProfileProperty = this.MyField.UserProfileProperty;

                    bool isPublishingPage = false;
                    bool publishingPageNeedsPreFilling = false;
                    if (SPContext.Current.ListItem != null)
                    {
                        isPublishingPage = PublishingPage.IsPublishingPage(SPContext.Current.ListItem);
                        if (string.IsNullOrEmpty(this.ItemFieldValue as string) && isPublishingPage)
                        {
                            publishingPageNeedsPreFilling = true;
                        }
                    }

                    if (publishingPageNeedsPreFilling ||
                        (!string.IsNullOrEmpty(userProfileProperty) && !this.Page.IsPostBack &&
                        SPContext.Current.FormContext.FormMode == SPControlMode.New))
                    {
                        // Try getting user profile property and mapping it to a term from a term set
                        SPServiceContext serviceContext = SPServiceContext.Current;
                        UserProfileManager userProfileManager = new UserProfileManager(serviceContext);
                        UserProfile userProfile = null;
                        try
                        {
                            userProfile = userProfileManager.GetUserProfile(true);
                        }
                        catch
                        { }

                        if (userProfile == null)
                        {
                            return;
                        }

                        TaxonomySession session = new TaxonomySession(SPContext.Current.Site);
                        TermStore termStore = session.TermStores.FirstOrDefault(ts => ts.Id == termStoreId);
                        if (termStore != null)
                        {
                            TermSet termSet = termStore.GetTermSet(termSetId);
                            if (termSet != null)
                            {
                                List<Term> terms = GetTermFromProfileProperty(serviceContext, userProfile, termSet, userProfileProperty);
                                if (terms != null && terms.Count > 0)
                                {
                                    if (this.AllowMultipleValues)
                                    {
                                        this.taxonomyWebControl.Text = string.Empty;
                                        foreach (Term term in terms)
                                        {
                                            this.taxonomyWebControl.Text += string.Format(“{0}|{1}”, term.Name, term.Id) + “;”;
                                        }
                                        this.taxonomyWebControl.Text = this.taxonomyWebControl.Text.Substring(0this.taxonomyWebControl.Text.Length  1);
                                    }
                                    else
                                    {
                                        this.taxonomyWebControl.Text = string.Format(“{0}|{1}”, terms[0].Name, terms[0].Id);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            else
            {
                this.Controls.Clear();
            }
        }

        /// <summary>
        /// Fills controls with values if neccessary
        /// </summary>
        /// <param name=”e”></param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            if (SPContext.Current.FormContext.FormMode == SPControlMode.Display)
            {
                LiteralControl displayText = new LiteralControl(string.Empty);
                string val = this.ItemFieldValue as string;
                if (!string.IsNullOrEmpty(val))
                {
                    if (this.AllowMultipleValues)
                    {
                        string[] splitValues = val.Split(new char[] { ‘;’ }, StringSplitOptions.RemoveEmptyEntries);
                        foreach (string str in splitValues)
                        {
                            string[] realValues = str.Split(new char[] { ‘|’ }, StringSplitOptions.RemoveEmptyEntries);
                            if (realValues.Length > 1)
                            {
                                displayText.Text += realValues[0] + ‘;’;
                            }
                        }
                    }
                    else
                    {
                        string[] realValues = val.Split(new char[] { ‘|’ }, StringSplitOptions.RemoveEmptyEntries);
                        if (realValues.Length > 1)
                        {
                            displayText.Text += realValues[0] + ‘;’;
                        }
                    }
                    this.Controls.Add(displayText);
                }
            }
            else if ((!Page.IsPostBack && SPContext.Current.FormContext.FormMode == SPControlMode.Edit) ||
                     (PublishingPage.IsPublishingPage(SPContext.Current.ListItem) &&
                      (SPContext.Current.FormContext.FormMode == SPControlMode.Edit ||
                       SPContext.Current.FormContext.FormMode == SPControlMode.New)))
            {
                string val = this.ItemFieldValue as string;
                if (!string.IsNullOrEmpty(val))
                {
                    this.taxonomyWebControl.Text = val;
                }
            }
        }

        /// <summary>
        /// Go through the values (this.taxonomyWebControl.Text) and add newly typed terms to termset if needed
        /// </summary>
        /// <returns>Value that includes new Guids of newly added terms</returns>
        private string AddNewTermsToTermSet()
        {
            string parsedValue = string.Empty;
            string unparsedValue = this.taxonomyWebControl.Text;
            if (!string.IsNullOrEmpty(unparsedValue) && this.taxonomyWebControl.SspId.FirstOrDefault() != null &&
                this.taxonomyWebControl.TermSetId.FirstOrDefault() != null)
            {
                TaxonomySession session = new TaxonomySession(SPContext.Current.Site); 
                TermStore termStore = session.TermStores.FirstOrDefault(ts => ts.Id == this.taxonomyWebControl.SspId.FirstOrDefault());
                TermSet termSet = termStore.GetTermSet(this.taxonomyWebControl.TermSetId.FirstOrDefault());

                if (this.AllowMultipleValues)
                {
                    string[] splitValues = unparsedValue.Split(new char[] { ‘;’ }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string str in splitValues)
                    {
                        string[] realValues = str.Split(new char[] { ‘|’ }, StringSplitOptions.RemoveEmptyEntries);
                        if (realValues.Length > 1)
                        {
                            if (termSet.GetTerms(realValues[0]false).Count <= 0)
                            {
                                Term term = null;
                                term = termSet.CreateTerm(realValues[0], termStore.WorkingLanguage);
                                if (term != null)
                                {
                                    realValues[1] = term.Id.ToString();
                                }
                            }
                            parsedValue += string.Concat(realValues[0]‘|’, realValues[1]‘;’);
                        }
                    }
                    parsedValue = parsedValue.TrimEnd(‘;’);
                }
                else
                {
                    string[] realValues = unparsedValue.Split(new char[] { ‘|’ }, StringSplitOptions.RemoveEmptyEntries);
                    if (realValues.Length > 1)
                    {
                        if (termSet.GetTerms(realValues[0]false).Count <= 0)
                        {
                            Term term = null;
                            term = termSet.CreateTerm(realValues[0], termStore.WorkingLanguage);
                            if (term != null)
                            {
                                realValues[1] = term.Id.ToString();
                            }
                        }
                    }
                    parsedValue = string.Concat(realValues[0]‘|’, realValues[1]);
                }
                termStore.CommitAll();
            }
            else
            {
                parsedValue = this.taxonomyWebControl.Text;
            }

            return parsedValue;
        }

        /// <summary>
        /// Gets the profile propery and tries to convert it to Term
        /// </summary>
        /// <param name=”userProfile”>Current user profile</param>
        /// <param name=”targetTermSet”>Term set that provides the terms</param>
        /// <param name=”propertyName”>User profile property name</param>
        /// <returns>null if no match found, Term otherwise</returns>
        private List<Term> GetTermFromProfileProperty(SPServiceContext context, UserProfile userProfile, TermSet targetTermSet, string propertyName)
        {
            List<Term> terms = null;
            ProfileSubtypeManager psm = ProfileSubtypeManager.Get(context);
            ProfileSubtype currentProfileSubtype = psm.GetProfileSubtype(ProfileSubtypeManager.GetDefaultProfileName(ProfileType.User));
            ProfileSubtypeProperty property = currentProfileSubtype.Properties.FirstOrDefault(=> p.Name == propertyName);

            if (property != null && userProfile[propertyName].Count > 0 && targetTermSet != null) // need that term set!
            {
                UserProfileValueCollection propValue = userProfile[propertyName];
                string finalPropValue = string.Empty;
                if (userProfile[propertyName].Count == 1)
                {
                    finalPropValue = propValue.Value.ToString();
                }
                else
                {
                    System.Collections.IEnumerator allValues = propValue.GetEnumerator();
                    allValues.MoveNext();
                    finalPropValue = allValues.Current.ToString();
                }
                terms = TryGetTermsFromTermSet(targetTermSet, propValue.Value.ToString());
            }
            return terms;
        }

        /// <summary>
        /// Tries to get terms from specified term set
        /// </summary>
        /// <param name=”termSet”>Term set that provides the terms</param>
        /// <param name=”alternativeLabel”>Alternative label of a term</param>
        /// <returns></returns>
        private List<Term> TryGetTermsFromTermSet(TermSet termSet, string alternativeLabel)
        {
            List<Term> results = new List<Term>();
            int alternativeLabelInNumber = 0;
            if (alternativeLabel.Contains(“_”))
            {
                alternativeLabel = alternativeLabel.Substring(alternativeLabel.LastIndexOf(‘_’)).TrimStart(‘_’);
                int.TryParse(alternativeLabel, out alternativeLabelInNumber);
            }

            TermCollection filteredTerms = termSet.GetTerms(alternativeLabel, false, StringMatchOption.ExactMatch, termSet.Terms.Counttrue);
            foreach (Term term in filteredTerms)
            {
                LabelCollection labels = null;
                try
                {
                    labels = term.GetAllLabels(System.Threading.Thread.CurrentThread.CurrentUICulture.LCID);
                }
                catch
                {
                    labels = term.Labels;
                }

                foreach (Microsoft.SharePoint.Taxonomy.Label label in labels)
                {
                    // find the exact match
                    if (label.Value.ToLower().Trim() == alternativeLabel.ToLower().Trim())
                    {
                        results.Add(term);
                        break;
                    }
                }
            }
            return results;
        }
    }
}

Basically, main binding to ASCX file is done through overriding at least DefaultTemplateName property and returning the ID of our RenderingTemplate element from previous code block (PreDefinedTaxonomyFieldRenderingTmpl). This will ensure that New and Edit forms will have our custom rendering. If you want to override rendering in Display form – you have to also override the DisplayTemplateName property, as is done in this example.

I understand that coding lacks thorough commenting from my side, but you should get the idea how to do it now. Everything that’s not covered is not very relevant as it is just business logic.

If you have any questions I’ll try to answer. Hope this helps someone 🙂

2 comments

    1. Hi Daug!
      Sorry for this. If I remember correctly it was picking term sets based on my custom logic… fell free to iterate through all of them from Taxonomy session if you wish to! Hope this helps

Leave a Reply

Your email address will not be published. Required fields are marked *