Leveraging internal controls and APIs using wrapper controls


SharePoint 2013 is a rich platform for building web solutions. And although it offers a rich API to leverage its capabilities in custom solutions, not all of the functionality is accessible through those APIs. There is however a way to access some of it using wrapper controls.

Inconvenient SharePoint internal APIs

Imagine the following scenario: you wanted to publish Open Graph metadata to control how the content of your public-facing website built using SharePoint 2013 is published on social networks. The basic set of Open Graph metadata requires you to include the URL and the title on each page.

SharePoint 2013, aside from the classic content publishing model that we know from SharePoint 2007 and 2010, introduces new ways for publishing content based on search. In the search-driven content publishing model, we can leverage new SEO functionality that SharePoint 2013 offers us to optimize our web content for Internet search engines, which includes an optimized title for the browser title bar and canonical URL – both which are a part of the Open Graph basic metadata set.

Depending on how the content of your SharePoint 2013 public-facing website is published, SharePoint is using a different approach to render the page title in the browser title bar and the canonical URL. All of this logic is wrapped in two controls SeoBrowserTitle and SeoCanonicalLink (both in the Microsoft.SharePoint.Publishing.WebControls namespace). Although those control by default just work, it’s challenging to use them in custom solutions.

Firs of all both the SeoBrowserTitle and SeoCanonicalLink controls are internal, meaning you cannot refer to them programmatically. Also the underlying API, that you could use to get to the same information as what those controls are rendering, is internal as well. Additionally, because both controls are internal, you cannot wrap them in a Panel or include them in an ITemplate, because the ASP.NET Template Parser cannot instantiate non-public controls.

Another challenge is, that the SeoCanonicalLink control renders the whole <link /> tag while for rendering the og:url meta property we only need the contents of the <link /> tag’s href attribute. Finally, should you find a way to render the information from the SEO controls on your page, there is yet another challenge. If you’re using Design Manager you might have noticed that it requires your Master Pages and Page Layouts to be valid XML. This means that, when using Design Manager, you are not allowed to include controls as a part of an attribute, like:

<meta property="og:url" content="<My:CustomSeoCanonicalLink runat='server' />"/>

The above notation is perfectly fine when directly editing the .master and .aspx files and since there are no Open Graph-specific controls provided with SharePoint 2013 it is exactly what you need to render the Open Graph metadata on your pages if you’re not using templated controls.

The last thing important to mention is, that social networks extract Open Graph metadata from the page’s HTML so you cannot use JavaScript to insert the Open Graph meta tags into the DOM.

Luckily there is a way after all to get the SEO page information and have it rendered as Open Graph metadata.

SEO Delegate Controls

If you take a closer look at how the SeoBrowserTitle and SeoCanonicalLink controls are used by default, you will find out that they are associated with one of the Delegate Controls located on the Master Page. Also, if you examined how the Delegate Controls work in SharePoint with regard to the control rendering process, you would discover that, based on the control registration information located in Features, SharePoint Delegate Controls are using reflection to create and configure instances of controls. In this approach it doesn’t matter whether a control is public or not.

Leveraging internal controls and APIs using wrapper controls

In order to render page SEO information as Open Graph metadata we need the following capabilities:

  • instantiate an internal control
  • render the contents in a templated way (for example render page title as a part of the og:title meta tag which will also allow us to use those controls with Design Manager)
  • postprocess its contents if necessary (for example to extract the value of the href attribute from the output of the SeoCanonicalLink control)

To illustrate the process of fulfilling those requirements let’s create three controls.

Wrapper Control for instantiating any control

Following is the code of the first control. This is a wrapper control that we can use to instantiate any control and which logic is very similar to how SharePoint Delegate Controls work.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml.Linq;

namespace Mavention.SharePoint.Seo.Controls {
    [ParseChildren(ChildrenAsProperties = true)]
    public class ControlWrapper : Control {
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public string Control { get; set; }

        private Control control;

        protected override void OnInit(EventArgs e) {
            EnsureChildControls();
            base.OnInit(e);
        }

        protected override void CreateChildControls() {
            if (!String.IsNullOrEmpty(Control)) {
                control = GetControlFromXml(Control);

                if (control != null) {
                    Controls.Add(control);
                }
            }
        }

        private static Control GetControlFromXml(string controlXml) {
            Control control = null;

            XElement xControl = XElement.Parse(controlXml);

            if (xControl.Attribute("assembly") != null &&
                xControl.Attribute("type") != null) {
                string controlAssembly = xControl.Attribute("assembly").Value;
                string controlType = xControl.Attribute("type").Value;

                IEnumerable<XAttribute> attributes = xControl.Attributes();
                Dictionary<string, string> properties = new Dictionary<string, string>(attributes.Count());
                foreach (XAttribute attribute in attributes) {
                    if (attribute.Name == "assembly" ||
                        attribute.Name == "type") {
                        continue;
                    }

                    properties[attribute.Name.ToString()] = attribute.Value;
                }

                Assembly assembly = Assembly.Load(controlAssembly);
                Type type = assembly.GetType(controlType);
                control = (Control)Activator.CreateInstance(type);

                foreach (KeyValuePair<string, string> property in properties) {
                    SetFieldOrProperty(type, control, property.Key, property.Value);
                }
            }

            return control;
        }

        private static void SetFieldOrProperty(Type controlType, Control control, string propertyName, string propertyValue) {
            BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.IgnoreCase;
            PropertyInfo property = controlType.GetProperty(propertyName, flags);

            if (property != null) {
                Type propertyType = property.PropertyType;
                property.SetValue(control, Convert.ChangeType(propertyValue, propertyType, null));
            }
            else {
                FieldInfo field = controlType.GetField(propertyName, flags);

                if (field != null) {
                    Type fieldType = field.FieldType;
                    field.SetValue(control, Convert.ChangeType(propertyValue, fieldType, null));
                }
            }
        }

        protected override void Render(HtmlTextWriter writer) {
            if (control != null &&
                control.GetType().FullName == "Microsoft.SharePoint.Publishing.WebControls.SeoBrowserTitle") {
                ContentPlaceHolder holder = Page.Master.FindControl("PlaceHolderPageTitle") as ContentPlaceHolder;

                if (holder != null) {
                    StringBuilder sb = new StringBuilder();
                    using (StringWriter sw = new StringWriter(sb)) {
                        using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
                            holder.RenderControl(htw);
                        }
                    }

                    writer.Write(sb.ToString().Trim());
                }
            }
            else {
                base.Render(writer);
            }
        }
    }
}

First we need to be able to specify which control we want to render. For this we specify a property in line 15. Next, as we want our control to be instantiated timely so that it can participate in the page lifecycle, we want to ensure that it is registered early in the page lifecycle during the init stage (line 20). Next, in the CreateChildControls method, we retrieve the instance of the control (line 26) and add it as a child control so that it will have access to all properties of the current page (line 29). Finally we render the control as a part of the wrapper control, so that the contents are rendered exactly where the wrapper control has been placed on the page (line 102).

One thing that we have to deal with separately is how the SeoBrowserTitle control is rendered. The way the SeoBrowserTitle control works, is that it renders its contents to the PlaceHolderPageTitle Content Place Holder so in order to get its contents, we have to intercept the rendering of that Content Place Holder rather than the SeoBrowserTitle control itself (lines 86-100).

The interesting part is how the control to be rendered is specified. To better understand it, let’s take a look at sample code that you would use to render the contents of the SeoBrowserTitle control.

<%@Register Tagprefix="Mavention" Namespace="Mavention.SharePoint.Seo.Controls" Assembly="Mavention.SharePoint.Seo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a285ef6967f781d3"%>
<Mavention:ControlWrapper runat="server">
<Control>
    <control type="Microsoft.SharePoint.Publishing.WebControls.SeoBrowserTitle" assembly="Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
</Control>
</Mavention:ControlWrapper>

The control to be rendered is defined as XML – just as you would do with a Delegate Control. The type and assembly attributes specify which control should be rendered and from which assembly it should be retrieved. So we start the process of instantiating the control by retrieving this information (lines 41 and 42). Some controls, like the FieldValue control used to display the value of a Publishing Field, require additional attributes to work correctly. So to set the value of those attributes we iterate through all attributes of the control element and store all attributes with their values for further processing (lines 44-53).

Once we have all the information we can start instantiating and configuring our control. We first retrieve the underlying assembly (line 55) and from there the control type (line 56). Next we create a new instance of the control (line 57). By iterating through the attributes collection we set the properties on the control instance (lines 59-61). The important part here is to take into account the types of different properties and convert the attribute values from string to their respective type before setting them (lines 72-73 and 79-80). Additionally, a public member can be defined either as a property or a field, and although on the outside there is no difference in how you interact with them, there are two different approaches to set their value when using reflection so we have to take that into account as well (lines 69, 71, 76 and 78).

With the WrapperControl in place we are now able to render the contents of any control in the desired place on a page, including the SeoBrowserTitle control, which by default renders its content to the PlaceHolderPageTitle Content Place Holder.

Rendering the contents of any control in a templated way

The next step is to ensure that we can render the contents of any control in a templated way. With this we will avoid the trouble of including server control in attributes of static HTML tags and will make it possible to use our controls with Design Manager.

Following is the code of a control that allows us to render the contents of any control in a templated way:

using System.IO;
using System.Text;
using System.Web.UI;

namespace Mavention.SharePoint.Seo.Controls {
    public class TemplatedControlWrapper : ControlWrapper {
        public ITemplate ContentTemplate { get; set; }

        protected Control ContentTemplateContainer;
        protected bool RenderBaseOnly;

        protected override void CreateChildControls() {
            if (ContentTemplate != null) {
                ContentTemplateContainer = new Control();
                ContentTemplate.InstantiateIn(ContentTemplateContainer);
            }

            base.CreateChildControls();
        }

        protected override void Render(HtmlTextWriter writer) {
            if (RenderBaseOnly) {
                base.Render(writer);
            }
            else {
                if (ContentTemplateContainer != null) {
                    // render template
                    StringBuilder template = new StringBuilder();
                    using (StringWriter sw = new StringWriter(template)) {
                        using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
                            ContentTemplateContainer.RenderControl(htw);
                        }
                    }

                    // render base
                    StringBuilder baseControl = new StringBuilder();
                    using (StringWriter sw = new StringWriter(baseControl)) {
                        using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
                            base.Render(htw);
                        }
                    }

                    writer.Write(template.ToString().Replace("$Value$", baseControl.ToString()));
                }
            }
        }
    }
}

To get access to the contents of any control we inherit from the previously created WrapperControl. To specify the template for rendering the contents we add a new property called ContentTemplate (line 7). Because this is an ITemplate, it allows you to use server-side controls as well. If you don’t need such support, you could simplify it by setting the type of the ContentTemplate property to string. Additionally we add two more members (lines 9 and 10) which we will need later on for postprocessing rendered content when extracting the URL from the SeoCanonicalLink control output.

We start with initiating the content template (lines 13-16). As all the logic for instantiating the control happens in the base class we can move on directly to the rendering stage. We first render the content template and store its output (lines 28-33). Next we do the same for the base control (lines 36-41). Finally we replace the $Value$ token from the content template with the output of the control and we render the result (line 43).

Following is how you would use the TemplatedControlWrapper control to render the contents of the SeoBrowserTitle control as the Open Graph og:title meta tag:

<%@Register Tagprefix="Mavention" Namespace="Mavention.SharePoint.Seo.Controls" Assembly="Mavention.SharePoint.Seo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a285ef6967f781d3"%>
<Mavention:TemplatedControlWrapper runat="server">
<Control>
    <control type="Microsoft.SharePoint.Publishing.WebControls.SeoBrowserTitle" assembly="Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
</Control>
<ContentTemplate>
    <meta property="og:title" content="$Value$" />
</ContentTemplate>
</Mavention:TemplatedControlWrapper>

Using the TemplatedControlWrapper is also not a problem with Design Manager where the markup would look as follows:

<!--SPM:<%@Register Tagprefix="Mavention" Namespace="Mavention.SharePoint.Seo.Controls" Assembly="Mavention.SharePoint.Seo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a285ef6967f781d3"%>-->
<!--MS:<Mavention:TemplatedControlWrapper runat="server">-->
<Control>
    <control type="Microsoft.SharePoint.Publishing.WebControls.SeoBrowserTitle" assembly="Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
</Control>
<ContentTemplate>
    <meta property="og:title" content="$Value$"/>
</ContentTemplate>
<!--ME:</Mavention:TemplatedControlWrapper>-->

With this we have fulfilled the second requirement for being able to render the contents of any control in a templated way.

Postprocessing the output of any control prior to rendering it

The final requirement, that we have to deal with, is to postprocess the contents of the control prior to rendering it. We need this for example to render the Open Graph URL meta property using the SeoCanonicalLink control. By default what the SeoCanonicalLink control renders is this:

<link rel="canonical" href="http://www.mavention.nl/" />

And we need it to become:

<meta property="og:url" content="http://www.mavention.nl/">

In order to fulfill this requirement we will build further on top of the TemplatedWrapperControl. Following is the code of the HyperlinkControlWrapper control which extracts the value of the href attribute of its control and renders it in the specified content template.

using System.IO;
using System.Text;
using System.Web.UI;

namespace Mavention.SharePoint.Seo.Controls {
    public class HyperlinkControlWrapper : TemplatedControlWrapper {
        protected override void Render(HtmlTextWriter writer) {
            if (ContentTemplateContainer != null) {
                // get content template
                StringBuilder template = new StringBuilder();
                using (StringWriter sw = new StringWriter(template)) {
                    using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
                        ContentTemplateContainer.RenderControl(htw);
                    }
                }

                // render only base control without the content template
                RenderBaseOnly = true;
                StringBuilder baseControl = new StringBuilder();
                using (StringWriter sw = new StringWriter(baseControl)) {
                    using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
                        base.Render(htw);
                    }
                }

                // extract the url from the base control and render it in the template
                string hyperlinkHtml = baseControl.ToString();
                int pos = hyperlinkHtml.IndexOf("href=\"");
                if (pos > -1) {
                    string url = hyperlinkHtml.Substring(pos + 6, hyperlinkHtml.IndexOf('"', pos + 6) - (pos + 6));
                    writer.Write(template.ToString().Replace("$Url$", url));
                }
            }
        }
    }
}

Because all of the control and template instantiation logic is already done in the base class we can directly proceed with the rendering stage. Once again we start with rendering the content template (lines 10-15). Next we make use of the RenderBaseOnly property defined in the base class (line 18). Without this we would get the contents of the control already applied to the template. Next we extract the hyperlink from the contents of the control (lines 27-32). Although we could use regular expressions, for such a simple extraction substring matching is probably easier to comprehend as well as better performing.

Following is how you would use the HyperlinkControlWrapper control to render the Open Graph og:url meta property using the SeoCanonicalLink control:

<%@Register Tagprefix="Mavention" Namespace="Mavention.SharePoint.Seo.Controls" Assembly="Mavention.SharePoint.Seo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a285ef6967f781d3"%>
<Mavention:HyperlinkControlWrapper runat="server">
<Control>
    <control type="Microsoft.SharePoint.Publishing.WebControls.SeoCanonicalLink" assembly="Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
</Control>
<ContentTemplate>
    <meta property="og:url" content="$Url$" />
</ContentTemplate>
</Mavention:HyperlinkControlWrapper>

Note how the $Value$ token has now been replaced with the $Url$ token to denote that we’re rendering only the URL part of the output of the base control rather than all of it.

Summary

SharePoint 2013 is a rich platform that offers developers access to its capabilities using a rich set of APIs. Unfortunately not all functionality is available to developers. Using wrapper controls allows you to leverage the output of internal controls such as SharePoint 2013 SEO controls for example to publish Open Graph metadata for your web pages.

Others found also helpful: