Saturday, 17 March 2012

Custom SharePoint Top Menu and QuickLaunch menu using SiteMapProviders

SharePoint MasterPages, when you open one of them the first time, look like a mix of html, .net 2.0 control definitions, inline CSS and javascript. Initially they look a bit daunting but, when you work on them for a while, they start to make more sense. I am not going to do a deep dive in the SharePoint Masterpages subject because there are plenty of online resources explaining their ins and outs.

This post will focus on how to customize SharePoint Navigation controls, such as the top menu and the quicklaunch menu, and the starting point is the MasterPage. I want to achieve the necessary menu customizations re-using SharePoint menus instead of deleting-commenting-replacing them with our custom ascx controls at MasterPage level.

SharePoint menus are not just objects to show menu items, they also apply caching and leverage security completely in synch with the SharePoint Object Model. If I replace those OOTB menu controls with my custom ones, I would also need to consider the effort of re-inventing the wheel to cache and secure their content. So, although initially it might look easier to just use our custom ascx controls, we would end up working a lot more than just re-using what’s been provided to us by Microsoft and the SharePoint team.

Delegate Controls wrapping SiteMapProviders?

When you take a second, or third, look in a MasterPage with the knowledge that gives you how SharePoint works, a world of possibilities appears in front of you.

One of the things that can give you, as a SharePoint developer, a lot of customization power is what’s called DelegateControls. I will not get into a deep dive of DelegateControls either; maybe with another post I will add more meat on the bone to this topic but, just to give you an idea of how powerful DelegateControls can be for you, you can replace objects wrapped in delegate controls with your custom ones, or even add your custom objects without replacing the existing ones. In some scenarios you don’t want to replace existing controls but to add your custom ones to the Delegate Control wrapper. For instance, there is a delegate control for html page header elements, so you could add your own html header elements without affecting the existing ones.

With this in mind, and our SharePoint MasterPage open, we can see the following two delegate controls that are related to the SharePoint menus:


1. Top Navigation Menu
The top navigation menu is rendered with the AspMenu. This control renders the content received from the DataSource. We can see in this vanilla, not customized, SharePoint Masterpage that the AspMenu is using the topSiteMap DataSource

<SharePoint:AspMenu
 ID="TopNavigationMenuV4"
 Runat="server"
 EnableViewState="false"
 DataSourceID="topSiteMap"
 AccessKey="<%$Resources:wss,navigation_accesskey%>"
 UseSimpleRendering="true"
 UseSeparateCss="false"
 Orientation="Horizontal"
 StaticDisplayLevels="2"
 MaximumDynamicDisplayLevels="1"
 SkipLinkText=""
 CssClass="s4-tn"/>

Just following the AspMenu html mark-up we find the SiteMapDataSource with id topSiteMap. The good news about it is that it is wrapped in a DelegateControl and therefore we could replace, or override, that datasource with our custom one. We will use the DelegateControl ControlId TopNavigationDataSource later on in order to replace it.

<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>


2. QuickLaunch Menu
The same happens to the QuickLaunch Menu because it is using another AspMenu to render its content and the only difference is that it is being fed from a different SiteMapDataSource, identified as QuickLaunchSiteMap, and also wrapped within a DelegateControl.

<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>

Now that we have located what we want to replace, how could we do it? We can use Visual Studio to create our custom SiteMapProvider, publish it to the GAC, as part of our project, and some small editing in web.config will get us up and running without major issues. I would say that editing the web.config manually is a dirty solution and there are ways to avoid editing it manually. You can check it in another of my blog posts. (off-topic: I find writing blog posts an interesting and rewarding experience because you find new topics to clarify and blog about while writing something not entirely related to them)

There is an existing MSDN article showing how to create a SiteMapProvider step by step, but it is not completely accurate and following that article can create more problems than necessary. One of the wrong assumptions in it says that you need to deploy your custom SiteMapProvider to the bin folder and increase the security trust to full-trust, again manually editing the web.config. This will not reach a production environment in any serious organization; at least I would never allow it, because allowing full trust to binaries running in a site collection could indeed put in serious risk the company, server and/or data. Security should be kept to minimal trust in order to protect the server and our organization. ALWAYS. No exceptions.

Creating our custom SiteMapProvider step by step

Create an empty SharePoint project (I’ve named mine as CustomSPNavigationProvider) and make sure you have/add the following two references:

Microsoft.SharePoint.Publishing
System.Web


Add a new c# class to the project. I have named my class MyCustomSiteMapProvider and this is where all the logic to create the menu items will take place.



In this example I’m just hardcoding a couple of nodes but you could build your own logic to answer any specific requirement. For instance, creating as many nodes as necessary from a configuration list and reproducing the security at item level while rendering the menu so the user will see links where he/she has access to.


We have our custom PortalSiteMapProvider but if we deploy the solution as it is, SharePoint will not behave any different and we want to switch our navigation on demand. To get us there we need to replace the delegate control TopNavigationDataSource with our custom provider.

To replace a delegate control we need to add a SharePoint Empty Element. I’ve called mine ControlsToOverride because this is exactly what we want to do, override existing delegate controls.



And edit the elements.xml file as follows. The only part that might be different in your environment would be the one to select the appropriate SiteMapProvider, in this case MyCustomNavigationProvider. This name is the one we will add to the SiteMap Providers nodes in the web.config. You will see it below, in the last step.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Sequence="30"
      Id="TopNavigationDataSource"
      ControlClass="System.Web.UI.WebControls.SiteMapDataSource"
      ControlAssembly="System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
    <Property Name="ID">topSiteMap</Property>
    <Property Name="SiteMapProvider">MyCustomNavigationProvider</Property>
    <Property Name="EnableViewState">false</Property>
    <Property Name="ShowStartingNode">true</Property>
  </Control>
</Elements>


When you add an Empty Control, a new Web scoped feature is added to your solution and the empty element will be part of that feature. This will leverage a custom Site Map Provider on demand. So when the feature is activated a custom top menu bar will be used and when it will be de-activated, the top menu bar that’s been set up in Site Settings will be used instead.

This is how the complete solution will look like in Visual Studio. As you can see, you don’t need a lot of work to create your custom provider.



The last step is to edit the web.config manually, adding the following line to the <providers> section within siteMap. As I said earlier, this step will be done manually in this blog post, but I would use a better approach to reach a production environment because our custom code needs to be repeatable and completely encapsulated in our WSP’s. (Check this blog post to do this from code)

Important! This web.config information needs to match your assembly/project/Class and PublicKeyToken. The information you see here would work for my project only. (Thanks to some feedback I've seen some people were just copy pasting this into their web.config without editing it to match their own project)

    <siteMap defaultProvider="CurrentNavigation" enabled="true">
      <providers>
  <add name="MyCustomNavigationProvider" type="CustomSPNavigationProvider.MyCustomSiteMapProvider, CustomSPNavigationProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a9a625f2ea253ae" NavigationType="Global" />
...
...
</providers>

The result
You are ready now to deploy the solution and activate the feature in any web that needs your custom top navigation.

Before activating it:



After the feature is activated:


[SharePoint 2013 Update] Things will be a lot easier as you can see here... 

66 comments:

  1. Hi,
    Very good article. It would be great if you post the entire solution in a download section.

    ReplyDelete
  2. Hi Sun,

    First of all, thanks for your time and feedback because it is greatly appreciated. I created this blog with the intention to, first, use it as a personal reference and second to help people in the community to learn the ins and outs of SharePoint development.

    I have been mentoring developers, and development teams, on how to get the most out of the SharePoint platform, learning the important key factors and leaving out all the SharePoint “noise”, a few years already. I have not been visible in the SharePoint community until recently but I've been around since the dark ages, around late SharePoint 2001 and early SharePoint 2003!! :)

    At some point in this mentoring experienced I realized that if you gave the code to people, all the meaning and reasons about “where, what, why and how” were left behind. Developers are code centric but SharePoint development is not a complex platform from a pure development perspective, it is a complex platform if you do not pay enough attention to detail and you need to get to know the ins and outs doing each step manually. There are two ways to learn something, memorizing it or practising it. I really encourage the later.

    Whenever I will blog about something more complex, that requires a kind of series material to work with etc… I will make that material available to be downloaded, but I don’t think this example requires that.

    Thanks again for your time and feedback and let us know how you get on with it!
    Dani

    ReplyDelete
  3. Hi Daniel,

    I followed your example pretty closely, with the exception that I derived my navigation provider from StaticSiteMapProvider and I'm unable to get the provider deployed using your method. Is your deployment method specific to providers derived from PortalSiteMapProvider or am I doing something else wrong.

    Regards,

    Fergie

    ReplyDelete
  4. Hi Fergie,

    The StaticSiteMapProvider is not a class used within the Microsoft.SharePoint.Publishing.Navigation namespace

    SharePoint navigation controls need to use some of the Navigation Providers or custom providers inherited from them. If you wanted to create a custom class, derived from StaticSiteMapProvider, you would need to implement all the other methods leveraged by the SharePoint Navigation providers out of the box.

    You can find here the link to the msdn article showing a list of what's available in the Navigation workspace...

    http://msdn.microsoft.com/en-us/library/ms570756.aspx

    I hope it helps and thanks for reading the blog!

    ReplyDelete
  5. I tried several times to get this deployment method to work but I was never able to get my site to load after adding the provider to the web.config. After deployment and load my site fails to load the files referenced in the web.config entry as evidenced by this message and confirmed in the stack trace:

    Could not load file or assembly 'SPListDrivenNavigationProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a9a625f2ea253ae' or one of its dependencies. The system cannot find the file specified

    I'm not sure what I did wrong, as I pretty faithfully followed the steps. One observation is that my feature name never updated when I added the empty control and updated elements.xml. It stayed Feature1 and I was reluctant to rename it. I think I referenced everything correctly according to the example in the elements.xml file, so not sure what happened. How the heck do you debug these things??

    ReplyDelete
    Replies
    1. Hi,

      It would be difficult to know what went wrong with your custom navigation provider, but I can help you out with your question about how to debug it. Make sure your feature is not activated by default just after it is deployed. You can change that in Visual Studio. Once you have deployed your bits and pieces, attach all w3wp.exe processes in your Visual Studio solution, add a break-point to your code and then go to your SharePoint site and activate the feature... you should be able to go step by step from this point.

      Delete
  6. Hi Daniel!
    I followed your steps, but after deploying the solution and activating the feature I got the following exeption:
    "The DataSourceID of 'V4QuickLaunchMenu' must be the ID of a control of type IHierarchicalDataSource. A control with ID 'QuickLaunchSiteMap' could not be found."

    After retracting the solution everything goes fine. I have no idea what can be wrong, i did nothing with 'V4QuickLaunchMenu' and 'QuickLaunchSiteMap'.

    ReplyDelete
    Replies
    1. Hi Gábor,

      As usual, it is difficult to troubleshoot remotely. I can say that sometimes SharePoint errors might be a bit misleading. What I would check is:

      - That you override TopNavigationDataSource and that this delegate control exists in your MasterPage.

      - The error could be in your code and it is breaking your custom SiteMapProvider. Then the error is misleading you to believe it is somewhere else, but if you retract your custom feature and things start to work again... I would create a no code custom site map provider instead. Or just debug the code after deploying it without activating it.

      I hope it helps!

      Delete
    2. Hi Daniel, thank you for the great info. However, I too am getting the above error and have looked through everything. Has anyone found a resolution to this issue?

      Delete
    3. Hi,

      Have you seen my last update related to the web.config update? It might be related to that, you need to modify the web.config file with the information about your dll not just copy-paste mine.

      Delete
    4. I've received this error too and it has nothing to do with V4QuickLaunchMenu.
      I got this error when I had retracted the solution but left the Provider entry in the web.config.
      I also got this error when the PublicKeyToken in the Provider entry was wrong. (I had re-signed the assembly which changes the key.)
      Finally, as Daniel suggests, if your custom provider has a defect, it may yield this misleading error. Comment out your node-building code. Don't even build any child nodes. Just return the base node collection.
      Doing all of these things should eliminate this error. Then re-implement your code carefully. Perhaps incrementally.

      Delete
  7. Hi Daniel,
    I thought I've already followed step by step but I kept getting errors.
    1. Create the project as guided.
    2. Deploy the application
    3. Modify the web.config.



    4. Restart IIS
    5. Open the site
    6. I got this error "The given assembly name or codebase was invalid. (Exception from HRESULT: 0x80131047)"

    Solution: https://docs.google.com/open?id=0Bzcfp7pLJ_N0dnR4LXhjaGpnSXc

    ReplyDelete
  8. If you are having problems with this example, it is probably because you are using the public key of his assembly and assembly name instead of your projects.

    eg, his project name is CustomSPNavigationProvider and his assembly is CustomSPNavigationProvider.MyCustomSiteMapProvider, CustomSPNavigationProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a9a625f2ea253ae

    this should be {your project}.{your provider} and your publickey which can be found in the assembly folder. The web.config would need to reflect your project info, not his.

    ReplyDelete
    Replies
    1. Hi Anonymous!

      Thanks for providing a possible solution to this. I thought that step was clear enough but I will edit my post now to highlight that the assembly description needs to match their own solution, not mine. :)

      Delete
  9. Hi Daniel,

    no problem. It seemed like the other were having problems because of this.

    So, I got your solution to work. My question is, how could we create nodes from a custom list? I'm new to development in SharePoint, .net C# also. I'm working on my first custom top nav solution to create a mega menu type nav. I want to add the ability to incorporate web services in the menu, so I thought the best way to do this would be to use a custom list to populate the PortalSiteMapProvider. I could then focus on the integration of the web services into the without worrying about the node items. I created a timer job to scan the web application for all root and sub-web sites and record them in a custom list.

    ReplyDelete
  10. Hi Matthew,

    There would be a lot of options to achieve that. For instance, with this SPContext.Current.Site.RootWeb.GetList you could get an instance of your list, assuming your list is there, and just crawl it, building the appropriate SiteMapNodes iteratively. I think you are almost there already :)

    Good luck with it and thanks for your feedback and comments.

    ReplyDelete
    Replies
    1. Creating a menu in this manner, will this provide caching, etc? Or will I need to create the cashing myself?

      I guess what I'm trying to get at is will I be able to use all the features of the PortalSiteMapProvider, or will there be limitations?

      Thanks

      Delete
    2. Hi Matthew,

      You should be able to use all the features of the PortalSiteMapProvider but you will need to take care of the caching for your nodes, the other ones will be receiving that from SharePoint OOTB.

      I'm sure you know about .net caching, but if you need to double check different options you could have a look on the 3rd point of this post:

      http://www.helpmeonsharepoint.com/2012/06/fine-tune-sharepoint-performance-with.html

      Answering your specific scenario two options could be used: Caching per user or singleton caching per server and shared with all users.

      I hope it helps!

      Delete
  11. Hello Daniel

    I have done exactly as you have mentioned in the article. The solution is deployed & feature is activated. But I dont see the custom menu. Please help if possible.

    Thanks.
    Divesh

    ReplyDelete
    Replies
    1. Hi Devish,

      I can only point you to the ULS logs and see if you can find more information there in order to troubleshoot your specific issue. ULS logs are the right place to go if you cannot see anything useful in the UI. I'm sure there must be something logged in there.

      Delete
    2. Hi Daniel,

      Another question. Can I use the code (or some other piece of code) to have a custom menu till a 3rd or 4th level. It means that a menu item has a child which has another child & it has another child. I tried with above piece of code & it does not work.

      Regards.
      Divesh

      Delete
    3. Hi Divesh... it should work tweaking the correct properties...

      Investigate MaximumDynamicDisplayLevels in the menu control. This should get you started.

      http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.menu.maximumdynamicdisplaylevels.aspx

      Delete
    4. Hey Dan,

      I have a question. I used the custom menu as mentioned above. We have subsites that use the same menu. In addition to that, we want to add another navigation within a subsite that is only for that subsite. Further, this navigation should be permission controlled so that every one should not see every link to navigate. Please let me know how I can do this.

      Delete
    5. Hi Divesh,

      You could do a simple if spcontext.current.site or spcontect.current.web is whatever build this menu... if not build the other one. With this you should get different menu's for sites and subsites.

      Then the access rights... if you are keeping your links in a separate list, if you set up access rights individually per item in that list and you query it without running with elevated privileges, then you should get the result for what the logged in user has access to see.

      This is one way, there could be some others but this should get you started!

      Good luck with it!

      Delete
  12. Hi Daniel,

    In your example, is there a way to hide or remove the first node so as to use only the nodes produced by your code?

    I created the ability to pull from a list which has all sites in it. I am getting duplication because of the root node being displayed. Would I need to use a custom control to override the navigation user control to do this?

    Thanks

    Matthew

    ReplyDelete
    Replies
    1. Hi Matthew,

      You could set the ShowStartingNode to false.



      ShowStartingNode
      Affects whether the starting node is returned by the data source.
      When this property is set to true, the data source returns the starting node. The menu receives the starting node, which can be the root node, and items below the starting node.
      When this property is set to false, the data source does not return the starting node. The menu receives only the items below the starting node.

      http://msdn.microsoft.com/en-us/library/bb897657.aspx

      Delete
    2. This comment has been removed by the author.

      Delete
  13. I thought so, but it doesn't seem to function as it should.

    in the master I have

    SharePoint:AspMenu
    ID="TopNavigationMenuV4"
    Runat="server"
    EnableViewState="false"
    DataSourceID="topSiteMap"
    AccessKey="<%$Resources:wss,navigation_accesskey%>"
    UseSimpleRendering="true"
    UseSeparateCss="false"
    Orientation="Horizontal"
    StaticDisplayLevels="2"
    MaximumDynamicDisplayLevels="1"
    SkipLinkText=""
    CssClass="s4-tn"
    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

    and not matter what I do, it still displays the root node. I have even changed the SiteMapProvider from SPNavigationProvider to CombinedNavSiteMapProvider and removed the StartingNodeUrl="sid:1002" as I have read in other posts, but still nothing.

    Any Ideas? I'm working with SharePoint 2010 Enterprise with publishing enabled and a custom Master Page derived from the v4.master.

    Thanks

    ReplyDelete
  14. Never mind, I'm a dummy. I was changing the ShowStartingNode on the master page and not in the override control element of my code. It's working now.

    ReplyDelete
  15. Great Matthew... so now you are up and running :)

    Congratulations and keep trying!!!

    ReplyDelete
  16. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Hi Matthew... I can see you found the solution then? :)

      Delete
  17. Hi Dan,

    as you know I started creating my own nav. I ran into a problem in my solution and when debugging it I realized I couldn't reference the ParentNode of my nav items. I redid your example above and on debugging found the same issue. The error is

    base {"Unable to cast object of type 'System.Web.SiteMapNode' to type 'Microsoft.SharePoint.Publishing.Navigation.PortalSiteMapNode'."} System.SystemException {System.InvalidCastException}

    which seems to affect NextSibling and Previous Sibling also. Have you encountered this? I have need to reference the ParentNode for what i am doing. Any Ideas?

    Thanks

    ReplyDelete
    Replies
    1. Hi Matthew,

      I never encountered that problem because I never had to access the ParentNode in any of my specific requirements/projects. I would need to investigate it as well... but seeing now SharePoint 2013 and their Term Store Navigation, these things will be a lot easier in the next version :) I might blog about it if I have some time in the next days :)

      Delete
  18. Ya know, you could have saved tons of time and effort with answering questions if you had just posted a fully working solution that readers could download and deploy themselves. I don't think anyone just wants to selfishly have something handed to them, but having something that is fully working allows them to then step thru the pieces to see HOW it works instead of wasting time debugging things that were just missing in your post. I'm not sure what you gained by refusing to do that but then giving vague answers to all sorts of abstract questions for the next 6 months. Just sayin'...

    ReplyDelete
    Replies
    1. Hi anonymous,

      No problem on answering questions to people that try to make the example work, because actually there is nothing missing here to make it work. The code is so simple that it doesn't make sense to step through while debugging. The main issue with the example is how to wire things together and that's why writing an article, explaining each step, seemed like the best approach. If you check the MSDN article I referred to, you are not given any of that.

      If you have a look to the rest of my blog posts you will see I do not post full solutions. The main reason for that, under my humble opinion, is that if you want to learn SharePoint development for real you actually need to try it yourself. I would say this idea is valid to anything else in life, for example, learning how to snowboard online watching videos is not the same as going down the slopes yourself :) Feel free to try it, post your question here and let everybody learn from your experience.

      Delete
  19. If you ever do go down the slopes, I hope you get a map showing you the way and a set of working skiis. Hopefully you don't get at the top of the mountain and somebody tells you that the best way to learn the mountain is to crash into some trees, trip over rocks, fall off cliffs, etc.

    ReplyDelete
    Replies
    1. I learned to ride my board exactly the way you describe and I had a lot of fun :) I understand not everybody can learn in the same way, so they can always get an instructor to avoid those falls... although sooner or later you need to do it on your own and you will fall. As you can see there is no learning without failing at some point. There are teachers/instructors/websites explaining the basics about SharePoint development and this blog post was just an exercise with everything you need step by step. As you can see, there are a lot of people that has been able to complete the example and even some of them have created a codeplex solution based on it. They have not said so here, but I know they did :)

      Good luck with your SharePoint development and post here your questions if you feel like it. Everybody will learn/share from it.

      Delete
    2. Someone is angry. We should be greatful for people sharing, kinda spoiled to yell at people for stuff we didn't get for free.

      Delete
  20. Read some posts about making cross site-collection navigation e.g. by calling a web-service (http://sadomovalex.blogspot.se/2010/12/cross-site-and-cross-site-collection.html).

    The solution provided in this great post should be able to do the same with less effort, reading the "source" Site Collection nodes:

    public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
    {
    var sourceProvider = new PortalSiteMapProvider();
    sourceProvider.CurrentSite = sourceSite;
    sourceProvider.CurrentWeb = sourceSite.RootWeb;
    return sourceProvider.GetChildNodes(node);
    }

    Or maybee I've misunderstood something.

    ReplyDelete
    Replies
    1. Hi,

      No... it is actually another solution using the same technique. What I have described in this blog post is how to customize the SiteMapProviders using custom logic and how to override delegate controls. The link you mention goes one step further in order to share the same navigation across site collections using a WebService. If what you had to do would be to copy/share the same navigation between site collections, your GetChildNodes would be a good starting point.

      Thanks for sharing your thoughts and ideas on this...

      Delete
  21. Your post is very helpful and I am able to make my example working with your approach.

    I see an issue with this approach for below scenario:

    I have multiple master pages for various web applications which uses same datasource TopNavigationDataSource.

    I have activated the web scoped feature to override the delegate control. Other web applications are all broken as they are using different master pages but the top navigation datasource is same TopNavigationDataSource.

    Did you come across with this issue? I think, in this scenario, I should create a custom delegate control instead of overriding the delegate control.

    Sai

    ReplyDelete
    Replies
    1. Hi Sai,

      Initially other Web Apps or Site collections shouldn't be affected by overriding a delegate control through a feature. That override should be only affecting the Site RootWeb wherever your feature is activated. So initially the error shouldn't be related to overriding the feature but it might be related to some error in your code within the GetChildNodes method.

      What you could do, in order to troubleshoot it, would be to change this line slightly... for instance check the name of your site in a hard-coded way.

      if (pNode.Type == NodeTypes.Area && pNode.WebId == SPContext.Current.Site.RootWeb.ID)

      Make sure that you retract your custom solution correctly, specially the dll in the gac, before deploying again. Sometimes the system keeps in cache old versions of dll files deployed to the gac. So making a methodical clean-up between deployments is always a good practice in these situations.

      I hope it helps...

      Delete
    2. Thanks Daniel.

      Your inputs helped and the issue is resolved.

      Appreciate your immediate reply. Thank you for blogging this article.

      Delete
  22. Mate

    Can this be used on Sharepoint foundation or is publishing required ?

    Thanks

    Dan

    ReplyDelete
    Replies
    1. Hi Dan,

      Sorry for the late answer. I have tried the code in my local environment without any publishing, standard or enterprise feature activated in a team site and it works without any problem. I was expecting that but I wanted to be sure before writing an answer to your question.

      I hope it helps!

      Delete
  23. It seems as though someone is helping themselves to your content!
    http://sampathnarsingam.blogspot.com/2012/10/custom-sharepoint-top-menu-and.html

    ReplyDelete
    Replies
    1. Hi! Thanks for letting me know!! It looks like people do not take even the time to update the links and they copy paste my blog entirely without putting a reference to it.

      Thanks for that... I will leave a comment and see if they publish it...

      Delete
    2. None of his posts has original content. He is getting the content from blogs all around the place and copy-pasting everything as it would be his own content. It is a shame there are "professionals" like that...

      Well, at least the community will realize Sampath Kumar is a fake and cannot do anything on his own.

      Delete
  24. Hi Daniel,
    Thanks for the great entry. I was able to make it work without a problem. I have one issue though. It always shows entries from the root of the site collection. I need it to show entries from the sub site when on a sub site. Whenever I try and point to the sub web through a portal site map provider I loose all other entries. Thanks for your help.

    John

    ReplyDelete
    Replies
    1. Hi John,

      Make sure the subsite navigation is not set as "use the same as the parent" or root. You can change that navigation settings using the site actions -> Site settings -> Navigation

      Delete
    2. Hi John,

      Also, if what you need is to add your menu option removing everything that is provided by the SiteMapNodeCollection, instead of the line below, create an empty SiteMapNodeCollection object without nodes or elements in it. Then you will have your own custom menu.

      SiteMapNodeCollection nodeColl = base.GetChildNodes(pNode);

      Delete
    3. Have the same issue. It's using the same nodes for both navigation controls.
      I think for the leftnavigation (quicknavigation, current navigation) you need to inherit from a different type of navigation provider. Not sure which one..

      Delete
    4. Found the solution 5min after posting a comment...

      In the override of the GetChildNodes function
      Replace
      SiteMapNodeCollection nodes = base.GetChildNodes(topNavigation);
      with
      var provider = PortalSiteMapProvider.CurrentNavSiteMapProvider;
      SiteMapNodeCollection nodes = provider.GetChildNodes(provider.CurrentNode);

      Your 'nodes' variable will now contain the correct nodes of the left navigation.

      Delete
  25. Daniel - this is not working for me, and when I debug - the GetChildNodes method is never called. Any thoughts as to why? I've made sure to override and breakpoint all of the overloads for the method. The initialize method IS running, but that only clarifies that the web.config entry is correct and the custom control is loading.

    Code is:
    namespace CustomNav
    {
    public class TeamSiteNavigationProvider : PortalSiteMapProvider
    {

    web.config entry is:


    (and we know that much is working)
    ________________________

    I've verified that I'm pointing to the right master page by removing the top nav element altogether (the menu does disappear as it should on page reload)

    and here is my whole placeholder element for the topnav there are some useless attributes here (starting url in both elements) but they should not be a problem:





















    ReplyDelete
    Replies
    1. Hi,

      If your breakpoint is not reached in Visual Studio, but it is actually executed... have you tried to:
      - IISReset and then access the site from Internet Explorer, then set the breakpoint in Visual Studio and attach to the appropriate w3wp.exe service from Visual Studio. This should be the process...

      - If even trying the above you cannot reach the breakpoint, start to debug the old way. Comment out everything that is "custom" and just call the base function return base.GetChildNodes(pNode);
      this should make your code execute but behave as the one out of the box and then start to add your logic slowly until something breaks, then you will know the line creating the issue and then you can investigate what's up with it.

      Delete
  26. looks like my element markup didn't post - replacing markup brackets with pipes...

    |asp:ContentPlaceHolder id="PlaceHolderHorizontalNav" runat="server"|
    |SharePoint:AspMenu
    ID="TopNavigationMenu"
    Runat="server"
    DataSourceID="topSiteMap"
    EnableViewState="true"
    AccessKey="|%$Resources:wss,navigation_accesskey%|"
    UseSimpleRendering="true"
    UseSeparateCss="false"
    Orientation="Horizontal"
    StaticDisplayLevels="2"
    MaximumDynamicDisplayLevels="1"
    DynamicHorizontalOffset="0"
    StaticPopoutImageUrl="/_layouts/images/menudark.gif"
    StaticPopoutImageTextFormatString=""
    DynamicHoverStyle-BackColor="#CBE3F0"
    SkipLinkText=""
    StaticSubMenuIndent="0"
    StartingNodeUrl="sid:1002"
    CssClass="ms-topNavContainer"|
    |StaticMenuStyle/|
    |StaticMenuItemStyle CssClass="ms-topnav" ItemSpacing="0px"/|
    |StaticSelectedStyle CssClass="ms-topnavselected" /|
    |StaticHoverStyle CssClass="ms-topNavHover" /|
    |DynamicMenuStyle BackColor="#F2F3F4" BorderColor="#A7B4CE" BorderWidth="1px"/|
    |DynamicMenuItemStyle CssClass="ms-topNavFlyOuts"/|
    |DynamicHoverStyle CssClass="ms-topNavFlyOutsHover"/|
    |DynamicSelectedStyle CssClass="ms-topNavFlyOutsSelected"/|
    |/SharePoint:AspMenu|

    |SharePoint:DelegateControl runat="server" ControlId="TopNavigationDataSource" Id="topNavigationDelegate"|
    |Template_Controls|

    |asp:SiteMapDataSource
    ShowStartingNode="True"
    SiteMapProvider="TeamSiteNavigationProvider"
    id="topSiteMap"
    runat="server"
    StartingNodeUrl="sid:1002"/|

    |/Template_Controls|
    |/SharePoint:DelegateControl|

    |/asp:ContentPlaceHolder|

    ReplyDelete
  27. on my last post (also anonymous) - is there a problem with using this solution in a publishing site?

    ReplyDelete
    Replies
    1. Hi,

      Yes, it should work in publishing sites without any issue. I tried in some time ago and it was working just fine.

      Delete
    2. Thanks, i figured it out, but I don't like the solution. I ditched the delegate control around the data source element in the master page (the one that we re-direct to our custom source.) I don't know why that has to be.

      I also found that you don't get the same stylings on your custom menus as the default nav with SPNavProv, even with the same settings on your ASPMenu element. I was hoping to just tack menu nodes onto the existing site nav. I've found I can only get that right by skipping the provider and moving up the chain, to the actual top nav

      Delete
  28. still m getting this error "The DataSourceID of 'TopNavigationMenu' must be the ID of a control of type IHierarchicalDataSource. A control with ID 'topSiteMap' could not be found."
    PLZ HELP!!!!!!

    ReplyDelete
  29. Hey Why would you use Navigation provider and use code . Why not use simple SPXmlContentMapProvider. Solution you have provided in not a good solution as you would need to deploy assebmly every time you have a change in navigation.

    Try this
    http://akhileshgandhi.wordpress.com/2012/10/12/sharepoint-multi-level-custom-navigation/

    ReplyDelete
    Replies
    1. I think your solution will be valid for static menu options, but if you want to show/hide different menu options based on some custom logic, so the menu options are not the same for all users, your solution lacks that flexibility.

      Obviously this was a simple example and I wouldn't deploy to a real project hard coded links within the code. You could store all menu options and logic in custom lists and then the administrator could change them as and when needed without deploying anything. You might have missed the point but I hope it is clearer now and gives you more options for your future solutions and projects.

      Delete
  30. great article.

    Quick question is there a way to add the node so it appears as the first item? I tried changing nodeColl.Add(childNode) to nodeColl.Insert(0,childNode) but it still adds it as the second item.

    ReplyDelete
  31. Hello Daniel!

    I'm following your tutorial to create a custom SiteMapProvider and like some other I got this problem:
    "The DataSourceID of 'V4QuickLaunchMenu' must be the ID of a control of type IHierarchicalDataSource. A control with ID 'QuickLaunchSiteMap' could not be found."

    I want to customize the QuickLaunch (so that I can create a hierarchial structure using pages that are in the same documentlibrary). I'm using the DelegateControl for the QuickLaunch, but I don't know how to change the properties here:



    I'm using your code in my CustomSiteMapProvider, just to try the look of it before trying to modify the code that would suit my needs.

    It seems like it's with the ControlsToOverride and the Elements.xml-file (the code is the same as yours) that the problem lie, because when I include it in my feature and deploy it, I get the error message from above. When I exclude it from the project and deploy it again, the error is gone again.

    As for the web.config-file (from the folder \14\CONFIG\) it looks like this:




    ...
    ...


    PublicKeyToken= the same as the other sitemapproviders in the web.config for my farm.

    Hope you understand my problems and can help.

    /Erica

    ReplyDelete