Replacing Navigation URLs Using STSADM
I was working on a project last Fall where a client of mine had a single site collection for their entire document library which was expected to be over 1TB. As a result of the large size of the site collection we decided to break it up into multiple site collections each contained within their own content database (we ended up with 12 in the end). The problem was that when we migrated all the libraries to the new site collections we ended up with hundreds of broken links due to navigation items (as well as web parts and list items) pointing to the original document libraries. The client was prepared to manually go through all the links to correct them but this just seemed a bit crazy to me so I quickly threw together a new command which would recursively go through all the webs and fix any navigation links that were pointing to the old content (I already had something for the web parts and list items). I named this command gl-replacenavigationurls. I actually had this command completed and available since November some time but I completely forgot about it so it never got documented – oops
. I wonder if there’s any other commands that I’ve created but didn’t document? Hmm…
The complete code is shown below (note that at present I’m only supporting MOSS for this one as I’ve not had time to do any WSS rework for it):
1: #if MOSS
2: using System;
3: using System.Collections.Generic;
4: using System.Collections.Specialized;
5: using System.Text;
6: using System.Text.RegularExpressions;
7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
8: using Microsoft.SharePoint;
9: using Microsoft.SharePoint.Administration;
10: using Microsoft.SharePoint.Navigation;
11: using Microsoft.SharePoint.Publishing;
12: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
13: using Microsoft.SharePoint.Publishing.Navigation;
14:
15: namespace Lapointe.SharePoint.STSADM.Commands.SiteCollectionSettings
16: {
17: public class ReplaceNavigationUrls : SPOperation
18: {
19: private static Regex m_searchString;
20: private static string m_replaceString;
21:
22: /// <summary>
23: /// Initializes a new instance of the <see cref="ReplaceNavigationUrls"/> class.
24: /// </summary>
25: public ReplaceNavigationUrls()
26: {
27: SPParamCollection parameters = new SPParamCollection();
28: parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify the site collection."));
29: parameters.Add(new SPParam("scope", "s", false, "site", new SPRegexValidator("(?i:^WebApplication$|^Site$|^Web$)")));
30: parameters.Add(new SPParam("searchstring", "search", true, null, new SPNonEmptyValidator(), "Please specify the search string."));
31: parameters.Add(new SPParam("replacestring", "replace", true, null, new SPNullOrNonEmptyValidator(), "Please specify the replace string."));
32: parameters.Add(new SPParam("quiet", "q"));
33:
34: StringBuilder sb = new StringBuilder();
35: sb.Append("\r\n\r\nReplaces URL values in the current and global navigation matching the provided search pattern.\r\n\r\nParameters:");
36: sb.Append("\r\n\t-url <url to search>");
37: sb.Append("\r\n\t-searchstring <regular expression string to search for>");
38: sb.Append("\r\n\t-replacestring <replacement string>");
39: sb.Append("\r\n\t[-quiet]");
40: sb.Append("\r\n\t[-scope <WebApplication | Site | Web> (defaults to Site)]");
41:
42: Init(parameters, sb.ToString());
43: }
44:
45: #region ISPStsadmCommand Members
46:
47: /// <summary>
48: /// Gets the help message.
49: /// </summary>
50: /// <param name="command">The command.</param>
51: /// <returns></returns>
52: public override string GetHelpMessage(string command)
53: {
54: return HelpMessage;
55: }
56:
57: /// <summary>
58: /// Runs the specified command.
59: /// </summary>
60: /// <param name="command">The command.</param>
61: /// <param name="keyValues">The key values.</param>
62: /// <param name="output">The output.</param>
63: /// <returns></returns>
64: public override int Execute(string command, StringDictionary keyValues, out string output)
65: {
66: output = string.Empty;
67:
68: Verbose = !Params["quiet"].UserTypedIn;
69: string url = Params["url"].Value.TrimEnd('/');
70: string scope = Params["scope"].Value.ToLowerInvariant();
71: m_searchString = new Regex(Params["searchstring"].Value);
72: m_replaceString = Params["replacestring"].Value;
73:
74: Log("Start Time: {0}", DateTime.Now.ToString());
75: SPEnumerator en;
76: switch (scope)
77: {
78: case "webapplication":
79: en = new SPEnumerator(SPWebApplication.Lookup(new Uri(url)));
80: en.SPWebEnumerated += SPWebEnumerated;
81: en.Enumerate();
82: break;
83: case "site":
84: using (SPSite site = new SPSite(url))
85: {
86: en = new SPEnumerator(site);
87: en.SPWebEnumerated += SPWebEnumerated;
88: en.Enumerate();
89: }
90: break;
91: case "web":
92: using (SPSite site = new SPSite(url))
93: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
94: {
95: en = new SPEnumerator(web);
96: en.SPWebEnumerated += SPWebEnumerated;
97: en.Enumerate();
98: }
99: break;
100: }
101: Log("Finish Time: {0}\r\n", DateTime.Now.ToString());
102:
103: return OUTPUT_SUCCESS;
104: }
105:
106: #endregion
107:
108: /// <summary>
109: /// Handles the enumerated event for each web within the scope.
110: /// </summary>
111: /// <param name="sender">The sender.</param>
112: /// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPWebEventArgs"/> instance containing the event data.</param>
113: private static void SPWebEnumerated(object sender, SPEnumerator.SPWebEventArgs e)
114: {
115: Log("Progress: Processing \"{0}\".", e.Web.Url);
116:
117: if (PublishingWeb.IsPublishingWeb(e.Web))
118: {
119: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(e.Web);
120: ReplaceUrls(e.Web, pubweb.GlobalNavigationNodes, true);
121: ReplaceUrls(e.Web, pubweb.CurrentNavigationNodes, false);
122: }
123: else
124: {
125: ReplaceUrls(e.Web, e.Web.Navigation.GlobalNodes, true);
126: ReplaceUrls(e.Web, e.Web.Navigation.TopNavigationBar, true);
127: ReplaceUrls(e.Web, e.Web.Navigation.QuickLaunch, false);
128: }
129: }
130:
131: /// <summary>
132: /// Replaces the urls within each node in the collection.
133: /// </summary>
134: /// <param name="web">The web.</param>
135: /// <param name="nodes">The nodes.</param>
136: /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
137: private static void ReplaceUrls(SPWeb web, SPNavigationNodeCollection nodes, bool isGlobal)
138: {
139: if (nodes == null || nodes.Count == 0)
140: return;
141:
142: List<SPNavigationNode> toUpdate = new List<SPNavigationNode>();
143: foreach (SPNavigationNode node in nodes)
144: {
145: if (m_searchString.IsMatch(node.Url))
146: toUpdate.Add(node);
147:
148: ReplaceUrls(web, node.Children, isGlobal);
149: }
150:
151: foreach (SPNavigationNode node in toUpdate)
152: {
153: string result = m_searchString.Replace(node.Url, m_replaceString);
154:
155: Log("Progress: Replacing \"{0}\" with \"{1}\".", node.Url, result);
156:
157:
158: NodeTypes type = NodeTypes.None;
159: if (node.Properties["NodeType"] != null && !string.IsNullOrEmpty(node.Properties["NodeType"].ToString()))
160: type = (NodeTypes)Enum.Parse(typeof(NodeTypes), node.Properties["NodeType"].ToString());
161:
162: if (type == NodeTypes.Area ||
163: type == NodeTypes.Page ||
164: type == NodeTypes.None ||
165: type == NodeTypes.List ||
166: type == NodeTypes.ListItem ||
167: type == NodeTypes.Heading)
168: {
169: CreateNode(web, node, result, nodes, isGlobal);
170: }
171: else
172: {
173: string oldUrl = node.Url;
174: node.Url = result;
175: try
176: {
177: node.Update();
178: }
179: catch
180: {
181: //Console.WriteLine("New Url={0}, Type={2}, Children={1}", node.Url, node.Children.Count, node.Properties["NodeType"]);
182: node.Url = oldUrl;
183: CreateNode(web, node, result, nodes, isGlobal);
184: }
185: }
186: }
187: }
188:
189: /// <summary>
190: /// Creates the node.
191: /// </summary>
192: /// <param name="web">The web.</param>
193: /// <param name="sourceNode">The source node.</param>
194: /// <param name="url">The URL.</param>
195: /// <param name="nodes">The nodes.</param>
196: /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
197: private static void CreateNode(SPWeb web, SPNavigationNode sourceNode, string url, SPNavigationNodeCollection nodes, bool isGlobal)
198: {
199: NodeTypes type = NodeTypes.None;
200: if (sourceNode.Properties["NodeType"] != null && !string.IsNullOrEmpty(sourceNode.Properties["NodeType"].ToString()))
201: type = (NodeTypes)Enum.Parse(typeof(NodeTypes), sourceNode.Properties["NodeType"].ToString());
202:
203: NodeTypes newType = type;
204: if (type == NodeTypes.Area)
205: newType = NodeTypes.AuthoredLinkToWeb;
206: else if (type == NodeTypes.Page)
207: newType = NodeTypes.AuthoredLinkToPage;
208: else if (type == NodeTypes.List || type == NodeTypes.ListItem)
209: newType = NodeTypes.AuthoredLink;
210:
211: SPNavigationNode newNode = SPNavigationSiteMapNode.CreateSPNavigationNode(
212: sourceNode.Title, url, newType, nodes);
213:
214: newNode.Properties["CreatedDate"] = sourceNode.Properties["CreatedDate"];
215: newNode.Properties["LastModifiedDate"] = sourceNode.Properties["LastModifiedDate"];
216: newNode.Properties["Description"] = sourceNode.Properties["Description"];
217: newNode.Properties["Target"] = sourceNode.Properties["Target"];
218:
219: newNode.Update();
220:
221: newNode.Move(nodes, sourceNode);
222:
223: Hide(web, sourceNode, type, isGlobal);
224: }
225:
226: /// <summary>
227: /// Hides the specified pub web.
228: /// </summary>
229: /// <param name="web">The pub web.</param>
230: /// <param name="node">The node.</param>
231: /// <param name="type">The type.</param>
232: /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
233: private static void Hide(SPWeb web, SPNavigationNode node, NodeTypes type, bool isGlobal)
234: {
235: if (type == NodeTypes.Area)
236: {
237: SPWeb childWeb = null;
238: string name = node.Url.Trim('/');
239: if (name.Length != 0 && name.IndexOf("/") > 0)
240: {
241: name = name.Substring(name.LastIndexOf('/') + 1);
242: }
243: try
244: {
245: childWeb = web.Webs[name];
246: }
247: catch (ArgumentException)
248: {
249: }
250:
251: if (childWeb != null && childWeb.Exists && childWeb.ServerRelativeUrl.ToLower() == node.Url.ToLower() && PublishingWeb.IsPublishingWeb(childWeb))
252: {
253: PublishingWeb tempPubWeb = PublishingWeb.GetPublishingWeb(childWeb);
254: if (isGlobal)
255: tempPubWeb.IncludeInGlobalNavigation = false;
256: else
257: tempPubWeb.IncludeInCurrentNavigation = false;
258: tempPubWeb.Update();
259: }
260: else
261: {
262: try
263: {
264: node.Delete();
265: }
266: catch (SPException)
267: {
268: }
269: }
270: }
271: else if (type == NodeTypes.Page)
272: {
273: PublishingPage page = null;
274: try
275: {
276: if (PublishingWeb.IsPublishingWeb(web))
277: {
278: PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(web);
279: page = pubWeb.GetPublishingPages()[node.Url];
280: }
281: else
282: {
283: try
284: {
285: node.Delete();
286: }
287: catch (SPException)
288: {
289: }
290: }
291: }
292: catch (ArgumentException)
293: {
294: }
295: if (page != null)
296: {
297: if (isGlobal)
298: page.IncludeInGlobalNavigation = false;
299: else
300: page.IncludeInCurrentNavigation = false;
301: page.Update();
302: }
303: }
304: else
305: node.Delete();
306: }
307: }
308: }
309: #endif
The help for the command is shown below:
C:\>stsadm -help gl-replacenavigationurls
stsadm -o gl-replacenavigationurls
Replaces URL values in the current and global navigation matching the provided search pattern.
Parameters:
-url <url to search>
-searchstring <regular expression string to search for>
-replacestring <replacement string>
[-quiet]
[-scope <WebApplication | Site | Web> (defaults to Site)]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-replacenavigationurls | MOSS 2007 | Released: 1/18/2009
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| url | Yes | URL of the web application or site collection. | -url "http://portal" | |
| searchstring | search | Yes | The regular expression search string. | -searchstring "(?i:/doccenter/IT)"
-search "(?i:/doccenter/IT)" |
| replacestring | replace | Yes | The replace string. | -replacestring "/docs/IT"
-replace "/docs/IT" |
| quiet | q | No | Specify to suppress status information while the command is running. | -quiet
-q |
| scope | s | No – defaults to site | The scope to use. Valid values are “WebApplication”, “Site”, and “Web”. | -scope site
-s site |
The following is an example of how to replace all references to “/doccenter/IT” with “/docs/IT”:
stsadm -o gl-replacenavigationurls –url "http://portal" –searchstring "(?i:/doccenter/IT)" –replacestring "/docs/IT" –scope WebApplication
Set Navigation Settings
A while back I had created a command which allowed you to set the navigation elements/nodes for a given site. The command allowed you to also set a couple of basic switches that appear on the Site Navigation Settings page (Home > Site Settings > Modify Navigation) but it didn't allow you to set all the flags. I thought about possibly modifying this command to allow all the flags to be set but then decided it would introduce some complexities that were just not worth introducing.
In the end I decided to create a new command, gl-setnavigationsettings, which would allow you to set all the values found on the above mentioned page with the exception of the navigation nodes themselves. Fortunately, the code for this was extremely simple (especially considering I already had chunks that I could use thanks to the other command I had created). The code simply sets properties on the PublishingWeb object based on parameters provided by the user:
1: public override int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
2: {
3: output = string.Empty;
4:
5: InitParameters(keyValues);
6:
7: string url = Params["url"].Value.TrimEnd('/');
8:
9: using (SPSite site = new SPSite(url))
10: {
11: using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
12: {
13: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
14:
15: if (Params["showsubsites"].UserTypedIn)
16: pubweb.IncludeSubSitesInNavigation = bool.Parse(Params["showsubsites"].Value);
17:
18: if (Params["showpages"].UserTypedIn)
19: pubweb.IncludePagesInNavigation = bool.Parse(Params["showpages"].Value);
20:
21: OrderingMethod sortMethod = pubweb.NavigationOrderingMethod;
22: if (Params["sortmethod"].UserTypedIn)
23: {
24: sortMethod = (OrderingMethod)Enum.Parse(typeof (OrderingMethod), Params["sortmethod"].Value, true);
25: pubweb.NavigationOrderingMethod = sortMethod;
26: }
27:
28: if (sortMethod != OrderingMethod.Manual)
29: {
30: if (Params["autosortmethod"].UserTypedIn)
31: pubweb.NavigationAutomaticSortingMethod = (AutomaticSortingMethod)Enum.Parse(typeof(AutomaticSortingMethod), Params["autosortmethod"].Value, true);
32: if (Params["sortascending"].UserTypedIn)
33: pubweb.NavigationSortAscending = bool.Parse(Params["sortascending"].Value);
34: }
35: else
36: {
37: if (Params["autosortmethod"].UserTypedIn)
38: Console.WriteLine("WARNING: parameter autosortmethod is incompatible with sortmethod {0}. The parameter will be ignored.", sortMethod);
39: if (Params["sortascending"].UserTypedIn)
40: Console.WriteLine("WARNING: parameter sortascending is incompatible with sortmethod {0}. The parameter will be ignored.", sortMethod);
41: }
42:
43: if (Params["inheritglobalnav"].UserTypedIn)
44: pubweb.InheritGlobalNavigation = bool.Parse(Params["inheritglobalnav"].Value);
45:
46: if (Params["currentnav"].UserTypedIn)
47: {
48: CurrentNavSettingsEnum currentNav = (CurrentNavSettingsEnum)Enum.Parse(typeof (CurrentNavSettingsEnum), Params["currentnav"].Value, true);
49: if (currentNav == CurrentNavSettingsEnum.InheritParent)
50: {
51: pubweb.InheritCurrentNavigation = true;
52: pubweb.NavigationShowSiblings = false;
53: }
54: else if (currentNav == CurrentNavSettingsEnum.CurrentSiteAndSiblings)
55: {
56: pubweb.InheritCurrentNavigation = true;
57: pubweb.NavigationShowSiblings = true;
58: }
59: else if (currentNav == CurrentNavSettingsEnum.CurrentSiteOnly)
60: {
61: pubweb.InheritCurrentNavigation = false;
62: pubweb.NavigationShowSiblings = false;
63: }
64: }
65:
66: pubweb.Update();
67: }
68: }
69:
70: return 1;
71: }
72:
73: internal enum CurrentNavSettingsEnum
74: {
75: InheritParent,
76: CurrentSiteAndSiblings,
77: CurrentSiteOnly
78: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-setnavigationsettings
stsadm -o gl-setnavigationsettings
Sets the navigation settings for a web site (use gl-setnavigationnodes to change the actual nodes that appear).
Parameters:
-url <site collection url>
[-showsubsites <true | false>]
[-showpages <true | false>]
[-sortmethod <automatic | manualwithautomaticpagesorting | manual>]
[-autosortmethod <title | createddate | lastmodifieddate>]
[-sortascending <true | false>]
[-inheritglobalnav <true | false>]
[-currentnav <inheritparent | currentsiteandsiblings | currentsiteonly>]
Here's an example of how to set various settings on a given web:
stsadm -o gl-setnavigationsettings -url "http://intranet/sitedirectory" -showsubsites true -showpages true -sortmethod automatic -autosortmethod title -sortascending true -inheritglobalnav false -currentnav currentsiteandsiblings
Move Web
As part of my upgrade I've got several hundred web sites that need to be moved around (either from one web application to another or just to a different location with the same web app or site collection). I figured that I needed an easy way to do this - the usual way is to either use the browser and go to the content and structure page to move webs around within a site collection or use the stsadm import and export commands to move between site collections.
What I wanted was a single command that I could call to move a web around regardless of what my target was (the same site collection or a different one). To move a web within the same site collection is really easy - you just set the ServerRelativeUrl property of the SPWeb object to the new path and then call Update() on the web object:
1: using (SPSite sourceSite = new SPSite(url))
2: using (SPWeb sourceWeb = sourceSite.OpenWeb())
3: using (SPSite parentSite = new SPSite(parentUrl))
4: using (SPWeb parentWeb = parentSite.OpenWeb())
5: {
6: ...
7: if (sourceSite.ID == parentSite.ID)
8: {
9: sourceWeb.ServerRelativeUrl = ConcatServerRelativeUrls(parentWeb.ServerRelativeUrl, sourceWeb.Name);
10: sourceWeb.Update();
11: }
12: else
13: {
14: ...
15: }
16: }
Moving a web to a different site collection requires a bit more effort. This is where you end up having to export the source web, create a placeholder web at the target, and then import the exported site to the placeholder site. Fortunately I already had some code to handle calling out to the stsadm import and export commands so I didn't have re-invent that (it's much easier to use these built in commands than to try and recreate them via the content deployment API - see the gl-importlist, gl-exportlist, and gl-copylist commands that I created earlier for examples of what's involved). Thanks to those existing helper methods the code became fairly simple and quick to write:
1: /// <summary>
2: /// Runs the specified command.
3: /// </summary>
4: /// <param name="command">The command.</param>
5: /// <param name="keyValues">The key values.</param>
6: /// <param name="output">The output.</param>
7: /// <returns></returns>
8: public override int Run(string command, StringDictionary keyValues, out string output)
9: {
10: output = string.Empty;
11:
12: InitParameters(keyValues);
13:
14: string url = Params["url"].Value.TrimEnd('/');
15: string parentUrl = Params["parenturl"].Value.TrimEnd('/');
16:
17: using (SPSite sourceSite = new SPSite(url))
18: using (SPWeb sourceWeb = sourceSite.OpenWeb())
19: using (SPSite parentSite = new SPSite(parentUrl))
20: using (SPWeb parentWeb = parentSite.OpenWeb())
21: {
22: if (sourceWeb.ID == parentWeb.ID)
23: {
24: throw new Exception("Source web and parent web cannot be the same.");
25: }
26: if (sourceWeb.ParentWeb.ID == parentWeb.ID)
27: {
28: throw new Exception(
29: "Parent web specified matches the source web's current parent - move is not necessary.");
30: }
31:
32: if (sourceSite.ID == parentSite.ID)
33: {
34: // This ones the easy one - just need to set the property and update the web.
35: sourceWeb.ServerRelativeUrl = ConcatServerRelativeUrls(parentWeb.ServerRelativeUrl, sourceWeb.Name);
36: sourceWeb.Update();
37: }
38: else
39: {
40: // Now for the hard one - we need to move to another site collection which requires using the export/import commands.
41: bool haltOnWarning = Params["haltonwarning"].UserTypedIn;
42: bool haltOnFatalError = Params["haltonfatalerror"].UserTypedIn;
43: bool includeUserSecurity = Params["includeusersecurity"].UserTypedIn;
44:
45: string fileName =
46: ConvertSubSiteToSiteCollection.ExportSite(url, haltOnWarning, haltOnFatalError, true, includeUserSecurity, true);
47:
48: string newWebUrl = ConcatServerRelativeUrls(parentUrl, sourceWeb.Name);
49: CreateWeb(newWebUrl);
50:
51: ConvertSubSiteToSiteCollection.ImportSite(fileName, newWebUrl, haltOnWarning, haltOnFatalError, true, includeUserSecurity, true);
52:
53: DeleteSubWebs(sourceWeb.Webs);
54:
55: sourceWeb.Delete();
56:
57: Directory.Delete(fileName, true);
58: }
59: }
60:
61: return 1;
62: }
63:
64: #endregion
65:
66: /// <summary>
67: /// Deletes the sub webs.
68: /// </summary>
69: /// <param name="webs">The webs.</param>
70: private static void DeleteSubWebs(SPWebCollection webs)
71: {
72: foreach (SPWeb web in webs)
73: {
74: if (web.Webs.Count > 0)
75: DeleteSubWebs(web.Webs);
76:
77: web.Delete();
78: }
79: }
80:
81: /// <summary>
82: /// Creates the web.
83: /// </summary>
84: /// <param name="strFullUrl">The STR full URL.</param>
85: private static void CreateWeb(string strFullUrl)
86: {
87: string strWebTemplate = null;
88: string strTitle = null;
89: string strDescription = null;
90: uint nLCID = 0;
91: bool convert = false;
92: bool useUniquePermissions = false;
93:
94: using (SPSiteAdministration administration = new SPSiteAdministration(strFullUrl))
95: administration.AddWeb(Utilities.GetServerRelUrlFromFullUrl(strFullUrl), strTitle, strDescription, nLCID, strWebTemplate, useUniquePermissions, convert);
96: }
Using this command is pretty straightforward - you just pass in the url of the source web and the url of the new parent web. I've got a couple of checks to make sure that you're not trying to set the parent to itself. If you're moving the web to a new site collection then you have 3 optional parameters of which only the "-includeusersecurity" has any real affect (the others just stop either the import or export if there's a warning or fatal error depending on which option you provide). The syntax of the command can be seen below:
C:\>stsadm -help gl-moveweb
stsadm -o gl-moveweb
Moves a web.
Parameters:
-url <url of web to move>
-parenturl <url of parent web>
[-haltonwarning (only considered if moving to a new site collection)]
[-haltonfatalerror (only considered if moving to a new site collection)]
[-includeusersecurity (only considered if moving to a new site collection)]
[-retainobjectidentity (only considered if moving to a new site collection)]
Here’s an example of how to move a web within the same site collection:
stsadm –o gl-moveweb -url "http://intranet/topics/divisions" -parenturl "http://intranet"
Here’s an example of how to move a web to a different site collection:
Update 9/21/2007: I've enhanced the command to take advantage of another new command I created: gl-updatev2tov3upgradeareaurlmappings (updates the url mapping of V2 bucket webs to V3 webs thereby reflecting the change of url as a result of the move so if a user tries to hit the V2 url it will redirect to the new and updated V3 url).stsadm –o gl-moveweb -url "http://intranet/sites/projectATeamSite" -parenturl "http://teamsites/projects" -includeusersecurity -haltonfatalerror
Update 10/8/2007: I've enhanced the command to take advantage of another new command I created: gl-import2 (object IDs are now maintained during the move so items dependent on a specific ID should no longer break).
Update 10/12/2007: I've fixed some issues with the retainobjectidentity setting and the use of the import2 command. As a result I've decided to make the retainobjectidentity an option specified via a parameter rather than the default behavior. The main issue is due to the fact that when importing sites using the retainObjectIdentity option the exported site must be a child of the root site or else things get really messed up (as a result I move the site to be under the root site, then export, then import, and then rename the site to match the original plus a possible suffix if the original name already existed at the target). The other issue I found is that for some reason some web parts will be imported twice on a given page - I figure though that this is easier to fix (simply delete the extra web part) than the alternative of having to retarget all the web parts to their new source.
Update 10/17/2007: I've made some changes to the gl-moveweb command so that it now supports moving a root level web (that is, converting a site collection to a sub-site). Note that there are limitations when converting a site collection to a sub-site. If you are using the fantastic 40 application templates you're likely to run into issues (see the comments for gl-convertsubsitetositecollection). Also - if you are moving a root web (site collection) the -retainobjectidentity parameter will not work (an error will be returned if you attempt to use it).
More Site Navigation Settings Commands
Last month I created a couple pretty basic commands to help me with setting the navigational elements of my site: "gl-enumnavigation" and "gl-addnavigationnode". You can find information about those commands here: Site Navigation Settings.
As I worked more on my upgraded site I found that what I really needed was a way to make drastic changes using an XML file rather than trying to add or remove one item at a time (note that I haven't created anything to allow removing a single item but I suspect after what I've just done that wouldn't be too hard). To address my needs I first needed to modify my original gl-enumnavigation command so that it could output XML instead of the flat list that it previously did (probably should have done that to begin with but...).
This was pretty easy to do except for one stumbling block - you'd think it would be easy to determine whether a site or page was hidden and that this info would be part of the SPNavigationNode object - unfortunately that's not the case - it took me a bit of digging to realize that I had to "find" the correct PublishingWeb or PublishingPage object and then checks it's IncludeInGlobalNavigation or IncludeInCurrentNavigation properties.
The second thing I needed to do was to create a new command which would be able to take the generated XML from the gl-enumnavigation command and use it to "rebuild" the navigation. This works pretty well in the sense that you can export using gl-enumnavigation, modify the generated XML to meet whatever custom needs you have, and then import using the gl-setnavigationnodes command that I created.
For my purposes this has enabled me to do a test upgrade - work the navigation to how by business users stipulated, save that out to a file and then re-run my upgrade any number of times and simple reset the navigation using the previously generated file. Another use could be to help get around the fact that there's no approval process for navigation changes - an administrator could make the proposed changes in a test site (either in a test farm or on the same production farm), get stakeholder approval, and then import the changes to the production site (and gl-enumnavigation could be used to make a backup of the existing navigation in the event that it becomes necessary to roll-back).
Once I had these two commands created I realized that it wasn't a big step to create a copy command. So I created gl-copynavigation which really just combines gl-enumnavigation and gl-setnavigationnodes but doesn't require you to deal with creating the file (though it does have an option to backup the target in case you want to revert the site). The two commands are detailed below.
1. gl-setnavigationnodes
I'd like to say that this was real easy to do - it should have been - but as it turned out it was much more difficult than I thought it would be (I'm beginning to detect a trend). I have two main methods which do all the work - the first, SetNavigation() does all the prep and cleanup work; the second, AddNodes() actually does the adding of the nodes to the appropriate collection. The real difficult part of all this was that what you see in the browser is not what you get when you query the GlobalNavigationNodes and CurrentNavigationNodes collections. These collections will not show the various sub-sites and pages that show up in the navigation unless you've explicitly set some property (by moving a node for example). So I had to take look in more than once place for everything and the logic of it all is really bizarre. The code is well documented so I won't go through it again here:
1: /// <summary>
2: /// Sets the navigation.
3: /// </summary>
4: /// <param name="web">The web site.</param>
5: /// <param name="xmlDoc">The XML doc containing the navigation nodes.</param>
6: /// <param name="showSubSites">if set to <c>true</c> [show sub sites].</param>
7: /// <param name="showPages">if set to <c>true</c> [show pages].</param>
8: /// <param name="deleteExistingGlobal">if set to <c>true</c> [delete existing global nodes].</param>
9: /// <param name="deleteExistingCurrent">if set to <c>true</c> [delete existing current nodes].</param>
10: public static void SetNavigation(SPWeb web, XmlDocument xmlDoc, bool showSubSites, bool showPages, bool deleteExistingGlobal, bool deleteExistingCurrent)
11: {
12: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
13:
14: // First need to set whether or not we show sub-sites and pages
15: pubweb.IncludeSubSitesInNavigation = showSubSites;
16: pubweb.IncludePagesInNavigation = showPages;
17: pubweb.Update();
18:
19: List<SPNavigationNode> existingGlobalNodes = new List<SPNavigationNode>();
20: List<SPNavigationNode> existingCurrentNodes = new List<SPNavigationNode>();
21: // We can't delete the navigation items until we've added the new ones so store the existing
22: // ones for later deletion (note that we don't have to store all of them - just the top level).
23: // I have no idea why this is the case - but when I tried to clear everything out first I got
24: // all kinds of funky errors that just made no sense to me - this works so....
25: foreach (SPNavigationNode node in pubweb.GlobalNavigationNodes)
26: existingGlobalNodes.Add(node);
27: foreach (SPNavigationNode node in pubweb.CurrentNavigationNodes)
28: existingCurrentNodes.Add(node);
29:
30: XmlNodeList newGlobalNodes = xmlDoc.SelectNodes("//Navigation/Global/Node");
31: XmlNodeList newCurrentNodes = xmlDoc.SelectNodes("//Navigation/Current/Node");
32:
33:
34: // If we've got global or current nodes in the xml then the intent is to reset those elements.
35: // If we've also specified to delete any existing elements then we need to first hide all the
36: // sub-sites and pages (you can't delete them because they don't exist as a node). Note that
37: // we are only doing this if showSubSites is true - if it's false we don't see them so no point
38: // in hiding them. Any non-sub-site or non-page will be deleted after we've added the new nodes.
39: if (newGlobalNodes.Count > 0 && deleteExistingGlobal && showSubSites)
40: {
41: // Initialize the sub-sites (forces the provided XML to specify whether any should be visible)
42: foreach (SPWeb tempWeb in pubweb.Web.Webs)
43: {
44: pubweb.ExcludeFromNavigation(true, tempWeb.ID);
45: }
46: pubweb.Update();
47: }
48: if (newCurrentNodes.Count > 0 && deleteExistingCurrent && showSubSites)
49: {
50: foreach (SPWeb tempWeb in pubweb.Web.Webs)
51: {
52: pubweb.ExcludeFromNavigation(false, tempWeb.ID);
53: }
54: pubweb.Update();
55: }
56:
57: // Now we need to add all the global nodes (if any - if the collection is empty the following will just return and do nothing)
58: AddNodes(pubweb, true, pubweb.GlobalNavigationNodes, newGlobalNodes);
59: // Update the web as the above may have made modifications
60: pubweb.Update();
61:
62: // Now delete all the previously existing global nodes.
63: if (newGlobalNodes.Count > 0 && deleteExistingGlobal)
64: {
65: foreach (SPNavigationNode node in existingGlobalNodes)
66: {
67: node.Delete();
68: }
69: }
70:
71: // Now we need to add all the current nodes (if any)
72: AddNodes(pubweb, false, pubweb.CurrentNavigationNodes, newCurrentNodes);
73: // Update the web as the above may have made modifications
74: pubweb.Update();
75:
76: // Now delete all the previously existing current nodes.
77: if (newCurrentNodes.Count > 0 && deleteExistingCurrent)
78: {
79: foreach (SPNavigationNode node in existingCurrentNodes)
80: {
81: node.Delete();
82: }
83: }
84: }
85:
86:
87: /// <summary>
88: /// Adds the nodes.
89: /// </summary>
90: /// <param name="pubWeb">The publishing web.</param>
91: /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
92: /// <param name="existingNodes">The existing nodes.</param>
93: /// <param name="newNodes">The new nodes.</param>
94: private static void AddNodes(PublishingWeb pubWeb, bool isGlobal, SPNavigationNodeCollection existingNodes, XmlNodeList newNodes)
95: {
96: if (newNodes.Count == 0)
97: return;
98:
99: for (int i = 0; i < newNodes.Count; i++)
100: {
101: XmlElement newNodeXml = (XmlElement)newNodes[i];
102: string url = newNodeXml.SelectSingleNode("Url").InnerText;
103: string title = newNodeXml.GetAttribute("Title");
104: NodeTypes type = NodeTypes.None;
105: if (newNodeXml.SelectSingleNode("NodeType") != null && !string.IsNullOrEmpty(newNodeXml.SelectSingleNode("NodeType").InnerText))
106: type = (NodeTypes)Enum.Parse(typeof(NodeTypes), newNodeXml.SelectSingleNode("NodeType").InnerText);
107:
108: bool isVisible = true;
109: if (!string.IsNullOrEmpty(newNodeXml.GetAttribute("IsVisible")))
110: isVisible = bool.Parse(newNodeXml.GetAttribute("IsVisible"));
111:
112: if (type == NodeTypes.Area)
113: {
114: // You can't just add an "Area" node (which represents a sub-site) to the current web if the
115: // url does not correspond with an actual sub-site (the code will appear to work but you won't
116: // see anything when you load the page). So we need to check and see if the node actually
117: // points to a sub-site - if it does not then change it to "AuthoredLinkToWeb".
118: SPWeb web = null;
119: string name = url.Trim('/');
120: if (name.Length != 0 && name.IndexOf("/") > 0)
121: {
122: name = name.Substring(name.LastIndexOf('/') + 1);
123: }
124: try
125: {
126: // Note that pubWeb.Web.Webs[] does not return null if the item doesn't exist - it simply throws an exception (I hate that!)
127: web = pubWeb.Web.Webs[name];
128: }
129: catch (ArgumentException)
130: {
131: }
132: if (web == null || !web.Exists || web.ServerRelativeUrl.ToLower() != url.ToLower())
133: {
134: // The url doesn't correspond with a sub-site for the current web so change the node type.
135: // This is most likely due to copying navigation elements from another site
136: type = NodeTypes.AuthoredLinkToWeb;
137: }
138: else if (web.Exists && web.ServerRelativeUrl.ToLower() == url.ToLower())
139: {
140: // We did find a matching sub-site so now we need to set the visibility
141: if (isVisible)
142: pubWeb.IncludeInNavigation(isGlobal, web.ID);
143: else
144: pubWeb.ExcludeFromNavigation(isGlobal, web.ID);
145: }
146:
147: }
148: else if (type == NodeTypes.Page)
149: {
150: // Adding links to pages has the same limitation as sub-sites (Area nodes) so we need to make
151: // sure it actually exists and if it doesn't then change the node type.
152: PublishingPage page = null;
153: try
154: {
155: // Note that GetPublishingPages()[] does not return null if the item doesn't exist - it simply throws an exception (I hate that!)
156: page = pubWeb.GetPublishingPages()[url];
157: }
158: catch (ArgumentException)
159: {
160: }
161: if (page == null)
162: {
163: // The url doesn't correspond with a page for the current web so change the node type.
164: // This is most likely due to copying navigation elements from another site
165: type = NodeTypes.AuthoredLinkToPage;
166: url = pubWeb.Web.Site.MakeFullUrl(url);
167: }
168: else
169: {
170: // We did find a matching page so now we need to set the visibility
171: if (isVisible)
172: pubWeb.IncludeInNavigation(isGlobal, page.ListItem.UniqueId);
173: else
174: pubWeb.ExcludeFromNavigation(isGlobal, page.ListItem.UniqueId);
175: }
176: }
177:
178: // If it's not a sub-site or a page that's part of the current web and it's set to
179: // not be visible then just move on to the next (there is no visibility setting for
180: // nodes that are not of type Area or Page).
181: if (!isVisible && type != NodeTypes.Area && type != NodeTypes.Page)
182: continue;
183:
184: // Finally, can add the node to the collection.
185: SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
186: title, url, type, existingNodes);
187:
188:
189: // Now we need to set all the other properties
190: foreach (XmlElement property in newNodeXml.ChildNodes)
191: {
192: // We've already set these so don't set them again.
193: if (property.Name == "Url" || property.Name == "Node" || property.Name == "NodeType")
194: continue;
195:
196: // CreatedDate and LastModifiedDate need to be the correct type - all other properties are strings
197: if (property.Name == "CreatedDate" && !string.IsNullOrEmpty(property.InnerText))
198: {
199: node.Properties["CreatedDate"] = DateTime.Parse(property.InnerText);
200: continue;
201: }
202: if (property.Name == "LastModifiedDate" && !string.IsNullOrEmpty(property.InnerText))
203: {
204: node.Properties["LastModifiedDate"] = DateTime.Parse(property.InnerText);
205: continue;
206: }
207:
208: node.Properties[property.Name] = property.InnerText;
209: }
210: // If we didn't have a CreatedDate or LastModifiedDate then set them to now.
211: if (node.Properties["CreatedDate"] == null)
212: node.Properties["CreatedDate"] = DateTime.Now;
213: if (node.Properties["LastModifiedDate"] == null)
214: node.Properties["LastModifiedDate"] = DateTime.Now;
215:
216: // Save our changes to the node.
217: node.Update();
218: node.MoveToLast(existingNodes); // Should already be at the end but I prefer to make sure![]()
219:
220: XmlNodeList childNodes = newNodeXml.SelectNodes("Node");
221:
222: // If we have child nodes then make a recursive call passing in the current nodes Children property as the collection to add to.
223: if (childNodes.Count > 0)
224: AddNodes(pubWeb, isGlobal, node.Children, childNodes);
225: }
226: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-setnavigationnodes
stsadm -o gl-setnavigationnodes
Rebuilds the Global and/or Current navigation based on a passed in XML file which can be generated by the gl-enumnavigation command and then modified (note that the Id attribute is ignored).
Parameters:
-url <site collection url>
-inputfile <xml file to use as input>
[-showsubsites <true (default) | false>]
[-showpages <true | false (default)>]
[-deleteexistingglobal <true (default) | false>]
[-deleteexistingcurrent <true (default) | false>]
[-backuptarget <filename>]
Here’s an example of how to set the navigation replacing both the global (Top Nav) and current (Quick Launch) navigation with that specified in the input file "input.xml":
stsadm –o gl-setnavigationnodes –url "http://intranet/hr" -inputfile "c:\input.xml" -backuptarget "c:\backup.xml"
If you want to do more of a merge (add everything from the XML file to the end of the navigation list) you would do the following:
stsadm –o gl-setnavigationnodes –url "http://intranet/hr" -inputfile "c:\input.xml" -backuptarget "c:\backup.xml" -deleteexistingglobal false -deleteexistingcurrent false
Doing the above would be useful if you've got a certain set of navigational elements that you want to have on all your site collections - you could define the XML file once and then use setnavigationnodes to add those elements to each site collection without having to do it manually.
Update 10/15/2007: I enhanced this command slightly. If you add the XML tag "<AutoAddSubSites />" to the source XML the code will replace this with nodes corresponding to all the sub-sites for the target web. This is useful if you've got a global navigation that you want to propagate to various site collections but you want the site collections sub-sites listed in addition to what you are importing. In my case I added the following XML to my source so that all sub-sites would appear under a heading called "Sub Sites":
1: <Node Title="Sub Sites" IsVisible="True">
2: <Url>/</Url>
3: <UrlFragment></UrlFragment>
4: <vti_navsequencechild>true</vti_navsequencechild>
5: <CreatedDate>10/15/2007 6:28:01 PM</CreatedDate>
6: <UrlQueryString></UrlQueryString>
7: <NodeType>Heading</NodeType>
8: <Audience></Audience>
9: <Target></Target>
10: <Description></Description>
11: <LastModifiedDate>10/15/2007 6:28:01 PM</LastModifiedDate>
12: <BlankUrl>True</BlankUrl>
13: <AutoAddSubSites />
14: </Node>
2. gl-copynavigation
I basically got this command for free - all I'm doing is utilizing what I'd already done for gl-enumnavigation and gl-setnavigationnodes. I haven't actually needed to use this myself but I thought someone might find it useful and as it was simple to create (which has been a nice change) I didn't really spend much time on it. Here's the core of the code:
1: XmlDocument xmlDoc;
2: // Get the XML from our source site.
3: using (SPSite sourceSite = new SPSite(sourceurl))
4: using (SPWeb sourceWeb = sourceSite.OpenWeb())
5: {
6: PublishingWeb sourcePubWeb = PublishingWeb.GetPublishingWeb(sourceWeb);
7: xmlDoc = EnumNavigation.GetNavigationAsXml(sourcePubWeb);
8: }
9:
10: // If the user only wants to copy the global or current then remove the other element
11: if (currentOnly)
12: xmlDoc.SelectSingleNode("//Navigation/Global").RemoveAll();
13: if (globalOnly)
14: xmlDoc.SelectSingleNode("//Navigation/Current").RemoveAll();
15:
16: // Set the navigation using the XML we got from the source.
17: using (SPSite targetSite = new SPSite(targeturl))
18: using (SPWeb targetWeb = targetSite.OpenWeb())
19: {
20: if (keyValues.ContainsKey("backuptarget"))
21: {
22: PublishingWeb targetPubWeb = PublishingWeb.GetPublishingWeb(targetWeb);
23: XmlDocument xmlBackupDoc = EnumNavigation.GetNavigationAsXml(targetPubWeb);
24: xmlBackupDoc.Save(keyValues["backuptarget"]);
25: }
26: SetNavigationNodes.SetNavigation(targetWeb, xmlDoc, showSubSites, showPages, deleteExistingGlobal, deleteExistingCurrent);
27: }
As you can see from the above - it's really quite simple. The syntax of the command can be seen below:
C:\>stsadm -help gl-copynavigation
stsadm -o gl-copynavigation
Copies the Global and/or Current navigation from one site collection to another.
Parameters:
-sourceurl <source site collection url>
-targeturl <target site collection url>
[-globalonly / -currentonly]
[-showsubsites <true (default) | false>]
[-showpages <true | false (default)>]
[-deleteexistingglobal <true (default) | false>]
[-deleteexistingcurrent <true (default) | false>]
[-backuptarget <filename>]
Here’s an example of how to copy both the global (Top Nav) and current (Quick Launch) navigation from the root site collection to a site collection at the managed path "hr":
stsadm –o gl-copynavigation –sourceurl "http://intranet/" -targeturl "http://intranet/hr" -backuptarget "c:\backup.xml"
If you'd like to just copy the global navigation and leave the quick launch (current) then you'd do the following:
stsadm –o gl-copynavigation –sourceurl "http://intranet/" -targeturl "http://intranet/hr" -backuptarget "c:\backup.xml" -globalonly
Setting deleteexistingglobal or deleteexistingcurrent to false would result in the source items being added to the existing navigation rather than replacing the existing navigation.
Update 11/2/2007: I've modified the gl-setnavigationnodes command so that it now allows you to add XML nodes to the passed in XML which will be replaced dynamically. The two nodes are <SiteCollectionUrl /> and <WebUrl />. If either (or both) of these two nodes appear in the XML they will be replaced with the appropriate server relative URL of the specified web or site collection. For example - you could have the following Node element in your XML:
<Node Id="2030" Title="Document Center" IsVisible="True"> <Url><SiteCollectionUrl />/Docs</Url> <vti_navsequencechild>true</vti_navsequencechild> <UrlQueryString></UrlQueryString> <NodeType>Area</NodeType> <Description>Main Document Center for Site Collection</Description> <UrlFragment></UrlFragment> </Node>
Site Navigation Settings
Setting the navigation is another common post upgrade/deployment task that should be easy to script. One of the challenges in using the commands I created was that it's necessary to know the node ID of the navigational elements. There's probably ways to change this to use the name rather than the ID but you can run into trouble when dealing with items with the same name. To set the navigation using the web interface go to your top level site ->Site Settings->Navigation.
This one took me a while to figure out as the code that Microsoft wrote to handle this was cryptic at best. It was very difficult to wade through some of the oddness. In the end I figured out that you simply had to get a PublishingWeb object and manipulate the GlobalNavigationNodes collection or the CurrentNavigationNodes collection depending on your intentions. In order to add a new node you need to know the existing node IDs so I created an enumerate command to get that information (again there's probably another way to approach this but this worked for my needs). The two commands I created are called gl-enumnavigation and gl-addnavigationnode.
1. gl-enumnavigation
The code for this turned out to be pretty simple. The main part of the code is a recursive routine which goes through all the nodes and their children. I added some indenting to give it more of a tree layout (UPDATE 9/4/2007: I added an xml switch to allow the output to be specified as XML instead of flat text - the results can also be dumped to an output file specified via an outputfile parameter).
1: public int Run(string command, StringDictionary keyValues, out string output)
2: {
3: ...
4: string url = keyValues["url"];
5: using (SPSite site = new SPSite(url))
6: {
7: using (SPWeb web = site.OpenWeb())
8: {
9: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
10:
11: StringBuilder sb = new StringBuilder();
12: sb.Append("Global Navigation:\r\n");
13: EnumerateCollection(pubweb.GlobalNavigationNodes, ref sb, 1);
14:
15: sb.Append("Current Navigation:\r\n");
16: EnumerateCollection(pubweb.CurrentNavigationNodes, ref sb, 1);
17:
18: output += sb.ToString();
19: }
20: }
21: ...
22: }
23: private static void EnumerateCollection(SPNavigationNodeCollection nodes, ref StringBuilder sb, int level)
24: {
25: if (nodes == null || nodes.Count == 0)
26: return;
27:
28: string indent = "";
29:
30: for (int i = 0; i < level; i++)
31: {
32: indent += " ";
33: }
34: if (level > 0)
35: indent += "- ";
36:
37: foreach (SPNavigationNode node in nodes)
38: {
39: sb.AppendFormat("{0}{1}: {2}\r\n", indent, node.Id, node.Title);
40: foreach (DictionaryEntry d in node.Properties)
41: sb.Append(indent + " :" + d.Key + "=" + d.Value + "\r\n");
42: EnumerateCollection(node.Children, ref sb, level+1);
43: }
44: }
As you can see from the code above, the EnumerateCollection static method does most of the work. It takes in an SPNavigationNodeCollection and iterates through the collection, passing any children back into the same method recursively for processing. The syntax of the command can be seen below:
C:\>stsadm -help gl-enumnavigation
stsadm -o gl-enumnavigation
Returns the site navigation hierarchy.
Parameters:
-url <site collection>
[-xml]
[-outputfile <file to output results to>
Here’s an example of how to return the site navigation:
stsadm –o gl-enumnavigation –url "http://intranet/"
The results of running the above command can be seen below (note that I'm also output all the properties associated with each node object which can be useful when trying to determine how the various NodeType enums are assigned:
C:\>stsadm -o gl-enumnavigation -url "http://intranet/
Global Navigation:
- 2018: Topics
:NodeType=Area
:vti_navsequencechild=true
- 2019: News
:NodeType=Area
:vti_navsequencechild=true
- 2021: Sites
:NodeType=Area
:vti_navsequencechild=true
Current Navigation:
- 2013: Topics
:NodeType=Area
:vti_navsequencechild=true
- 2014: News
:NodeType=Area
:vti_navsequencechild=true
- 2016: Sites
:NodeType=Area
:vti_navsequencechild=true
2. gl-addnavigationnode
Figuring out how to add a new node took a little more digging. Turns out that you need to use SPNavigationSiteMapNode.CreateSPNavigationNode() which is a static method that returns back an SPNavigationNode object. You pass into this static method the name, link, type, and nodes collection that the new node belongs to (global or current). Once the node is created you set the remaining properties and call Update(). At this point the node exists as the last item - now you simply move it to where you want (no need to update again):
1: string url = keyValues["url"];
2: string linkUrl = keyValues["linkurl"];
3: string name = keyValues["name"];
4: string description = keyValues["description"];
5: NodeTypes type = (NodeTypes)Enum.Parse(typeof(NodeTypes), keyValues["type"]);
6: bool global = keyValues.ContainsKey("global");
7: bool current = keyValues.ContainsKey("current");
8: bool addAsFirst = keyValues.ContainsKey("addasfirst");
9: bool addAsLast = keyValues.ContainsKey("addaslast");
10: bool addAfter = keyValues.ContainsKey("addafter");
11: bool newWindow = keyValues.ContainsKey("newwindow");
12: string previousNodeID = string.Empty;
13: if (addAfter)
14: previousNodeID = keyValues["addafter"];
15:
16: using (SPSite site = new SPSite(url))
17: {
18: using (SPWeb web = site.OpenWeb())
19: {
20: PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
21:
22: SPNavigationNodeCollection nodes;
23: if (global)
24: nodes = pubweb.GlobalNavigationNodes;
25: else if (current)
26: nodes = pubweb.CurrentNavigationNodes;
27: else
28: throw new ApplicationException("Unknown error occured.");
29:
30: SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
31: name, linkUrl, type, nodes);
32:
33: node.Properties["CreatedDate"] = DateTime.Now;
34: node.Properties["LastModifiedDate"] = DateTime.Now;
35: node.Properties["Description"] = description;
36: if (newWindow)
37: node.Properties["Target"] = "_blank";
38: else
39: node.Properties["Target"] = string.Empty;
40:
41: node.Update();
42:
43: if (addAsFirst)
44: node.MoveToFirst(nodes);
45: else if (addAsLast)
46: node.MoveToLast(nodes);
47: else if (addAfter)
48: {
49: SPNavigationNode previousNode = web.Navigation.GetNodeById(int.Parse(previousNodeID));
50: if (previousNode == null)
51: {
52: output = "Previous node was not found.";
53: return 0;
54: }
55: node.Move(nodes, previousNode);
56: }
57: }
58: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-addnavigationnode
stsadm -o gl-addnavigationnode
Adds a new node to the site hierarchy.
Parameters:
-url <site collection url>
-name <navigation name>
-linkurl <link url>
-type <link type: None | Area | Page | List | ListItem | PageLayout | Heading | AuthoredLinkToPage | AuthoredLinkToWeb | AuthoredLinkPlain | AuthoredLink | Default | Custom | All>
-global / -current
-addasfirst / -addaslast / -addafter <previous node id>
[-newwindow]
[-description <description text>]
Here’s an example of how to add a new node after an existing node:
stsadm –o gl-addnavigationnode–url "http://intranet/" -name "Help Desk" -linkurl "http://helpdesk/" -type AuthoredLinkPlain -global -addafter 2019 -newwindow -description "Corporate Help Desk Application"
Check out the books I've contributed to at Amazon.com: