[SharePoint 2013] Adding Links to the Suite Bar (Newsfeed, SkyDrive, Sites) By Overriding the SuiteLinksDelegate Delegate Control

SharePoint 2013 features a new set of links called the Suite Bar Links that are displayed in the top right corner of every SharePoint page. By default these links include “Newsfeed”, “SkyDrive”, and “Sites”.

When first seeing this links my first thought was “how do I change them?”. In exploring the master page, we can see that the links are added to the page with this delegate control:

<SharePoint:DelegateControl id="ID_SuiteLinksDelegate" ControlId="SuiteLinksDelegate" runat="server" />

This presents two options for changing the links:

1. Remove the delegate control (or hide it) and simply hard code the links in the master page. This is only a viable option if you using a single language site and you are only making the changes for a single site collection. If you were using a multilingual site, you would lose the automatically translated links that SharePoint provides. And you would need to make this change in the master pages of every site collection.

2. Override the delegate control with a custom control. This presents a unique challenge in that all of the default links are hard-coded in non-public methods of the SharePoint assemblies.

Creating a Custom Delegate Control to Override SuiteLinksDelegate

The goal with this custom control is to maintain the same functionality as the built-in SuiteLinksDelegate control while adding our own links. To do this, we are going to use similar code to what the built-in control uses, some .NET reflection, and some techniques of my own.

To get started, ensure that you have SharePoint 2013 installed and configured along with Visual Studio 2012 installed with the SharePoint 2013 developer tools.

  1. Launch Visual Studio 2012 and create a new project: Templates > Visual C# > Office/SharePoint > SharePoint Solutions > SharePoint 2013 Project. For this example, I named the project “SharePointDelegates”. When prompted, select Farm Solution as the solution type.

  2. Add the following references to your project (you will need to browse to C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\): Microsoft.Office.Server.dll, Microsoft.Office.Server.Search.dll, Microsoft.Office.Server.UserProfiles.dll, and Microsoft.SharePoint.Portal.dll.

  3. Add a “UrlUtility” class (right-click the solution name > Add > Class.

  4. The UrlUtility class is used by the default SuiteLinksDelegate control and rather than reinvent the wheel, we are just copying the class from the SharePoint assemblies:

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Administration;
    using Microsoft.SharePoint.Upgrade;
    using Microsoft.SharePoint.Utilities;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace SharePointDelegates
    {
        /// <summary>
        /// Methods borrowed from the existing SuiteLinks Delegate Control
        /// </summary>
        internal class UrlUtility : SPUrlUtility
        {
            private UrlUtility()
            {
            }
    
            internal static string ConvertToLegalFileName(string inputName, char replacementChar)
            {
                int length = inputName.Length;
                StringBuilder builder = new StringBuilder(inputName);
                bool flag = false;
                for (int i = length - 1; i >= 0; i--)
                {
                    char character = inputName[i];
                    bool flag2 = character == '.';
                    if (!SPUrlUtility.IsLegalCharInUrl(character) || (flag2 && (((i == 0) || (i == (length - 1))) || flag)))
                    {
                        builder[i] = replacementChar;
                    }
                    flag = flag2;
                }
                return builder.ToString();
            }
    
            internal static string EnsureNoTrailingSlash(string strUrl)
            {
                char[] trimChars = new char[] { '/' };
                return strUrl.TrimEnd(trimChars);
            }
    
            internal static string EnsureTrailingSlash(string strUrl)
            {
                if (!strUrl.EndsWith("/", StringComparison.OrdinalIgnoreCase))
                {
                    strUrl = strUrl + "/";
                }
                return strUrl;
            }
    
            public static bool EquivalentUris(Uri uri1, Uri uri2)
            {
                uri1 = SPFarm.Local.AlternateUrlCollections.RebaseUriWithAlternateUri(uri1, 0);
                uri2 = SPFarm.Local.AlternateUrlCollections.RebaseUriWithAlternateUri(uri2, 0);
                return uri1.Equals(uri2);
            }
    
            public static string GetImageUrl(string imageName)
            {
                return ("/" + SPUtility.ContextLayoutsFolder + "/images/" + imageName);
            }
    
            internal static string GetUniqueObjectUrl(SPSite site, string desiredServerRelativeUrl, string fileExtension, int maxRetry)
            {
                string uniqueUrl = null;
                if (!SPManager.PeekIsUpgradeRunning())
                {
                    Guid siteId = site.ID;
                    SPSecurity.RunWithElevatedPrivileges(delegate
                    {
                        using (SPSite spSite = new SPSite(siteId))
                        {
                            uniqueUrl = GetUniqueObjectUrlHelper(spSite, desiredServerRelativeUrl, fileExtension, maxRetry);
                        }
                    });
                }
                else
                {
                    uniqueUrl = GetUniqueObjectUrlHelper(site, desiredServerRelativeUrl, fileExtension, maxRetry);
                }
                return uniqueUrl;
            }
    
            private static string GetUniqueObjectUrlHelper(SPSite site, string desiredServerRelativeUrl, string fileExtension, int maxRetry)
            {
                if (desiredServerRelativeUrl == null)
                {
                    throw new ArgumentNullException("desiredServerRelativeUrl");
                }
                if (site == null)
                {
                    throw new ArgumentNullException("site");
                }
                desiredServerRelativeUrl = desiredServerRelativeUrl.Trim();
                if (desiredServerRelativeUrl.EndsWith("/", StringComparison.OrdinalIgnoreCase))
                {
                    throw new ArgumentException();
                }
                int num = 0;
                string str = desiredServerRelativeUrl;
                bool flag = false;
                if (!string.IsNullOrEmpty(fileExtension))
                {
                    flag = true;
                    if (str.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase))
                    {
                        str = str.Substring(0, str.Length - fileExtension.Length);
                    }
                    desiredServerRelativeUrl = str + fileExtension;
                }
                do
                {
                    try
                    {
                        SPWeb web = site.OpenWeb();
                        try
                        {
                            if (web.GetObject(desiredServerRelativeUrl) != null)
                            {
                                desiredServerRelativeUrl = str + num.ToString(CultureInfo.InvariantCulture);
                                if (flag)
                                {
                                    desiredServerRelativeUrl = desiredServerRelativeUrl + fileExtension;
                                }
                                num++;
                            }
                            else
                            {
                                return desiredServerRelativeUrl;
                            }
                        }
                        finally
                        {
                            if (web != null)
                            {
                                web.Close();
                            }
                        }
                    }
                    catch (FileNotFoundException)
                    {
                        return desiredServerRelativeUrl;
                    }
                }
                while ((maxRetry < 0) || (num <= maxRetry));
                return null;
            }
    
            internal static string SafeAppendQueryStringParameter(string strUrl, string strKey, string strValue)
            {
                if (strUrl.IndexOf(strKey + "=", StringComparison.OrdinalIgnoreCase) < 0)
                {
                    string str = SPHttpUtility.UrlKeyValueEncode(strKey, strValue);
                    if (strUrl.IndexOf("?") > 0)
                    {
                        strUrl = strUrl.Trim() + "&" + str;
                        return strUrl;
                    }
                    strUrl = strUrl.Trim() + "?" + str;
                }
                return strUrl;
            }
        }
    }
    
  5. Add a “SuiteLinksHelper” class.

  6. The SuiteLinksHelper class contains methods that mimic the internal SharePoint methods used by the SuiteLinksDelegate control. In places that accessed other internal SharePoint methods, .NET reflection is used instead to access the same methods.

    using Microsoft.Office.Server.UserProfiles;
    using Microsoft.Office.Server.Administration;
    using Microsoft.SharePoint;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.Reflection;
    using Microsoft.SharePoint.Utilities;
    using Microsoft.SharePoint.WebControls;
    using Microsoft.SharePoint.Administration;
    using Microsoft.SharePoint.Portal.WebControls;
    
    namespace SharePointDelegates
    {
        /// <summary>
        /// Methods borrowed from the existing SuiteLinks Delegate Control
        /// Portions of the code that accessed internal SharePoint methods were replaced with reflection
        /// </summary>
        class SuiteLinksHelper
        {        
        	// Sets an item in the cache of the current context
            internal static void SetItem(string key, string value)
            {
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.Items[key] = value;
                }
            }
    
            // Gets an item from the cache of the current context
            internal static string GetItem(string key)
            {
                if (HttpContext.Current == null)
                {
                    return null;
                }
                return (HttpContext.Current.Items[key] as string);
            }
    
            // Gets the current SharePoint Url Zone (alternate access map)
            internal static SPUrlZone CurrentUrlZone
            {
                get
                {
                    if (HttpContext.Current != null)
                    {
                        try
                        {
                            return SPControl.GetContextSite(HttpContext.Current).Zone;
                        }
                        catch { }
                    }
                    return SPUrlZone.Default;
                }
            }
    
            // Sets the needed Urls for the default SuiteLinksDelegate control in the cache of the current context
            internal static void EnsureProfileUrlsCached()
            {
                var upaProxyType = Type.GetType("Microsoft.Office.Server.Administration.UserProfileApplicationProxy, Microsoft.Office.Server.UserProfiles, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
                var userProfileManager = new UserProfileManager(SPServiceContext.Current, false, false);
                var prop = userProfileManager.GetType().GetProperty("UserProfileApplicationProxy", AllBindings);
                var proxy = prop.GetValue(userProfileManager, null);
                if (proxy != null)
                {                
                    var rawPartitionIDProperty = upaProxyType.GetMethod("GetRawPartitionID", AllBindings, null, new Type[] { typeof(SPServiceContext) }, null);
                    object[] rawPartitionIDParameters = { SPServiceContext.Current };
                    var rawPartitionID = rawPartitionIDProperty.Invoke(proxy, rawPartitionIDParameters) as Guid?;
    
                    var mySitePortalUrlProperty = upaProxyType.GetMethod("GetMySitePortalUrl", AllBindings, null, new Type[] { typeof(SPUrlZone), typeof(Guid) }, null);
                    object[] mySitePortalUrlParameters = { CurrentUrlZone, rawPartitionID };
                    var mySitePortalUrl = mySitePortalUrlProperty.Invoke(proxy, mySitePortalUrlParameters) as string;
                    
                    if (!string.IsNullOrEmpty(mySitePortalUrl))
                    {
                        if (string.IsNullOrEmpty(GetItem("SocialData$MySiteHostURL")))
                        {
                            string str2 = UrlUtility.EnsureTrailingSlash(mySitePortalUrl);
                            SetItem("SocialData$MySiteHostURL", str2);
                        }
                        if (string.IsNullOrEmpty(GetItem("SocialData$ProfileURL")))
                        {
                            string userProfileURL = GetUserProfileURL(mySitePortalUrl, string.Empty, string.Empty);
                            SetItem("SocialData$ProfileURL", userProfileURL);
                        }
                        if (string.IsNullOrEmpty(GetItem("SocialData$MyProfileSettingsURL")))
                        {
                            string str4 = GetEditProfileUrl(SPServiceContext.Current, mySitePortalUrl, "", "");
                            SetItem("SocialData$MyProfileSettingsURL", str4);
                        }
                        if ((SPContext.Current != null) && (SPContext.Current.Web != null))
                        {
                            if (string.IsNullOrEmpty(GetItem("SocialData$MyAlertsURL")))
                            {
                                string str5 = SPUrlUtility.CombineUrl(SPContext.Current.Web.ServerRelativeUrl, SPUtility.ContextLayoutsFolder + "/mysubs.aspx");
                                SetItem("SocialData$MyAlertsURL", str5);
                            }
                            if (string.IsNullOrEmpty(GetItem("SocialData$MyLanguageAndRegionURL")))
                            {
                                string strUrl = SPUrlUtility.CombineUrl(SPContext.Current.Web.ServerRelativeUrl, SPUtility.ContextLayoutsFolder + "/regionalsetng.aspx?type=user");
                                string strValue = SPHttpUtility.HtmlEncode(DeltaPage.RemoveDeltaQueryParameters(SPContext.Current.Site.MakeFullUrl(HttpContext.Current.Request.RawUrl.ToString())));
                                strUrl = UrlUtility.SafeAppendQueryStringParameter(strUrl, "source", strValue);
                                SetItem("SocialData$MyLanguageAndRegionURL", strUrl);
                            }
                        }
                    }
                }
            }
    
            // Returns the Url to the My Site host from the cache of the current context
            internal static string MySiteHostURL
            {
                get
                {
                    return GetItem("SocialData$MySiteHostURL");
                }
            }
    
            // Gets the localized string from the SharePoint string resources
            internal static string GetString(LocStringId lsid)
            {
                var stringResourceManagerType = Type.GetType("Microsoft.SharePoint.Portal.WebControls.StringResourceManager, Microsoft.Office.Server.Search, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
                var getStringProperty = stringResourceManagerType.GetMethod("GetString", AllBindings);
                object[] parameters = { lsid };
                return getStringProperty.Invoke(null, parameters) as string;
            }
    
            // Gets the Url to the current user's profile
            internal static string GetUserProfileURL(string profileWebUrl, string strIdentifier, string strValue)
            {
                if (string.IsNullOrEmpty(profileWebUrl))
                {
                    return null;
                }
                return (UrlUtility.EnsureTrailingSlash(profileWebUrl) + "Person.aspx" + strIdentifier + (strIdentifier.Equals("?accountname=", StringComparison.OrdinalIgnoreCase) ? SPHttpUtility.UrlKeyValueEncode(strValue).Replace(":", "%3A") : SPHttpUtility.UrlKeyValueEncode(strValue)));
            }
    
            // Gets the Url to the current user's profile edit page
            internal static string GetEditProfileUrl(SPServiceContext serviceContext, string profileWebUrl, string sourceUrl = "", string section = "")
            {
                if (serviceContext == null)
                {
                    return string.Empty;
                }
                SPUserSettingsProvider usp = null;
                if ((SPContext.Current != null) && (SPContext.Current.Site != null))
                {
                    SPWebApplication webApplication = SPContext.Current.Site.WebApplication;
                    if (webApplication != null)
                    {
                        usp = webApplication.UserSettingsProvider;
                    }
                    if (string.IsNullOrEmpty(sourceUrl))
                    {
                        sourceUrl = SPContext.Current.Site.MakeFullUrl(HttpContext.Current.Request.RawUrl.ToString());
                        sourceUrl = SPHttpUtility.HtmlEncode(DeltaPage.RemoveDeltaQueryParameters(sourceUrl));
                    }
                }
                return GetEditProfileUrl(GetMySitePortalLayoutsUrl(profileWebUrl, "EditProfile.aspx"), usp, sourceUrl, section);
            }
    
            private static string GetEditProfileUrl(string baseUrl, SPUserSettingsProvider usp, string sourceUrl, string section)
            {
                if (!string.IsNullOrEmpty(section))
                {
                    baseUrl = UrlUtility.SafeAppendQueryStringParameter(baseUrl, "Section", section);
                }
                if (usp != null)
                {
                    baseUrl = UrlUtility.SafeAppendQueryStringParameter(baseUrl, "UserSettingsProvider", usp.ProviderIdentifier.ToString());
                    if (!string.IsNullOrEmpty(sourceUrl))
                    {
                        baseUrl = UrlUtility.SafeAppendQueryStringParameter(baseUrl, "ReturnUrl", sourceUrl);
                    }
                }
                return baseUrl;
            }
    
            // Gets the Url of the specified relative path in the My Site _layouts directory
            internal static string GetMySitePortalLayoutsUrl(string profileWebUrl, string relativePath)
            {
                return (UrlUtility.EnsureTrailingSlash(profileWebUrl) + (SPUtility.ContextLayoutsFolder + "/") + relativePath);
            }
    
            // Checks to see if the user has access to the User Profile links (will not render the links without)
            internal static bool CheckUserAccess
            {
                get
                {
                    var upaProxyType = Type.GetType("Microsoft.Office.Server.Administration.UserProfileApplicationProxy, Microsoft.Office.Server.UserProfiles, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
                    var userProfileManager = new UserProfileManager(SPServiceContext.Current, false, false);
                    var prop = userProfileManager.GetType().GetProperty("UserProfileApplicationProxy", AllBindings);
                    var proxy = prop.GetValue(userProfileManager, null);
                    var upaProxyIsAvailableProperty = upaProxyType.GetMethod("IsAvailable", AllBindings);
                    object[] parameters = { SPServiceContext.Current };
                    var upaProxyIsAvailable = (bool?) upaProxyIsAvailableProperty.Invoke(proxy, parameters);
                    
                    string item = GetItem("SocialData$CheckUserAccess");
                    if (!string.IsNullOrEmpty(item))
                    {
                        return item.Equals(bool.TrueString);
                    }
                    if (upaProxyIsAvailable != true)
                    {
                        return false;
                    }
                    bool flag = false;
                    if (proxy != null)
                    {
                        try
                        {
                            var upaProxyCheckAccessProperty = upaProxyType.GetMethod("CheckUserAccess", AllBindings);
                            object[] accessParameters = { SPServiceContext.Current, 0L | 1L };
                            var upaProxyCheckAccess = (bool?) upaProxyCheckAccessProperty.Invoke(proxy, parameters);
    
                            flag = upaProxyCheckAccess == true ? true : false;
                        }
                        catch (UserProfileException)
                        {
                        }
                    }
                    SetItem("SocialData$CheckUserAccess", flag.ToString());
                    return flag;
                }
            }
    
            // Collection of Binding Flags used for .NET reflection
            private static BindingFlags AllBindings
            {
                get
                {
                    return BindingFlags.CreateInstance |
                        BindingFlags.FlattenHierarchy |
                        BindingFlags.GetField |
                        BindingFlags.GetProperty |
                        BindingFlags.IgnoreCase |
                        BindingFlags.Instance |
                        BindingFlags.InvokeMethod |
                        BindingFlags.NonPublic |
                        BindingFlags.Public |
                        BindingFlags.SetField |
                        BindingFlags.SetProperty |
                        BindingFlags.Static;
                }
            }
        }
    }
    
  7. Add a new Empty Element to create the Delegate Control override reference. (Right-click solution name > Add > New Item > Empty Element (in the Office/SharePoint group). For this example, I used “SuiteLinksControl” as the name of the element.

  8. In the Elements.xml file of the Empty Element, we are using some XML to specify the Id of the Delegate Control to override and the source of our custom user control (which we haven’t created yet):

    <?xml version="1.0" encoding="utf-8"?>
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
      <Control Id="SuiteLinksDelegate" 
               Sequence="90"
               ControlSrc="~/_ControlTemplates/15/SharePointDelegates/SuiteLinksControl.ascx">    
      </Control>  
    </Elements>
  9. In the Solution Explorer, highligh the Empty Element (SuiteLinksControl). In the Properties window, select the [...] button for the Safe Controls property to open the Safe Control Entries window.

  10. Add a new Safe Control entry for our custom user control (which we haven’t created yet):

    • Name: SharePointDelegatesControlTemplates
    • Assembly: $SharePoint.Project.AssemblyFullName$
    • Namespace: SharePointDelegates.CONTROLTEMPLATES.SharePointDelegates
    • Safe: True
    • Safe Against Script: True
    • Type Name: *
  11. When we add the Empty Element, a new Feature was automatically added. In the Solution Explorer rename it to something more meaningful (like SuiteLinksFeature).

  12. Open the SuiteLinksFeature and set the scope to Farm. In addition, you should change the Title to something more meaningful (like “Custom Suite Links”). The Title is what gets displayed on the Features page when activating it.

  13. Add the CONTROLTEMPLATES SharePoint mapped folder (right-click Solution Name > Add > Add SharePoint Mapped Folder > Expand TEMPLATE and Select CONTROLTEMPLATES.

  14. To ensure no conflicts occur, add a new folder under the CONTROLTEMPLATES folder with the same name as the solution (like “SharePointDelegates”).

  15. Add a new user control to the SharePointDelegates folder called “SuiteLinksControl.ascx”. (Right-click the SharePointDelegates folder > Add > New Item > User Control (Farm Solution only)

  16. Open the SuiteLinksControl.ascx.cs file and add the following code:

    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Portal.WebControls;
    using Microsoft.SharePoint.Utilities;
    using Microsoft.SharePoint.WebControls;
    using System.Globalization;
    using System.IO;
    using System.Collections;
    
    namespace SharePointDelegates.CONTROLTEMPLATES.SharePointDelegates
    {
        /// <summary>
        /// The custom SuiteLinksControl copies most of its code from the default 
        /// SuiteLinksDelegate control with a few items added for the custom links
        /// </summary>
        public partial class SuiteLinksControl : UserControl
        {
            // Links to Place Before Default Links
            void RenderLinksBefore(HtmlTextWriter writer)
            {
                //RenderSuiteLink(writer, UrlUtility.EnsureTrailingSlash(this.MySiteHostURL) + "Lookout.aspx", "Lookout", GetSuiteLinkControlId("ShellLookout"));
                RenderSuiteLink(writer, "http://www.bing.com", "Bing", GetSuiteLinkControlId("ShellBing"));
            }
    
            // Links to Place After Default Links
            void RenderLinksAfter(HtmlTextWriter writer)
            {
                RenderSuiteLink(writer, "http://msdn.microsoft.com", "MSDN", GetSuiteLinkControlId("ShellMsdn"));
            }
    
            // Default SharePoint link control IDs
            private const string allDocumentsLinkControlIdPart = "ShellDocuments";
            private const string allSitesLinkControlIdPart = "ShellSites";
            private const string newsfeedLinkControlIdPart = "ShellNewsfeed";
    
            // Methods
            public string GetDesignTimeHtml()
            {
                this.SetControl();
                StringWriter writer = new StringWriter(CultureInfo.CurrentCulture);
                HtmlTextWriter writer2 = new HtmlTextWriter(writer);
                this.Render(writer2);
                writer2.Close();
                return writer.ToString();
            }
    
            private string GetSuiteLinkControlId(string linkId)
            {
                return (this.ClientID + "_" + linkId);
            }
    
            protected override void OnInit(EventArgs e)
            {
                base.OnInit(e);
    
                if (SuiteLinksHelper.CheckUserAccess)
                {
                    SuiteLinksHelper.EnsureProfileUrlsCached();
                    this.MySiteHostURL = SuiteLinksHelper.MySiteHostURL;
                    this.AllDocumentsLinkControlId = this.GetSuiteLinkControlId("ShellDocuments");
                    this.AllSitesLinkControlId = this.GetSuiteLinkControlId("ShellSites");
                    this.NewsfeedLinkControlId = this.GetSuiteLinkControlId("ShellNewsfeed");
                }
            }
    
            protected override void OnPreRender(EventArgs e)
            {
                this.SetControl();
                base.OnPreRender(e);
                if (!string.IsNullOrEmpty(this.MySiteHostURL))
                {
                    ScriptLink.RegisterOnDemand(this, this.Page, "MyLinks.js", false);
                    ScriptLink.RegisterOnDemand(this, this.Page, "sp.js", false);
                    ScriptLink.RegisterOnDemand(this, this.Page, "SP.UI.MySiteNavigation.js", false);
                    string script = string.Format(CultureInfo.InvariantCulture, "_spBodyOnLoadFunctions.push(function() {{ EnsureScriptParams('{0}', 'RenderMySiteLinksFromServer', '{1}', '{2}'); }});", new object[] { "MyLinks.js", SPHttpUtility.EcmaScriptStringLiteralEncode(this.AllDocumentsLinkControlId), SPHttpUtility.EcmaScriptStringLiteralEncode(this.AllSitesLinkControlId) });
                    SPPageContentManager.RegisterStartupScript(this, base.GetType(), "RenderMySiteLinksFromServer", script);
                }
            }
    
            protected override void Render(HtmlTextWriter writer)
            {
                if (!string.IsNullOrEmpty(this.MySiteHostURL))
                {
                    string url = SPUrlUtility.CombineUrl(this.MySiteHostURL, "/default.aspx");
                    string str2 = SPUrlUtility.CombineUrl(this.MySiteHostURL, SPUrlUtility.CombineUrl(SPUtility.RELATIVE_LAYOUTS_LATESTVERSION, "/MySite.aspx?MySiteRedirect=AllDocuments"));
                    string str3 = SPUrlUtility.CombineUrl(this.MySiteHostURL, SPUrlUtility.CombineUrl(SPUtility.RELATIVE_LAYOUTS_LATESTVERSION, "/MySite.aspx?MySiteRedirect=AllSites"));
                    writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-core-suiteLinkList");
                    writer.RenderBeginTag(HtmlTextWriterTag.Ul);
                    // Render our custom before links first
                    RenderLinksBefore(writer);
                    RenderSuiteLink(writer, url, SuiteLinksHelper.GetString(LocStringId.MySuiteLinks_Newsfeed), this.NewsfeedLinkControlId);
                    RenderSuiteLink(writer, str2, SuiteLinksHelper.GetString(LocStringId.MySuiteLinks_Documents), this.AllDocumentsLinkControlId);
                    RenderSuiteLink(writer, str3, SuiteLinksHelper.GetString(LocStringId.MySuiteLinks_Sites), this.AllSitesLinkControlId);
                    // Render our custom after links last
                    RenderLinksAfter(writer);
                    writer.RenderEndTag();
                }
            }
    
            protected static void RenderSuiteLink(HtmlTextWriter writer, string url, string name, string linkId)
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-core-suiteLink");
                writer.RenderBeginTag(HtmlTextWriterTag.Li);
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-core-suiteLink-a");
                writer.AddAttribute(HtmlTextWriterAttribute.Href, url);
                writer.AddAttribute(HtmlTextWriterAttribute.Id, linkId);
                writer.RenderBeginTag(HtmlTextWriterTag.A);
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-verticalAlignMiddle");
                writer.RenderBeginTag(HtmlTextWriterTag.Span);
                writer.Write(name);
                writer.RenderEndTag();
                writer.RenderEndTag();
                writer.RenderEndTag();
            }
    
            protected void SetControl()
            {
                if (!this.Page.IsCallback)
                {
                    if (!SPUtility.IsCompatibilityLevel15Up)
                    {
                        this.Visible = false;
                    }
                    else if (string.IsNullOrEmpty(this.MySiteHostURL))
                    {
                        this.Visible = false;
                    }
                }
            }
    
            // Properties
            private string AllDocumentsLinkControlId { get; set; }
    
            private string AllSitesLinkControlId { get; set; }
    
            private string MySiteHostURL { get; set; }
    
            private string NewsfeedLinkControlId { get; set; }
        }
    }
    
  17. Now that we have our custom control ready to go we can deploy the solution and test. (Right-click the solution name > Deploy)

  18. You should now see the new links at the top right corner of the page when you open your SharePoint site.

That’s it. There is a lot of opportunity to extend this idea (such as with an administration page to manage custom links). It is just unfortunate that Microsoft hard-coded these links rather than making them configurable.

John Chapman

Hello, I'm John Chapman. I am a SharePoint Developer for Sitrion (formerly NewsGator) living in Denver, Colorado. I develop solutions using SharePoint and .NET, and I thrive on the challenge of writing code to overcome the impossible, annoying, or otherwise difficult obstacles.

More Posts - Website - Twitter - LinkedIn - Google Plus

  • Rick

    This worked great on my development site, but when I tried to do this on my 2 tier production site, nothing happened. I got the project to deploy, but the menus did not change.

    • James May

      The same thing happennded with me deployed to farm ok appeared in the features of th central admin when active the suite link menue disapears nothing added

  • Jan

    Tried the above. New to VS so I’m sure I just miss a simple thing. after hitting “deploy” I get several errors saying: “Statement cannot appear outside of a method body/multiline lambda.” This is happening in my “SuitelinksHelper.vb” and “UrlUtility.vb” class. Any idea of what to try?

  • David

    Great post. Works like a charm. Assume you hide the original “Newsfeed”, “SkyDrive”, and “Sites” links and add your own custom page. How do you handle the creation of the MySite for a user who has never accessed their site before?

    • chapmanjw

      All of the My Site Host pages can be casted to an IPersonalPage. That has the current user’s profile and from there you can check to see if they have a personal site. If not, provision it. In order to ensure this doesn’t get called multiple times before it completes, you might create a cookie (or something to that effect) that indicates the process has started and not to start it again. You can use the sample code here in an AdditionalPageHead control, a user control you put on your master page, a web part on the My Site host, etc.

      var personalPage = this.Page as IPersonalPage;
      if (personalPage != null
      && personalPage.Profile != null
      && personalPage.Profile.PersonalSite == null)
      personalPage.Profile.CreatePersonalSite();

  • http://www.facebook.com/daniel.oneal.967 Daniel ONeal

    Worked perfectly for me, but I noticed that the little indicator at the bottom for which link you’re on (Sites, SkyDrive, Newsfeed) goes away. Any way to keep that functionality?

    • chapmanjw

      This was changed for RTM (this was posted pre-RTM). Here are somethings you can modify to get that back:

      private static void RenderSuiteLink(HtmlTextWriter writer, string url, string name, string linkId, bool isActiveLink)
      {
      // Attempt to AAM the URL
      var aamUrl = string.Empty;
      try
      {
      SPUrlZone zone = SPContext.Current.Site.Zone;
      aamUrl = SPFarm.Local.AlternateUrlCollections.RebaseUriWithAlternateUri(new Uri(url), zone).AbsoluteUri;
      if (string.IsNullOrEmpty(aamUrl))
      aamUrl = url;
      }
      catch
      {
      aamUrl = url;
      }
      writer.AddAttribute(HtmlTextWriterAttribute.Class, “ms-core-suiteLink”);
      writer.RenderBeginTag(HtmlTextWriterTag.Li);
      writer.AddAttribute(HtmlTextWriterAttribute.Class, “ms-core-suiteLink-a”);
      writer.AddAttribute(HtmlTextWriterAttribute.Href, url);
      writer.AddAttribute(HtmlTextWriterAttribute.Id, linkId);
      writer.RenderBeginTag(HtmlTextWriterTag.A);
      writer.RenderBeginTag(HtmlTextWriterTag.Span);
      writer.Write(name);
      if (isActiveLink)
      {
      writer.AddAttribute(HtmlTextWriterAttribute.Id, “Suite_ActiveLinkIndicator_Clip”);
      writer.AddAttribute(HtmlTextWriterAttribute.Class, “ms-suitenav-caratBox”);
      writer.RenderBeginTag(HtmlTextWriterTag.Span);
      writer.AddAttribute(HtmlTextWriterAttribute.Id, “Suite_ActiveLinkIndicator”);
      writer.AddAttribute(HtmlTextWriterAttribute.Class, “ms-suitenav-caratIcon”);
      writer.AddAttribute(HtmlTextWriterAttribute.Src, SPUtility.GetThemedImageUrl(SPUrlUtility.CombineUrl(SPUtility.ContextImagesRoot, “spcommon.png”), “spcommon”));
      writer.RenderBeginTag(HtmlTextWriterTag.Img);
      writer.RenderEndTag();
      writer.RenderEndTag();
      }

      writer.RenderEndTag();
      writer.RenderEndTag();
      writer.RenderEndTag();
      }

      internal enum SuiteLinkType
      {
      Documents = 0,
      Sites = 1,
      Newsfeed = 2
      }

      private SuiteLinkType GetActiveLink(int webTemplateId, int listTemplateId)
      {
      switch (webTemplateId)
      {
      case 0×15:
      {
      if (listTemplateId == 700)
      return SuiteLinkType.Documents;
      string absolutePath = SPAlternateUrl.ContextUri.AbsolutePath;
      if (absolutePath.EndsWith(“/Social/FollowedContent.aspx”, StringComparison.OrdinalIgnoreCase))
      return SuiteLinkType.Documents;
      if (absolutePath.EndsWith(“/Social/Sites.aspx”, StringComparison.OrdinalIgnoreCase))
      return SuiteLinkType.Sites;
      return SuiteLinkType.Newsfeed;
      }
      case 0×36:
      return SuiteLinkType.Newsfeed;
      }
      return SuiteLinkType.Sites;
      }

      And then with your call to render the links, just add a bool at the end to indicate if it is active. In this instance, I have gotten the active link as “activeLink”:

      RenderSuiteLink(writer, url, SocialControlHelper.GetString(LocStringId.MySuiteLinks_Newsfeed), this.NewsfeedLinkControlId, activeLink == SuiteLinkType.Newsfeed);

      • Jason Guthrie

        Everything works great, on numerous environments with little effort… except I had trouble with this addition:

        RenderSuiteLink(writer, http://Server1, “Server1″, GetSuiteLinkControlId(“ShellServerOne”), activeLink == SuiteLinkType.Sites);

        The name ‘activeLink’ does not exist in the current context. Can’t figure out this activeLink” part.

        I was also unsuccessful with using icons in the links using the following:
        RenderSuiteLink(writer, UrlUtility.GetImageUrl(“PD01.png”) + “http://Server1/Pages/PD.aspx”, “PD”, GetSuiteLinkControlId(“ShellPD”), true);

        • Neal Mukundan

          Hi Jason

          Just in case you didn’t find an answer, here’s something that worked for me.

          string absolutePath = SPAlternateUrl.ContextUri.AbsolutePath;
          bool MyProfileIsActive = true;
          bool SkyDriveIsActive = false;

          if (absolutePath.EndsWith(“/person.aspx”, StringComparison.OrdinalIgnoreCase))
          {
          MyProfileIsActive = true;
          SkyDriveIsActive = false;
          }
          else if (absolutePath.EndsWith(“/Documents/Forms/All.aspx”, StringComparison.OrdinalIgnoreCase))
          {
          MyProfileIsActive = false;
          SkyDriveIsActive = true;
          }

          RenderSuiteLink(writer, url, name1, this.MyProfileControlId, MyProfileIsActive);
          RenderSuiteLink(writer, str2, name2, this.SkyDriveProControlId, SkyDriveIsActive);
          writer.RenderEndTag();

          Good luck!

  • L. Esteban V Munoz

    This is exactly what I was looking for, but the code about the activeLink does not compile because the activeLink variable does not exist, exactly this line: activeLink == SuiteLinkType.Newsfeed. Right now I hardcoded true and false to see the carat icon be rendered and it works perfect. However I need that it renders the caratIcon, if the current web is the active web, how can I do that?

  • L. Esteban V Munoz

    I am using resharper to clean my code, and the constants at the top of the SuiteLinksDelegateCtrl are never used.

  • Alex Dove

    I am trying to build the solution, however, I am getting errors about:
    ‘Microsoft.SharePoint.Portal.WebControls.LocStringId’ does not contain a definition for ‘MySuiteLinks_Newsfeed’
    What am I doing wrong?

    • chapmanjw

      There were changes to this control for Service Pack 1. I’ll get an updated post eventually. In the meantime, that reference should still work, but the SkyDrive one was simply replaced with “OneDrive” hard-coded (instead of the resource string). If the MySuiteLinks_Newsfeed constant still doesn’t work, replace it with 0x1bb1 (which is what that constant represents).

  • Evgeny Zinger

    Hello. Thank you very match, it’s working!
    But I had some problems – when I have done “SuiteLinksHelper” I had error with “HttpContext” (I’m working on clean VS 2013). It was becouse in code we are using System.Web, but do not Add this reference. Please, correct your page. So we need to Add Reference “System.Web” (I have added System.Web.http too) for right working.

  • L. Esteban V Munoz

    Hello John, your article is great and I implemented it many months ago, however I faced the following problem:

    1,. WHen I upgrade to SP1, the Sites Link renders as “Your trial version has expired”,

    http://screencast.com/t/UDukskykh

    this happens in this line:

    RenderSuiteLink(writer, str3, SuiteLinksHelper.GetString(LocStringId.MySuiteLinks_Sites),

    AllSitesLinkControlId, false, false);

    I guess they changed the resource files between RTM and SP1, but any idea how to fix this?

    • chapmanjw

      The SkyDrive link string is now hard coded to “OneDrive”. As for the other two, I have started using my own resource strings in my own code so future updates don’t cause this again.

      • Paul Ozeruga

        Would you be kind to provide more info on how you updated your solution?
        Thank you!

  • John Lowe

    is it possible to implement this solution in office 365?

    • chapmanjw

      At present, you could attempt to do this in a sandboxed solution. However, MS is phasing out code-based sandboxed solutions from Office 365. So, it would only work for a little while. Your only real solution for Office 365 is to edit the master page. There, hide the existing control and manually add the HTML markup for the links.