iPhone Detected, site running in minimal mode.
Home     Tags/Archives     Tweets     About Kevin

​I just spent a bunch of time working around a SharePoint bug related to manipulating the order that navigation items appear in a MOSS Publishing site on the TopNavigationBar.  Here I will spell out what you need to get to get various pieces of this to work- but first, let's define what we are trying to accomplish.

Say you have a site that has been created in some way.  It doesn't really matter if it was declaritively deployed from a Site Definition, restored from a backup, generated by a SharePoint Deployment Wizard CMP, or built up manually from code.  The thing is once it's done being created you need to modify from code various aspects of the top navigation bar including manually setting the order of the items.  Again, it doesn't matter where this code is running from, whether it be a PowerShell script, a command line application, or a suicide webpart placed on the site.  Ok, all that being said, *MY* specific requirements were to:

  • Change Top (global) Nav to Include Subwebs, Hide Pages, and support manual (non-automatic) ordering
  • Hide one of the subwebs
  • Reorder the other subwebs to a custom (ie. arbitrary) order
  • All changes must be done fully in code with no "manual human steps" required as it was part of an automated deployment script.
The good news is that to do the first two bullets, things pretty much work like they are supposed to, although finding exactly how to do some of that took a bit of work, so I'll reproduce them here.  Unfortunately for the third bullet there's a bug that gets in the way and so weird workarounds are required.

First of all, my code segments below will assume you have referenced Microsoft.SharePoint.dll and Microsoft.SharePoint.Publishing.dll and have the following usings at the top of your file:

   1:  using Microsoft.SharePoint;
   2:  using Microsoft.SharePoint.Publishing;
   3:  using Microsoft.SharePoint.Navigation;​
   4:  using System.Collections.Generic;

To set the different properties of the root web's navigation settings simply do the following:

   1:  using (var site = new SPSite(webUrl))
   2:  {
   3:      var rootPubweb = PublishingWeb.GetPublishingWeb(site.RootWeb);
   4:      rootPubweb.NavigationOrderingMethod = OrderingMethod.Manual;
   5:      rootPubweb.IncludeSubSitesInNavigation = true;
   6:      rootPubweb.IncludePagesInNavigation = false;
   7:      rootPubweb.Update();
   8:  }

Now, if you want to hide a particular subweb from the top navigation, you actually need to set the property of the subweb, not a property in the root web or it's navigation collections.  So, you would add the following (inside the SPSite using statement):

   1:  var subwebs = rootPubweb.GetPublishingWebs();
   2:  subwebs["subwebToHide"].IncludeInGlobalNavigation = false;
   3:  subwebs["subwebToHide"].Update();
   4:  rootPubweb.Update();

So far so good.  Now when it gets to actually specifying the order of the nodes, the documentation that you will find will direct you to get an SPNavigationNodeCollection from rootWebInstance.Navigation.TopNavigationBar.  If you iterate over the SPNavigationNode 's in this collection you can set their order by using the .MoveToFirst, .MoveToLast, and .Move methods.  Unfortunately on a newly build publishing site this collection will not contain any of your subwebs, only the top level pages (even though we specified to NOT include pages and to include subwebs).  In scouring the internet I found that others had indeed come across this bug as well.  What's odd is that if you use the SharePoint GUI and go to the root web's navigation settings (/_layouts/AreaNavigationSettings.aspx), select any subweb in the "Global Navigation" tree and click one of the Move buttons, then some magic code runs behind the scenes and the Navigation.TopNavigationBar collection will now be properly populated (and you won't need the following work around any more for that particular site).  However, as my requirement was to not require any manual steps I had to go on to develop the workaround below.

So I embarked on an effort to reproduce the "magic code" that initialized the navigation collection properly.  There was much trial and error involved while making incremental changes and then interogating object properties with PowerShell, etc...  In the end the successful strategy was to create the navigation nodes manually (in code).  The trickiest part though was getting them to be set as "internal" and not "external" links (this is a read-only property on each NavigationNode)-- it turned out the node's Url property had to be set just right for it to work.  Here is my workaround code:

   1:  using (var site = new SPSite(webUrl))
   2:  {
   3:      var rootPubweb = PublishingWeb.GetPublishingWeb(site.RootWeb);
   4:      var subwebs = rootPubweb.GetPublishingWebs();
   5:     
   6:      //Build a temporary hash of the node titles (so we can use them instead of index numbers)
   7:      SPNavigationNodeCollection nodes = site.RootWeb.Navigation.TopNavigationBar;
   8:      Dictionary<string, SPNavigationNode> nodeHash = new Dictionary<string, SPNavigationNode>();
   9:      for (int x = 0; x < nodes.Count; x++)
  10:      {
  11:          nodeHash.Add(nodes[x].Title, nodes[x]);
  12:      }
  13:      
  14:      //This block fixes the evil SharePoint defect ------------------------
  15:      foreach (PublishingWeb subweb in subwebs)
  16:      {
  17:          if (!nodeHash.ContainsKey(subweb.Title))
  18:          {
  19:              string relUrl = subweb.ParentPublishingWeb.Uri.MakeRelativeUri(subweb.Uri).ToString();
  20:              SPNavigationNode nn =
  21:                  Microsoft.SharePoint.Publishing.Navigation.SPNavigationSiteMapNode.CreateSPNavigationNode(
  22:                      subweb.Title, relUrl, NodeTypes.Area, site.RootWeb.Navigation.TopNavigationBar);
  23:              nn.Update();
  24:          }
  25:      }
  26:      site.RootWeb.Update();
  27:      //---------------------------------------------------------------------
  28:   
  29:      //Rebuild the hash table since now we should have the correct nodes!
  30:      nodeHash = new Dictionary<string, SPNavigationNode>();
  31:      for (int x = 0; x < nodes.Count; x++)
  32:      {
  33:          nodeHash.Add(nodes[x].Title, nodes[x]);
  34:      }
  35:   
  36:      string[] subwebTitlesInOrder = {"FirstNavItem", "SecondNavItem", "ThirdNavItem", "FourthNavItem"};
  37:      
  38:      //Order the nodes in the proper order
  39:      SPNavigationNode currentNode;
  40:      SPNavigationNode previousNode=null;
  41:   
  42:      for (int i = 0; i < subwebTitlesInOrder.Length; i++)
  43:      {
  44:          currentNode = nodeHash[subwebTitlesInOrder[i]];
  45:          if (i == 0)
  46:          {
  47:              currentNode.MoveToFirst(nodes);
  48:          }
  49:          else
  50:          {
  51:              currentNode.Move(nodes, previousNode);
  52:          }
  53:          previousNode = currentNode;
  54:      }
  55:      site.RootWeb.Update();
  56:  }
And that's it!
​​


IndyTechFest Logo
 
Seems like I keep getting behind on posting these (at least every other one-- then I catch up by posting two :)  Anyway a few weeks back I did a presentation here in Indianapolis for the Indy TechFest.  I had a great audience both in size, over 50, and in makeup-- almost everyone there was a developer, which is a first for me when giving this DEV presentation.  This time around I did my "Leveraging SharePoint 2010 as a Social Computing Development Platform" session, and it was the first time that I presented it on the RTM bits (and it all worked! yey!).
 
As usual, I didn't do slides, only a mindmap, which can be downloaded here.  If you'd like the sample code, read my blog posts from SPS Charlotte and nSpin as they have a link and some pointers as how to get it all to work.
 
Thanks to the event organizers, sponsors, volunteers, and attendees (especially all of you that came to my session!) for a great day!


This past weekend I presented at the SharePoint Saturday event in Charlotte, NC.  I had a great time both during the event and hanging out before and after with folks.
 
My session was in the first slot (8:30AM) and I presented my "Leveraging SharePoint 2010 as a Social Computing Development Platform".  Now even under normal circumstances that's a pretty heavy way to start off a morning (it's a 300 level developer presentation).  However, after I polled the audience at the start, I found I had a full room with only a handful of developers.  So I kind of changed my presentation on the fly and did quite a bit more demo'ing of the social computing features (from an end-user perspective) and completely cut out my 3rd demo (creating an activity feed custom gatherer for twitter).  All in all I think it went quite well!
 
For those that were at the session (or my other ones in the past) know that I had an aversion to slide based presentations.  However, posted below is both the mindmap version of the presentation (just click on the thumbnail to get a very large pannable image) as well as a slide deck version (just pretend the SPSIndy logo is SPSCLT).
Presentation Mindmap
 
PowerPoint
 
Code SamplesAnd finally, as promised, the icon to the right will get you a zip of all the code samples I used in the presentation.  If you are going to try and actually make this code work your self, you should probably go read this post, as it has more information about each VS project and what you may need to do to get it all working (read the comments too).
 
I want to thank the SPSCLT organizers, especially Brian Gough and Dan Lewis, the speakers, sponsors, and attendees for a great event!  I'm very happy I made the trip down and hope to see some of you at other events very soon!


Here is the mindmap and code samples from the presentation I gave last night (2010-Jan-14) at nSpin entitled "Overview of the SP2010 Social Platform for Developers".  NOTE: This is NOT the presentation I'm giving at SPSINDY.  To view/download the whole mindmap just click on the thumb below:
Mindmap Thumb
As for the codesamples, here are a couple notes:
  • General
    • These are all VS2010 projects
    • You will need to change some of the hardcoded SPSite URLS to yours- anywhere you see http://sp2010
    • The first and third projects must be run on a SharePoint 2010 server.  As such, you need to insure that your project's framework targets are .Net 3.5 and the CPU targets are either x64 or AnyCPU -- do not try to use x86 for these.  The second project doesn't matter.  If you load the solution file, these will all be set properly.
  • The first project is a visual webpart (AreaSpecialist) that accesses the user profile manager to pull down out a profile image, and allows the user to "give them Kudos" as it tracks their peer rating.
    • You'll need to change the deploy location in the project settings.
    • Remember to create the custom property in the user profile service application called "KudosRating" of type integer.
    • This version of the code has the elevated privilege implemented.
  • The second project is a windows form application (SocialBrowser) that allows a user to browse around your portal or the internet and it automatically adds any comments that have been made by folks within your organization to the datagrid at the bottom by accessing the SocialData webservice.
    • You will need to change the URL for the social data webservice reference, and then choose "update webservice".
  • The third project is a command line application that is a custom gatherer that imports and publishes twitter feeds to the SharePoint activity feed.
    • Don't forget to create the custom profile property "TwitterAccount" and fill in a couple in your user's profiles.
    • You need to run the "RegisterTwitterActivityApplication" at least once successfully (it's presently commented out in Program.Main).
    • After you register the twitter application, doublecheck that "Twitter" is available as an ActivityType at the bottom of your "Edit Profile" screen (and check the box if it isn't already).
    • Remember to deploy the RESX files to \14\Resources
    • Remember to deploy the twitter image (if you want to use it) to \14\Templates\Images
    • This code was cobbled together from the official custom gatherer sample as well as this blog post and this one.

To download all the sample code projects click the image below:

Codesample Download Thumb

All content posted here is free for anyone else to (re)use so long as you attribute it.  If I get at least 10 requests in the replies to this blog post, I will record a camtasia screencast and post it for everyone that couldn't attend last night.


I’ve seen a few questions out on Twitter in the past few weeks on how to change SharePoint page behavior based on querystrings passed in an URL.  While the context for this question leads to many possible alternatives (filter web parts, custom web part, xslt, js, etc…)  I found that the advice I was giving in most cases was “if you just need something quick (and dirty), then just inject some javascript.  It seems though that arn’t many easily findable tutorials around for how to do this, so I decided to write this article.  Please note that this is NOT the best way to accomplish every possible problem scenario taking into account security, maintainability, performance, etc…  It does however work which makes it a viable tool to add to your bag of tricks in my book.  Also, I’m going to present the code samples here using straight javascript, but you could certainly streamline some of the code by using other frameworks like jQuery.

So to begin, the first thing we need is some javascript that can read the querystring and parse it into a JS array for us to use later.  There are a ton of these snippits all around the web, here’s the one I use:

function getQueryStringArray() {
    var qs = location.search.substring(1, location.search.length);
    var args = qs.split("&");
    var vals = new Object();
    for (var i=0; i < args.length; i++) {
        var nameVal = args[i].split("=");
        var temp = unescape(nameVal[1]).split('+');
        nameVal[1] = temp.join(' ');
        vals[nameVal[0]] = nameVal[1];
    }
    return vals;
}

Ok, so now we need a function to “find” the control we’re after on the page so that we can do stuff to it (like set it’s value, or hide it).  I have found that the easiest way of doing this is to look for an element that has the right TagName (e.g. “input”), the right SharePoint Datatype (e.g. “TextField”), and SharePoint FieldName (e.g. “Title”).  The TagName is easy, since that’s the actual tag of the element in the DOM.  The SharePoint Datatype is actually appended to the END of the ID attribute, so a little parsing will be required.  Finally, the SharePoint FieldName is the value assigned to the TITLE attribute of the element.  It would be cool if we could just key off this alone, but obviously on any given page you can’t guarantee all items will have a unique TITLE attribute.  Here is an example of an element that you may want to target on a form’s NewForm.aspx or EditForm.aspx pages:

<input name="ctl00$m$g_beee8c5c_b1a1_4356_84f5_
462f43dc6b4a$ctl00 $ctl04$ctl09$ctl04$ctl00$ctl00$TextField" type="text" maxlength="255" id="ctl00_m_g_beee8c5c_b1a1_4356_84f5_
462f43dc6b4a_ctl00_ctl04_ctl09_ctl04_ctl00
_ctl00_TextField" title="RelatedCT" class="ms-long" />

And here is an example function that you could use to parse the DOM and return the target element based on these three pieces of information:

function getFieldElement(tagName, identifier, title) {
  var len = identifier.length;
  var tags = document.getElementsByTagName(tagName);
  for (var i=0; i < tags.length; i++) {
    var tempString = tags[i].id;
    if (tags[i].title == title &&
            (identifier == "" ||
             tempString.indexOf(identifier) == tempString.length – len
            )
       ) {
         return tags[i];
    }
  }
  return null;
}

// So for Example, to return the target element above we’d make the following call
var theElement = getFieldElement(“input”, “TextField”, “RelatedCT”);

Once you have a handle to the element, you can do all sorts of things with it.  For exmaple, setting it’s value, making it readonly (if you don’t want to the user to change it’s value once you’ve populated it from the querystring), or even hiding the whole field all-together.  Here is an example function that can does all of the above (note, slight changes are required to work with non-textfields such as checkboxes, dropdowns, lookup lists, etc.)

function processField(theFieldName, theValue, setReadOnly, setHidden) {
    var el=getFieldElement("input", "TextField", theFieldName);
    if(el!=null) {
        if(theValue!=null) {  el.value=theValue;  }
        if(setReadOnly) {  theField.readOnly=true;  }
        if(setHidden) { 
            //Set the whole table row to not display (includes label and all)
            theField.parentElement.parentElement.parentElement
        .style.display="none";
        }
    }
}

Ok, now that we have all of our helper functions ready to go, we need to put them all in a JS file and drop that where it can be accessed from the form pages (or where ever you plan to do the injection).  Possible places to consider are a document library (if you don’t have physical access to the server) or  12/TEMPLATE/LAYOUTS/1033 (if you do have physical server access).  The nice thing about the later location is that you can either directly link to the script from anywhere via /_layouts, OR you can use the  <SharePoint:ScriptLink> tag to have SharePoint automatically generate a non-client-cachable link to the script.  You will also need to use the _spBodyOnLoadFunctionNames.push method on the pages to get SharePoint to correctly insert you “injected” code into the proper spot in the whole JS Init of the pages (remember there are all sorts of things SharePoint pages need to run before we get to your injected code).  When I edit/customize a list’s NewForm.aspx or EditForm.aspx page I generally like to inject this bit of code at the very top of the PlaceHolderMain content placeholder.  Here is an example that links to my saved helper functions from above, pushes a function onto the OnLoad chain, and then set’s the values and visibility of 3 fields from the querystring.

<asp:Content ContentPlaceHolderID=”PlaceHolderMain” runat=”server”>
     <SharePoint:ScriptLink runat=”server” Name=”ListFuncs.js”/>
     <script type=”text/javascript”>
          _spBodyOnloadFunctionNames.push(“processFields”);
          function processFields() {
               var vals=getQueryStringArray();
               processField(“TaskList”, vals[“List”], true, false);
               processField(“TaskID”, vals[“TaskID”], true, true);
               processField(“OriginalAuthor”, vals[“author”], false, false);
          }
     </script>
[ … rest of the form …]
</asp:Content>

While this article certainly was an exhaustive tutorial on how to do this form of SharePoint customization, I’m hoping it will be enough to get some of you started in the right direction and experimenting.

  << Older      [more posts]   

RSS FeedBack to the HomepageMy Twitter FeedMy Stumbles

Tags

Hide Low Frequency Tags

Archives

Recent Posts