Tuesday 17 January 2012

Cross Site Collection Navigation

Several days ago I thought how to show quick launch menu from a specific site. The main problem was in web application's structure. There was logical distinction between site collections. Some of them had to play a role of parent, other had to be its child. So, my task was to show menu from current site and its parent site collection.


SharePoint Server 2010 provides coolest functionality for the site navigation managment. If you activate Publishing feature, in Site Settings you can find a new menu which is called "Navigation" (Look and Feel section). There is an option that configures which items should be displayed in your current site navigation (quick launch menu). But this option works only with the webs that are in the same site collection.

picture 1

In case of cross collection site navigation you should investigate how this feature works and how can you reuse this functionality in your approach.

First of all, lets look at the master page markup and find out how quick launch menu is rendered.

<Sharepoint:SPNavigationManager 
     id="QuickLaunchNavigationManager" runat="server" QuickLaunchControlId="QuickLaunchMenu"     ContainedControl="QuickLaunch" EnableViewState="false" 
    CssClass="ms-quicklaunch-navmgr">
     <
div>
           <
SharePoint:DelegateControl runat="server" ControlId="QuickLaunchDataSource">
                  <
Template_Controls>
                         <
asp:SiteMapDataSource SiteMapProvider="SPNavigationProvider" ShowStartingNode="False" id="QuickLaunchSiteMap" 
                                                                  StartingNodeUrl="sid:1025"  runat="server" />
                 </
Template_Controls>
           </
SharePoint:DelegateControl>
           <
SharePoint:UIVersionedContent UIVersion="3" runat="server">
                 <
ContentTemplate>
                         <
SharePoint:AspMenu id="QuickLaunchMenu" runat="server" DataSourceId="QuickLaunchSiteMap" Orientation="Vertical" StaticDisplayLevels="2" 
                                                               ItemWrap="true" MaximumDynamicDisplayLevels="0" StaticSubMenuIndent="0" SkipLinkText="" CssClass="s4-die">
                                       <LevelMenuItemStyles>
                                              <
asp:menuitemstyle CssClass="ms-navheader" />
                                              <
asp:menuitemstyle CssClass="ms-navitem" />
                                      </
LevelMenuItemStyles>
                                      <
LevelSubMenuStyles>
                                              <
asp:submenustyle CssClass="ms-navSubMenu1" />
                                              <
asp:submenustyle CssClass="ms-navSubMenu2" />
                                      </
LevelSubMenuStyles>
                                      <
LevelSelectedStyles>
                                              <
asp:menuitemstyle CssClass="ms-selectednavheader" />
                                              <
asp:menuitemstyle CssClass="ms-selectednav" />
                                     </
LevelSelectedStyles>
                          </
SharePoint:AspMenu>
                </
ContentTemplate>
           </
SharePoint:UIVersionedContent>
           <
SharePoint:UIVersionedContent UIVersion="4" runat="server">
                <
ContentTemplate>
                          <
SharePoint:AspMenu id="V4QuickLaunchMenu" runat="server" EnableViewState="false" DataSourceId="QuickLaunchSiteMap"  
                                                               UseSimpleRendering="true"UseSeparateCss="false" Orientation="Vertical" StaticDisplayLevels="2" 
                                                               MaximumDynamicDisplayLevels="0" SkipLinkText="" CssClass="s4-ql" />
                </
ContentTemplate>
            </
SharePoint:UIVersionedContent>
    </
div>
</
Sharepoint:SPNavigationManager>
listing 1


The most interesting part of this markup is a delegate control definition.

<SharePoint:DelegateControl runat="server" ControlId="QuickLaunchDataSource">
         <
Template_Controls>
                 <
asp:SiteMapDataSource SiteMapProvider="SPNavigationProvider" ShowStartingNode="False" id="QuickLaunchSiteMap" StartingNodeUrl="sid:1025"   
                                                           runat="server" />
         </
Template_Controls>  
</SharePoint:DelegateControl>
listing 2

It means that the data source of quick launch menu can be changed by a feature. If you go to 14\TEMPLATE\FEATURES\Navigation\NavigationSiteSettings.xml, you can find that the Publising feature has a definition of QuickLaunchDataSource delegate control and this control is used as the data source for our menu.

<Control Id="QuickLaunchDataSource" Sequence="50" ControlClass="Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapDataSourceSwitch"
               ControlAssembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
           <
Property Name="ID">QuickLaunchSiteMap</Property>
           <
Property Name="SiteMapProvider">CurrentNavigation</Property>
           <
Property Name="EnableViewState">false</Property>
           <
Property Name="StartFromCurrentNode">true</Property>
           <
Property Name="ShowStartingNode">false</Property>
           <
Property Name="TrimNonCurrentTypes">Heading</Property>
</
Control>
listing 3

Now you know which markup and data source you should to reuse. But it is not pretty easy. The reason of this in PortalSiteMapDataSourceSwitch object. This object uses provider that is retrieves data which depends on HttpContext.Current. So if you wish to get quick launch menu from another site collection you should change current http context. Let's do it!

Open Visual Studion 2010 and choose Visual Web Part Template. Actually, we need to create a control that should be added to the master page, but in case of web part we can easily deploy it to any site for testing. Later we can delete this web part and leave only its user control.

As our control should display quick lanch navigation, open web part's control markup and copy code from master page that is represented in listing 1. If you deploy the solution and add the web part to a page you will see the quick launch navigation menu for current site.

Now we should change data source. As it is described before we should change http context and  create an instance of PortalSiteMapDataSourceSwitch object in it. For this purpose we can create a following class:

public class ContextSwitcher: IDisposable
{

           private
HttpContext _temp;
           private SPSite _site;
           private SPWeb _web;

           public
ContextSwitcher(string url)
          {
                      _site = new SPSite(url);
                      _web = _site.OpenWeb();
                      _temp = HttpContext.Current

                     HttpRequest request = new HttpRequest("", _web.Url, "");
                     request.
Browser = new HttpBrowserCapabilities();
                     HttpContext.Current = new HttpContext(request, new HttpResponse(new StringWriter()));
                     HttpContext.Current.Items["HttpHandlerSPWeb"] = _web;
          }


          public
void Dispose()
         {
                     HttpContext.Current = _temp;
                     if (_web != null) _web.Dispose();
                     if (_site != null) _site.Dispose();
          }
}
listing 4

Then we can create method for data retrieving. In this method we should configure the data source as it was done in delegate control of publishing feature. But in our case we have a little shade. The PortalSiteMapDataSourceSwitch object is inherited from SiteMapDataSource that is a static class. This class is initialized on an application pool's start, thus you should invoke OnInit method by hand. The simplest way to do this is to create your own data source class, inherit this class from PortalSiteMapDataSourceSwitch and implement initialization method.

public class Source : PortalSiteMapDataSourceSwitch
{

           public
void Initialize()
          {
                      base.OnInit(EventArgs.Empty);
          }
}
listing 5

This method should be invoked when the settings for this data source are set.

private SiteMapDataSource GetCurrentNavigationSource()
{
           
           Source source = new Source();
           source.
EnableViewState = false;
           source.
StartFromCurrentNode = true;
           source.
ShowStartingNode = false;
           source.
TrimNonCurrentTypes = NodeTypes.Heading;
           source.
SiteMapProvider = "CurrentNavigation";
          

           source.Initialize();

           return source;
}
listing 6

Now all preparations are done and we can bind quick launch navigation menu by our data source.

protected void Page_Load(object sender, EventArgs e)
{

          using
(new ContextSwitcher(Url))
         {

                    BindNavigation(GetCurrentNavigationSource);
          }
}


private
void BindNavigation(Func<SiteMapDataSource> getSource)
{

          BindNavigation(QuickLaunchMenu, getSource);
          BindNavigation(V4QuickLaunchMenu, getSource);
}


private
void BindNavigation(AspMenu menu, Func<SiteMapDataSource> getSource)
{
          if (menu != null)
          {
                   menu.
DataSource = getSource.Invoke();
                   menu.
DataBind();
          }
}
listing 7

This approach is flexible, you  can get navigation from any site of your web application by site url.

3 comments:

  1. Would this example work for the top navigation to allow multiple site collections to share navigation from another site collection?
    I am not sure where all the example code gets deployed.
    I know I need a Feature to override the existing delegate control for the the data source for the navigation but not sure what the master page changes would be and how to deploy the custom class etc. Do you have a working example of the master page and Feature etc?

    ReplyDelete
  2. Please ask there - http://spsite.pro/Blog/Lists/Posts/Post.aspx?List=509f0903-7838-4b1d-8fe3-13f0de43ca5e&ID=3. This blog is moved there.

    ReplyDelete
  3. I offer a solution to navigate between and aggregate data from multiple site collections using managed metadata. Using a centralized site collection provisioning tool and stamping the (inheritable) metadata onto the site collection a lean but mean search-based navigation can be implemented. It offers you similar functionality as SharePoint 2013 will with its Search Query Webpart. Have a look: http://www.sharepointconsultant.ch/location-based-tagging/

    ReplyDelete