Hello everyone!
Today, I’m going to follow up on the post I wrote recently about how I created a custom navigation provider for SharePoint’s top navigation, and how it was quite a resource costly module, resulting in slowness of page loading, thus substracting from end user experience and satisfaction.
A number of customizations I already have in place, such as
- custom masterpage
- custom masterpage code behind c# file
- deployment of resources into SharePoint hive layouts folder
allowed me to piece together a working code that in essence
- Upon page load fired a JavaScript code that calls a web service
- Web service renders Microsoft.SharePoint.WebControls.AspMenu web control inside a surrogate page
- HTML is obtained from rendering the control and is returned to the main page as web service result
- Upon HTML recieval the JavaScript code appends this to original page
- At this point we have the HTML but we do not have all the JavaScript event hooks that AspMenu control applies to its HTML.
- I have figured out the code that re-applies the hooks in JavaScript, so this is the last step of my JS handler.
And, for the code itself
Firstly, masterpage markup and code-behind excerpts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<%@ Master Language="C#" Inherits="SP2013.Intranet.GenericClasses.IntranetMasterPage, SP2013.Intranet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=20648c82d4e0eddd" %> ... <%@ Register TagPrefix="Custom" Namespace="SP2013.Intranet.CustomWebControls" Assembly="SP2013.Intranet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=20648c82d4e0eddd" %> ... ... ... <SharePoint:AjaxDelta ID="DeltaTopNavigation" BlockElement="true" CssClass="ms-displayInline ms-core-navigation" role="navigation" runat="server"> <SharePoint:DelegateControl runat="server" ControlId="TopNavigationDataSource" ID="topNavigationDelegate"> <Template_Controls> <asp:SiteMapDataSource ShowStartingNode="False" SiteMapProvider="SPNavigationProvider" ID="topSiteMap" runat="server" StartingNodeUrl="sid:1002" /> </Template_Controls> </SharePoint:DelegateControl> <asp:ContentPlaceHolder ID="PlaceHolderTopNavBar" runat="server"> <!-- OUR CUSTOM WEB CONTROL HERE --> <Custom:AspMenuAsync runat="server" /> </asp:ContentPlaceHolder> </SharePoint:AjaxDelta> |
In the masterage code-behind we are injecting a javascript code into the page, that produces a variable called “currentNavigationProviderName” that holds the name of currently used global navigation provider. I chose this place to pull the value from, as at this point any possible delegate controls for top navigation data source have been applied and we are safe to retrieve the name.
Description how to create and use UlsLogging class can be found in my other post here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); try { DelegateControl topNavigationDelegate = (DelegateControl)Page.Master.FindControl("topNavigationDelegate"); if (topNavigationDelegate != null && topNavigationDelegate.Controls.Count > 0) { SiteMapDataSource dataSource = topNavigationDelegate.Controls[0] as SiteMapDataSource; if (dataSource != null) { Page.Master.FindControl("PlaceHolderBodyAreaClass").Controls.Add( new LiteralControl() { Text = "<script type='text/javascript'>var currentNavigationProviderName = '" + dataSource.SiteMapProvider + "';</script>" }); } } } catch (Exception ex) { UlsLogging.Current.LogError(UlsLogging.EventCategory.MASTERPAGE, ex.ToString()); } } |
AspMenuAsync web control
Nothing much here, we’re spawning a div that will hold the results from web service call and registering the javascript file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace SP2013.Intranet.CustomWebControls { public class AspMenuAsync : WebControl { protected override void CreateChildControls() { base.CreateChildControls(); this.Controls.Add(new LiteralControl(@" <div class='aspMenuAsyncContainer'></div> <script type='text/javascript' src='/_layouts/15/intranet/scripts/aspmenuasync.js?v" + Guid.NewGuid().ToString("D").Replace("-", string.Empty) + "'></script>")); } } } |
JS code calls custom web service (covered below), on successful receival of HTML appends it and re-applies the out of the box AspMenu control JS hooks on the created HTML.
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 |
_spBodyOnLoadFunctions.push(aspMenuAsyncStartup); function aspMenuAsyncStartup() { $('.aspMenuAsyncContainer').attr('style', 'display: inline-block;'); var context = new SP.ClientContext(); var relativeWebUrl = context.get_url(); var fullWebUrl = window.location.protocol + '//' + window.location.host + relativeWebUrl; var showStartingNode = false; var neededProviderName = ''; if (typeof (currentNavigationProviderName) == "undefined") { neededProviderName = 'SPNavigationProvider' showStartingNode = true; } else { neededProviderName = currentNavigationProviderName; } $.ajax({ type: 'POST', contentType: 'application/json; charset=utf-8', dataType: 'json', url: fullWebUrl + '/_layouts/15/Intranet/WebServices/AspMenuAsyncService.svc/getControlHtml', data: JSON.stringify({ currentNavigationProviderName: neededProviderName, showStartingNode: showStartingNode, unique: (new Date()).getTime() }), cache: false, success: function (data, status, jqXHR) { $('.aspMenuAsyncContainer').html(data); $create(SP.UI.AspMenu, null, null, null, $get('zz1a_TopNavigationMenu')); }, error: function (msgObj, url, status) { $('.aspMenuAsyncContainer').html(msgObj); } }); } |
Web service part
I will not delve deeply here on how to provision your web service, please refer to my earlier post, part of which describes the process.
Once you have web service provisioned with .svc and code-behind in place it’s time to look at my code-behind. What the web service does is create an instance of Page class, add our AspMenu control to it (you might want to tweak the property assignment to suit your needs) and then executes the page in-memory using the HttpContext.Current.Server.Execute method. Resulting HTML is put into a string writer and returned as HTML.
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 |
using SP2013.Intranet.CustomWebControls; using SP2013.Intranet.GenericClasses; using Microsoft.SharePoint.WebControls; using System.IO; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace SP2013.Intranet.WebServices { [ServiceContract(Namespace = "SP2013.Intranet.WebServices.AspMenuAsyncService")] interface IAspMenuAsyncService { [WebInvoke(UriTemplate = "/getControlHtml", Method = "POST", BodyStyle = WebMessageBodyStyle.WrappedRequest, ResponseFormat = WebMessageFormat.Json)] [OperationContract] string getControlHtml(string currentNavigationProviderName, bool showStartingNode); } [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class aspMenuAsyncService : IAspMenuAsyncService { public string getControlHtml(string currentNavigationProviderName, bool showStartingNode) { UlsLogging.Current.LogInformation(UlsLogging.EventCategory.PAGES_TREE_VIEW, "AspMenuAsyncService web service start"); ModifiedPage pageHolder = new ModifiedPage(); SiteMapDataSource topSiteMap = new SiteMapDataSource(); topSiteMap.ShowStartingNode = false; topSiteMap.SiteMapProvider = currentNavigationProviderName; topSiteMap.ID = "topSiteMap"; if (showStartingNode) { topSiteMap.StartingNodeUrl = "sid:1002"; } pageHolder.Controls.Add(topSiteMap); AspMenu menu = new AspMenu(); menu.AJAXRender = false; menu.ID = "TopNavigationMenu"; menu.EnableViewState = false; menu.DataSourceID = "topSiteMap"; menu.UseSimpleRendering = true; menu.UseSeparateCSS = false; menu.Orientation = System.Web.UI.WebControls.Orientation.Horizontal; menu.StaticDisplayLevels = 2; menu.AdjustForShowStartingNode = true; menu.MaximumDynamicDisplayLevels = 6; menu.SkipLinkText = string.Empty; pageHolder.Controls.Add(topSiteMap); pageHolder.Controls.Add(menu); StringWriter output = new StringWriter(); HttpContext.Current.Server.Execute(pageHolder, output, true); return output.ToString().Replace("zz1_", "zz1a_"); } private class ModifiedPage : Page { public override void VerifyRenderingInServerForm(Control control) { /* Do nothing */ } public override bool EnableEventValidation { get { return false; } set { /* Do nothing */} } } } } |
Piecing it all together
In the end we have decoupled loading of top navigation control from the main page loading cycle. This approach could be tailored to loading other types of controls, such as AspTreeView and the like.
Hope this information was valuable in some way, it surely has made UX of our end users more smooth.