Creating a Custom Welcome Menu for SharePoint Server 2010 – Part 2

Part 2 – Edit Profile and My Site links

A project I am working on called for a custom implementation of the SharePoint Welcome menu control. The replacement control needed to provide the following:

  • Implement the look and behaviors that the designers created.
  • Exclude some default SharePoint menu items (such as My Regional Settings).
  • Reuse some existing SharePoint menu items (such as Sign Out/Sign in as a Different User).
  • Allow for the addition of other custom menu items in the future.
  • Display thumbnail of the user’s profile picture.

I had to create a control that looked and behaved like the following:

CropperCapture2_thumb1

The designers came up with some clean, semantic markup married with some jquery, for me to start from:

<div class="header">

    <img class="header-logo" src="/images/header_logo.png" alt="" width="169" height="78" />

    <div class="header-bar">

        <div class="profile_menu">

            <a href="#" class="profile_btn">

                <span class="image"><img src="" style="width:20px;height:20px" /></span>

                <span class="name">John Doe</span>

                <span class="arrow"></span>

            </a>

            <div class="profile_dropdown">

                <div class="dropdown_bkgrd">

                    <ul>

                        <li>Edit Profile</li>

                        <li>My Site</li>

                        <li>Log Out</li>

                        <li>Sign in as a Different User</li>

                    </ul>

                </div>

            </div>

        </div>

    </div>

</div>

This is part 2 of a three part series.

    1. Part 1 – Overview, Profile Picture, and User Name
    2. Part 2 – Edit Profile and My Site links
    3. Part 3 – Sign Out/Sign in as a Different User

Leveraging the SocialNavigationControl for My Site and My Profile links

The My Site and My Profile links are rendered by the SocialNavigationControl, and contain rendering logic that branches depending on whether a My Site host has been installed. I didn’t want to recreate this so I went digging with Reflector.

I found this code for the control’s OnInit method:

protected override void OnInit(EventArgs e)

{

    base.OnInit(e);

 

    if (!base.IsPostBack && SocialControlHelper.CheckUserAccess)

    {

        this.ProfileURL = SocialControlHelper.ProfileURL;

        this.MySiteHostURL = SocialControlHelper.MySiteHostURL;

    }

}

 

Sweet! Everything I needed was right there, from the SocialControlHelper class.

Wait a minute, Doh! SocialControlHelper is marked as sealed, internal. Wonderful, don’t you love it when Microsoft does that?

Fortunately, upon looking at the code for SocialControlHelper, it runs once, and caches the two URLs it’s responsible for in the HttpContext.Items collection using the following string keys:

private const string FLD_MySiteHostURL = "SocialData$MySiteHostURL";

private const string FLD_ProfileURL = "SocialData$ProfileURL";

 

Perfect, anything can grab it from there. The SocialNavigationControl does its rendering of menu items in the OnPreRender event. This means that by including the control on a page, and setting it’s visibility to FALSE, it won’t render anything, and we can still leverage the items in SocialDataHelper by getting the urls from HttpContext.Items. We can add this to our ASCX markup at the top:

<!-- The social data control is used here just to test for existence of mysite/profile pages -->

<asp:PlaceHolder ID="placeHolderSocialData" runat="server"><SPSWC:SocialNavigationControl ID="socialNav" runat="server" Visible="false"></SPSWC:SocialNavigationControl></asp:PlaceHolder>

 

Some ASCX code-behind:

string myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

string mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

 

if (!Page.IsPostBack)

{

    myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

    ViewState[Constants.VIEWSTATE_PROFILE_URL] = myProfileUrl;

    mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

    ViewState[Constants.VIEWSTATE_MYSITE_URL] = mySiteHostUrl;

}

else

{

    myProfileUrl = ViewState[Constants.VIEWSTATE_PROFILE_URL] as string;

    mySiteHostUrl = ViewState[Constants.VIEWSTATE_MYSITE_URL] as string;

}

 

if (string.IsNullOrEmpty(mySiteHostUrl))

{

    // No MySite host, or user does not have rights to view profile page...

}

else

{

    // User has a MySite, show the link to it...

}

 

if (string.IsNullOrEmpty(myProfileUrl))

{

    // Use the SharePoint foundation profile page, since the MySite host is not available...        

}

else

{

    // Use the MySite profile page...

}

Notice that I am storing these URLs in ViewState as well. The SocialNavigationControl only fetches data on initial page load. If you have applications/web parts/user controls that will be present during postbacks, then you will want to persist these links somewhere like ViewState.

Reconstructing the SocialNavigationControl’s Javascript

Now for the hard part, filling in the conditionals above. The Welcome menu is comprised of three different controls, so they don’t all render the same way. The primary control, PersonalActions, renders the core menu and the bulk of the menu items in HTML. The SocialNavigationControl, however, uses javascript to find the existing menu on the page and then append its own items to it. It’s pretty ugly and hacky (from Reflector):

private void SetMyProfileMenuJavascript()

{

    string key = this.ClientID + "_insertMyProfileMenu";

    DebugStringBuilder builder = new DebugStringBuilder();

    builder.AppendLine("function " + key + "() {");

    builder.AppendLine("    var menus = document.getElementsByTagName('menu');");

    builder.AppendLine("    if (menus == null)  return;");

    builder.AppendLine("    var menu = null;");

    builder.AppendLine("    for (var i=0, len=menus.length; i<len; i++) {");

    builder.AppendLine("        if (menus[i].id.lastIndexOf('PersonalActionMenu') != -1) {");

    builder.AppendLine("tt       menu = menus[i]; break;");

    builder.AppendLine("        }");

    builder.AppendLine("    }");

    builder.AppendLine("    if (menu == null)   return;");

    bool flag = false;

    bool flag2 = false;

    if (!string.IsNullOrEmpty(this.ProfileURL))

    {

        flag = true;

        builder.AppendLine("    var elm = document.createElement('ie:menuitem');");

        builder.AppendLine("    elm.setAttribute('menugroupid', '50');");

        builder.AppendLine("    elm.setAttribute('description', '" + SPHttpUtility.EcmaScriptStringLiteralEncode(StringResourceManager.GetString(LocStringId.MyProfileMenu_description)) + "');");

        builder.AppendLine("    elm.setAttribute('text', '" + SPHttpUtility.EcmaScriptStringLiteralEncode(StringResourceManager.GetString(LocStringId.MyProfileMenu_text)) + "');");

        builder.AppendLine(@"    elm.setAttribute('onmenuclick', 'STSNavigate2(event,'" + SPHttpUtility.EcmaScriptStringLiteralEncode(this.ProfileURL) + @"')');");

        builder.AppendLine("    elm.setAttribute('id', 'ID_MySiteLinksMenu');");

    }

    if (!string.IsNullOrEmpty(this.MySiteHostURL))

    {

        flag2 = true;

        builder.AppendLine("    var elm2 = document.createElement('ie:menuitem');");

        builder.AppendLine("    elm2.setAttribute('menugroupid', '50');");

        builder.AppendLine("    elm2.setAttribute('description', '" + SPHttpUtility.EcmaScriptStringLiteralEncode(StringResourceManager.GetString(LocStringId.MultPages_MySite_Description)) + "');");

        builder.AppendLine("    elm2.setAttribute('text', '" + SPHttpUtility.EcmaScriptStringLiteralEncode(StringResourceManager.GetString(LocStringId.MultPages_MySite_Label)) + "');");

        builder.AppendLine(@"    elm2.setAttribute('onmenuclick', 'STSNavigate2(event,'" + SPHttpUtility.EcmaScriptStringLiteralEncode(this.MySiteHostURL) + @"')');");

        builder.AppendLine("    elm2.setAttribute('id', 'ID_MySiteMenu');");

    }

    builder.AppendLine("    var sep = null;");

    builder.AppendLine("    for (var i=0, len=menu.childNodes.length; i<len; i++) {");

    builder.AppendLine("    t   if (menu.childNodes[i].id == undefined || menu.childNodes[i].id == '') {");

    builder.AppendLine("            sep = menu.childNodes[i]; break;");

    builder.AppendLine("        }");

    builder.AppendLine("    }");

    if (flag)

    {

        builder.AppendLine("    if (sep != null)");

        builder.AppendLine("        menu.insertBefore(sep.cloneNode(true), menu.firstChild);");

        builder.AppendLine("    menu.insertBefore(elm, menu.firstChild);");

    }

    if (flag2)

    {

        builder.AppendLine("    if (sep != null)");

        builder.AppendLine("        menu.insertBefore(sep.cloneNode(true), menu.firstChild);");

        builder.AppendLine("    menu.insertBefore(elm2, menu.firstChild);");

    }

    builder.AppendLine("};");

    builder.AppendLine("_spBodyOnLoadFunctionNames.push('" + key + "');");

    this.Page.ClientScript.RegisterStartupScript(base.GetType(), key, builder.ToString(), true);

}

 

The OOB behavior for the Welcome menu will actually render two profile links if you have SharePoint Server installed:

  1. My Profile (linking to the My Site Host profile page)
  2. My Regional Settings (linking to userdisp.aspx, the SharePoint Foundation profile page)

The My Regional Settings link is added by the PersonalActions control (from Reflector):

PostCacheSubstitutionText text2 = new PostCacheSubstitutionText();

text2.TextType = PostCacheSubstitutionTextType.UserId;

text2.PrefixHtml = "<script type="text/javascript">n//<![CDATA[nvar _spUserId=";

text2.SuffixHtml = ";n//]]>n</script>";

this.Controls.Add(text2);

SPUser currentUser = this.Web.CurrentUser;

MenuItemTemplate menuItem = base.GetMenuItem("ID_PersonalInformation");

if (menuItem != null)

{

    if (currentUser == null)

    {

        menuItem.Visible = false;

    }

    else

    {

        string serverRelativeUrlFromUrl = this.Web.GetServerRelativeUrlFromUrl("_layouts/userdisp.aspx?Force=True&ID=");

        menuItem.ClientOnClickScript = "javascript:GoToPage('" + SPHttpUtility.EcmaScriptStringLiteralEncode(serverRelativeUrlFromUrl) + "' + _spUserId);return false;";

    }

}

My design called for only displaying one “Edit Profile” link, which should be smart enough to combine these two approaches, and link to userdisp.aspx if SharePoint Foundation used, and to the My Site Host profile page if available.

To achieve this, the first step is to adjust the ASCX markup to include some PlaceHolder controls that can be customized in code-behind:

<div class="profile_dropdown">

<asp:PlaceHolder ID="placeHolderUserID" runat="server"></asp:PlaceHolder>

    <div class="dropdown_bkgrd">

        <ul>

            <asp:PlaceHolder ID="placeHolderEditProfile" runat="server"><li><asp:LinkButton ID="linkButtonEditProfile" runat="server" EnableViewState="false" Text="Edit Profile"></asp:LinkButton></li></asp:PlaceHolder>

            <asp:PlaceHolder ID="placeHolderMySite" runat="server"><li><asp:LinkButton ID="linkButtonMySite" runat="server" EnableViewState="false" Text="My Site"></asp:LinkButton></li></asp:PlaceHolder>

            <li>Log Out</li>

            <li>Sign in as a Different User</li>

        </ul>

    </div>

</div>

Here is the code-behind I used that combines the output from the PersonalActions control and SocialNavigationControl:

string myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

string mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

 

if (!Page.IsPostBack)

{

    myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

    ViewState[Constants.VIEWSTATE_PROFILE_URL] = myProfileUrl;

    mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

    ViewState[Constants.VIEWSTATE_MYSITE_URL] = mySiteHostUrl;

}

else

{

    myProfileUrl = ViewState[Constants.VIEWSTATE_PROFILE_URL] as string;

    mySiteHostUrl = ViewState[Constants.VIEWSTATE_MYSITE_URL] as string;

}

 

if (string.IsNullOrEmpty(mySiteHostUrl))

{

    // No MySite host, or user does not have rights to view profile page...

    placeHolderMySite.Visible = false;

}

else

{

    // User has a MySite, show the link to it...

    placeHolderMySite.Visible = true;

    linkButtonMySite.OnClientClick = string.Format("STSNavigate2(event, '{0}');return false;", SPHttpUtility.EcmaScriptStringLiteralEncode(mySiteHostUrl));

}

if (string.IsNullOrEmpty(myProfileUrl))

{

    // Use the SharePoint foundation profile page, since the MySite host is not available

    PostCacheSubstitutionText pt = new PostCacheSubstitutionText();

    pt.TextType = PostCacheSubstitutionTextType.UserId;

    pt.PrefixHtml = "<script type="text/javascript">n//<![CDATA[nvar _spUserId=";

    pt.SuffixHtml = ";n//]]>n</script>";

    placeHolderUserID.Controls.Add(pt);

    string serverRelativeUrlFromUrl = this.ResolveClientUrl("~/_layouts/userdisp.aspx?Force=True&ID=");

    linkButtonEditProfile.OnClientClick = "GoToPage('" + SPHttpUtility.EcmaScriptStringLiteralEncode(serverRelativeUrlFromUrl) + "' + _spUserId);return false;";

}

else

{

    // Use the MySite profile page...

    linkButtonEditProfile.OnClientClick = string.Format("STSNavigate2(event, '{0}');return false;", SPHttpUtility.EcmaScriptStringLiteralEncode(myProfileUrl));

}

Summary

In this post, we deconstructed the SocialNavigationControl, and figured out how to render intelligent links to My Site and My Profile pages.

Our ASCX markup so far is:

<%@ 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" %>

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="CustomWelcomeMenuControl.ascx.cs" Inherits="CustomWelcomeMenu.ControlTemplates.CustomWelcomeMenu.CustomWelcomeMenuControl" %>

<div class="header">

    <img class="header-logo" src="/images/header_logo.png" alt="" width="169" height="78" />

    <div class="header-bar">

        <div class="profile_menu">

        <a href="#" class="profile_btn">

            <span class="image"><asp:Image ID="imageProfilePicture" runat="server" Width="20px" Height="20px" /></span>

            <span class="name"><asp:PlaceHolder ID="placeHolderWelcomeName" runat="server"></asp:PlaceHolder></span>

            <span class="arrow"></span>

        </a>

            <div class="profile_dropdown">

            <asp:PlaceHolder ID="placeHolderUserID" runat="server"></asp:PlaceHolder>

                <div class="dropdown_bkgrd">

                    <ul>

                        <asp:PlaceHolder ID="placeHolderEditProfile" runat="server"><li><asp:LinkButton ID="linkButtonEditProfile" runat="server" EnableViewState="false" Text="Edit Profile"></asp:LinkButton></li></asp:PlaceHolder>

                        <asp:PlaceHolder ID="placeHolderMySite" runat="server"><li><asp:LinkButton ID="linkButtonMySite" runat="server" EnableViewState="false" Text="My Site"></asp:LinkButton></li></asp:PlaceHolder>

                        <li>Log Out</li>

                        <li>Sign in as a Different User</li>

                    </ul>

                </div>

            </div>

        </div>

    </div>

</div>

 

Our ASCX code-behind so far is:

using System;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint;

using Microsoft.SharePoint.WebControls;

using Microsoft.SharePoint.Utilities;

 

namespace CustomWelcomeMenu.ControlTemplates.CustomWelcomeMenu

{

    public partial class CustomWelcomeMenuControl : UserControl

    {

        protected void Page_PreRender(object sender, EventArgs e)

        {    

    

            // Username

            PostCacheSubstitutionText welcomeText = new PostCacheSubstitutionText();

            welcomeText.TextType = PostCacheSubstitutionTextType.UserName;

            placeHolderWelcomeName.Controls.Add(welcomeText);

 

            // Profile picture

            SPList userInfoList = SPContext.Current.Web.GetCatalog(SPListTemplateType.UserInformation);

            SPListItem userProfileItem = userInfoList.GetItemById(SPContext.Current.Web.CurrentUser.ID);

            if (userProfileItem["Picture"] == null || string.IsNullOrEmpty(userProfileItem["Picture"].ToString()))

            {

                // Use default picture (or replace with another)

                imageProfilePicture.ImageUrl = "/_layouts/images/PERSON.GIF";

            }

            else

            {

                SPFieldUrlValue value = new SPFieldUrlValue(userProfileItem["Picture"].ToString());

                imageProfilePicture.ImageUrl = value.Url;

            }

 

                string myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

                string mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

    

            if (!Page.IsPostBack)

            {

                myProfileUrl = Context.Items["SocialData$ProfileURL"] as string;

                ViewState[Constants.VIEWSTATE_PROFILE_URL] = myProfileUrl;

                mySiteHostUrl = Context.Items["SocialData$MySiteHostURL"] as string;

                ViewState[Constants.VIEWSTATE_MYSITE_URL] = mySiteHostUrl;

            }

            else

            {

                myProfileUrl = ViewState[Constants.VIEWSTATE_PROFILE_URL] as string;

                mySiteHostUrl = ViewState[Constants.VIEWSTATE_MYSITE_URL] as string;

            }

    

    

            if (string.IsNullOrEmpty(mySiteHostUrl))

            {

                // No MySite host, or user does not have rights to view profile page...

                placeHolderMySite.Visible = false;

 

            }

            else

            {

                // User has a MySite, show the link to it...

                placeHolderMySite.Visible = true;

                linkButtonMySite.OnClientClick = string.Format("STSNavigate2(event, '{0}');return false;", SPHttpUtility.EcmaScriptStringLiteralEncode(mySiteHostUrl));

            }

    

            if (string.IsNullOrEmpty(myProfileUrl))

            {

                // Use the SharePoint foundation profile page, since the MySite host is not available

                PostCacheSubstitutionText pt = new PostCacheSubstitutionText();

                pt.TextType = PostCacheSubstitutionTextType.UserId;

                pt.PrefixHtml = "<script type="text/javascript">n//<![CDATA[nvar _spUserId=";

                pt.SuffixHtml = ";n//]]>n</script>";

                placeHolderUserID.Controls.Add(pt);

                string serverRelativeUrlFromUrl = this.ResolveClientUrl("~/_layouts/userdisp.aspx?Force=True&ID=");

                linkButtonEditProfile.OnClientClick = "GoToPage('" + SPHttpUtility.EcmaScriptStringLiteralEncode(serverRelativeUrlFromUrl) + "' + _spUserId);return false;";

            }

            else

            {

                // Use the MySite profile page...

                linkButtonEditProfile.OnClientClick = string.Format("STSNavigate2(event, '{0}');return false;", SPHttpUtility.EcmaScriptStringLiteralEncode(myProfileUrl));

            }

 

        }

    }

}

Next, check out Part 3, where we learn how to display links to Sign Out and Sign in as a Different User.