This particular command was one that I didn’t actually need but rather I needed a starting place for a couple other commands that I did need (to be documented) and moving a web part seemed a simple enough starting place for manipulating web parts. So this one was really just a template for my other commands but it works rather well and who knows, maybe someone will benefit from it. Moving a web part is actually quite simple – you just call the SPLimitedWebPartManager’s MoveWebPart method passing in the web part to move, the zone ID to move to, and the zone index (position in the zone). And then you just call SaveChanges passing in the web part.

All the real work is in locating the web part to move which is a little trickier than it should be because the Title field is not unique but it’s a lot more convenient for the user to use the title rather than the cryptic ID. The other thing is the the zone ID is a string and could theoretically be any value so the caller needs to know what values to provide (this is where the gl-enumpagewebparts command comes in handy along with retrieving the web parts ID if necessary). To facilitate getting the web part I created a couple helper methods, one gets the web part by ID the other by Title:

 1/// <summary>
 2/// Gets the web part by id.
 3/// </summary>
 4/// <param name="web">The web.</param>
 5/// <param name="url">The URL.</param>
 6/// <param name="id">The id.</param>
 7/// <param name="manager">The web part manager.</param>
 8/// <returns></returns>
 9internal static WebPart GetWebPartById(SPWeb web, string url, string id, out SPLimitedWebPartManager manager)
10{
11    manager = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
12    WebPart wp = manager.WebParts[id];
13    if (wp == null)
14    {
15        manager = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
16        wp = manager.WebParts[id];
17    }
18    return wp;
19}
20 
21/// <summary>
22/// Gets the web part by title.
23/// </summary>
24/// <param name="web">The web.</param>
25/// <param name="url">The URL.</param>
26/// <param name="title">The title.</param>
27/// <param name="manager">The web part manager.</param>
28/// <returns></returns>
29internal static WebPart GetWebPartByTitle(SPWeb web, string url, string title, out SPLimitedWebPartManager manager)
30{
31    manager = web.GetLimitedWebPartManager(url, PersonalizationScope.Shared);
32    List<WebPart> foundParts = new List<WebPart>();
33    WebPart wp = null;
34    foreach (WebPart tempWP in manager.WebParts)
35    {
36        if (tempWP.DisplayTitle.ToLowerInvariant() == title.ToLowerInvariant())
37        {
38            foundParts.Add(tempWP);
39            wp = tempWP;
40        }
41    }
42    if (foundParts.Count == 0)
43    {
44        manager = web.GetLimitedWebPartManager(url, PersonalizationScope.User);
45        foreach (WebPart tempWP in manager.WebParts)
46        {
47            if (tempWP.DisplayTitle.ToLowerInvariant() == title.ToLowerInvariant())
48            {
49                foundParts.Add(tempWP);
50                wp = tempWP;
51            }
52        }
53    }
54    if (foundParts.Count > 1)
55    {
56        string msg = "Found more than one web part matching the specified title.  Use the ID instead:\r\n\r\n";
57        XmlDocument xmlDoc = new XmlDocument();
58        string tempXml = null;
59        foreach (WebPart tempWP in foundParts)
60        {
61            tempXml += EnumPageWebParts.GetWebPartDetailsMinimal(tempWP, manager);
62        }
63        xmlDoc.LoadXml("<MatchingWebParts>" + tempXml + "</MatchingWebParts>");
64        throw new SPException(msg + GetFormattedXml(xmlDoc));
65    }
66    return wp;
67}

Once you’ve got the web part the rest is fairly easy – just need to provide a bit of logic to figure out which method to call and then handle the check in/out of the web part page so that the move operation can work:

 1public override int Run(string command, StringDictionary keyValues, out string output)
 2{
 3    output = string.Empty;
 4
 5    InitParameters(keyValues);
 6
 7    SPBinaryParameterValidator.Validate("id", Params["id"].Value, "title", Params["title"].Value);
 8    if (!Params["zone"].UserTypedIn && !Params["zoneindex"].UserTypedIn)
 9        throw new SPSyntaxException("You must specify at least the zone or zoneindex parameters.");
10
11    string url = Params["url"].Value;
12
13    using (SPSite site = new SPSite(url))
14    using (SPWeb web = site.OpenWeb()) // The url contains a filename so AllWebs[] will not work unless we want to try and parse which we don't
15    {
16        SPFile file = web.GetFile(url);
17
18        if (!Utilities.IsCheckedOut(file.Item) || !Utilities.IsCheckedOutByCurrentUser(file.Item))
19        file.CheckOut(); // If it's checked out by another user then this will throw an informative exception so let it do so.
20
21        string displayTitle = string.Empty;
22        try
23        {
24            WebPart wp;
25            SPLimitedWebPartManager manager;
26            if (Params["id"].UserTypedIn)
27            {
28                wp = Utilities.GetWebPartById(web, url, Params["id"].Value, out manager);
29            }
30            else
31            {
32                wp = Utilities.GetWebPartByTitle(web, url, Params["title"].Value, out manager);
33                if (wp == null)
34                {
35                    throw new SPException(
36                        "Unable to find specified web part.  Try specifying the -id parameter instead (use enumpagewebparts to get the ID)");
37                }
38            }
39
40            if (wp == null)
41            {
42                throw new SPException("Unable to find specified web part.");
43            }
44
45            string zoneID = manager.GetZoneID(wp);
46            int zoneIndex = wp.ZoneIndex;
47
48            if (Params["zone"].UserTypedIn)
49                zoneID = Params["zone"].Value;
50            if (Params["zoneindex"].UserTypedIn)
51                zoneIndex = int.Parse(Params["zoneindex"].Value);
52
53            // Set this so that we can add it to the check-in comment.
54            displayTitle = wp.DisplayTitle;
55
56            manager.MoveWebPart(wp, zoneID, zoneIndex);
57            manager.SaveChanges(wp);
58        }
59        finally
60        {
61            file.CheckIn("Checking in changes to page layout due to moving of web part " + displayTitle);
62
63            if (Params["publish"].UserTypedIn)
64            f   ile.Publish("Publishing changes to page layout due to moving of web part " + displayTitle);
65        }
66    }
67
68    return 1;
69}

The syntax of the command I created can be seen below.

C:\>stsadm -help gl-movewebpart

stsadm -o gl-movewebpart

Moves a web part on a page.

Parameters:
        -url <web part page URL>
        {-id <web part ID> |
         -title <web part title>}
        [-zone <zone ID>]
        [-zoneindex <zone index>]
        [-publish]

Here’s an example of how to move a web part to a different zone on a page:

stsadm -o gl-movewebpart -url "http://intranet/hr/pages/default.aspx" -title "Grouped Listings" -zone MiddleLeftZone -zoneindex 1 -publish