Ensuring a Valid SPContext via Feature Activation
I’ve been meaning to blog about this for a while but just haven’t gotten around to it. Have you ever needed to add a web part to a page during Feature activation? Of course you can do this declaratively using CAML but I usually prefer to do this stuff via code. The challenge is that occasionally you will need to activate the Feature outside the context of a web request – such as via STSADM – this becomes critical for certain web parts, such as earlier versions of the KPI List Web Part, which required a valid SPContext in order to be added to a page (this web part was fixed in the August 2008 Cumulative Update (or thereabouts) so that it no longer requires a valid SPContext object).
If you’re faced with adding a web part (or any other artifact) which requires a valid SPContext object outside of a web request than you can create your own context with the following three lines of code:
1: HttpRequest httpRequest = new HttpRequest("", web.Url, "");
2: HttpContext.Current = new HttpContext(httpRequest, new HttpResponse(new StringWriter()));
3: SPControl.SetContextWeb(HttpContext.Current, web);
public static void AddWebPart(SPWeb web, string page, string webPartXmlFile, string zone, int zoneId, bool deleteExistingIfFound) { bool cleanupContext = false; try { if (HttpContext.Current == null) { cleanupContext = true; HttpRequest httpRequest = new HttpRequest("", web.Url, ""); HttpContext.Current = new HttpContext(httpRequest, new HttpResponse(new StringWriter())); SPControl.SetContextWeb(HttpContext.Current, web); } using (SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(page, PersonalizationScope.Shared)) { string err; XmlTextReader reader = null; System.Web.UI.WebControls.WebParts.WebPart wp; try { string webPartXml = File.ReadAllText(webPartXmlFile); webPartXml = webPartXml.Replace("${siteCollection}", web.Site.Url); webPartXml = webPartXml.Replace("${site}", web.Url); webPartXml = webPartXml.Replace("${webTitle}", HttpUtility.HtmlEncode(web.Title)); reader = new XmlTextReader(new StringReader(webPartXml)); wp = manager.ImportWebPart(reader, out err); if (!string.IsNullOrEmpty(err)) throw new Exception(err); } finally { if (reader != null) reader.Close(); } // Delete existing web part with same title so that we only have the latest version on the page foreach (System.Web.UI.WebControls.WebParts.WebPart wpTemp in manager.WebParts) { if (wpTemp.Title == wp.Title) { if (deleteExistingIfFound) manager.DeleteWebPart(wpTemp); else { wpTemp.Dispose(); return; } break; } wpTemp.Dispose(); } manager.AddWebPart(wp, zone, zoneId); } } finally { if (HttpContext.Current != null && cleanupContext) { HttpContext.Current = null; } } }
Custom List Views and WSS 4.0′s XSLT-based List View
Some of us have known about this for a while but we’ve been unable to talk about it until now – Microsoft released the following article two days ago: The CustomListView rule in Pre-Upgrade Checker can warn that customized list views that will not be upgraded. Read this article closely because for those of you that hate the List View Web Part as much as I do you’ll be extremely pleased to hear about the future direction of it. Here’s a snippet from that article:
The following will not be upgraded to the new XSLT-based list view:
- A list view that uses custom Collaborative Application Markup Language (CAML)
- A list view that is not associated with a feature
- A list view that is associated with a custom feature
A list view that is not upgraded will still render properly in Windows SharePoint Services 4.0. However, it will not inherit any benefits of the new XSLT-based list view, such as SharePoint Designer customization support, conditional formatting, and improved developer experience with XSLT standard-based language support.
…
The new XSLT-based list view is the default view that will be used in the next version release of Windows SharePoint Services. It will replace the existing list view in Windows SharePoint Services 3.0.
So there’s a couple of significant things with this article – the first is that we can now officially say that the List View Web Part, as a CAML based web part, is dead in the next version and will be replaced with a much better XSLT-based List View Web Part. The second is that you now have some valuable information to keep in mind when doing custom development – avoid creating custom list views if you plan to upgrade to the next version. To clarify the bullets above – based on my understanding (which could be wrong or could change) if you’ve customized a view via the browser you’re safe – but if you’ve customized the CAML directly using a tool or the API (within a Feature for example) then you will have to manually upgrade those views.
Web Part Page History CodePlex Project
Have you ever been working with a web part page (either a standard web part page or a publishing page) and made a bunch of changes to your web part configurations and content over a period of time only to realize sometime down the road that you need to revert to a previous version? You then go to the version history for the page and find the one you want, do the revert and then return to the page only to find that none of your web parts were affected by the revert? If you haven't already encountered this "feature", don't worry, you eventually will
The problem is that SharePoint stores its web part configurations differently than item level (meta data, page fields, whatever) data. So there is no ability to roll back to a previous version of your web part changes.
In order to address this deficiency I decided to put together my first "real" CodePlex project. Of course I have my STSADM extensions that have been quite popular but for the time being I'm not ready to open that project up for collaboration. For this new project, however, I'm much more open to the idea - mainly because I simply can't test it and there may be lots of ways in which people way smarter than me may be able to improve upon what I started (note that initially I'll be very selective as to who can directly contribute but please don't hesitate to share any improvement ideas).
You can find the project here: http://www.codeplex.com/wph. I've also created a reasonably details document that explains how to use the project and how it works. The initial release is very much an alpha release - I've done enough testing to know that it works for simple cases. My goal was to get something, even if it's rough, out to the public so that I can get a sense of what people think of the approach and if there's even a need for it (if you think there is please share your feedback so I know that my time on this was worth it).
Programmatically Setting Web Part Audience Targeting
I've been doing some work on my gl-exportlistitem2 and gl-addlistitem commands so that I can support the import of web part pages. I thought I was about done until I discovered that I had an issue with pages and web parts that were using audience targeting. There was actually two issues - one was that when I imported a page to another farm the GUID used to store the audience didn't match up so it would lose its setting for the page. The other issue was that some web parts (specifically anything that is not a V2 web part) do not export the AuthorizationFilter property which is where the audience settings are stored. I find this very odd that they chose to omit this property when exported. Note that for V2 web parts the audience information is exported because it is stored in the IsIncludedFilter property of the web part which has been marked as obsolete (internally this property just references the AuthorizationFilter property).
When I set out to do this I thought it would be pretty easy - just store the name with the audience ID and store the configuration information in a MetaData element that I have which I could then use to set the property during the import. The problem that I ran into was the format of the AuthorizationFilter property - it's just not documented anywhere! If you view the MSDN documentation for the property it says the following:
The Web Part infrastructure does not implement any default behavior for the AuthorizationFilter property. However, the property is provided so that you can assign an arbitrary string value to a custom Web Part. This property can be checked by SPWebPartManager during its AuthorizeWebPart event to determine whether the control can be added to the page.
The thing is - there's nothing arbitrary about this property - it has a very explicit format which if you don't follow will result in the web part failing to process the audience settings - the format of this matches the format of the Audience field of the web part page - if you mess that field up you will get an error when trying to get into the page settings.
So what is the format of the property? First you need to understand that there are three types of audience information that the property is storing: Global Audiences created via the SSP; Distribution Lists; SharePoint Groups. Each of these values is stored slightly differently and the order of their appearance is important. Each item is delimited with two semi-colons: ";;".
The global audience is stored as a GUID and is the only type of item that can be stored without any delimiter information if no other items exist. Multiple items are separated with a comma.
The distribution list is stored as an LDAP string. Multiple items are separated with a new line character ("\n").
The SharePoint groups are stored as a named value (i.e., "Members", "Owners", etc.) and multiple items are separated with a comma.
So putting it all together you would construct a string containing one or more elements of each like so:
1: string[] globalAudienceIDs = new string[] {"e4687e64-c9d8-4860-bbc3-ec036bf9915d"};
2: string[] dlDistinguishedNames = new string[] {"cn=group1,cn=users,dc=spdev,dc=com", "cn=group2,cn=users,dc=spdev,dc=com"};
3: string[] sharePointGroupNames = new string[] {"Demo Members", "Demo Owners"};
4:
5: string result = string.Format("{0};;{1};;{2}",
6: string.Join(",", globalAudienceIDs),
7: string.Join("\n", dlDistinguishedNames),
8: string.Join(",", sharePointGroupNames));
That being said - if you can avoid hard coding this formatting than you should. So how do you avoid it - you use the GetAudienceIDsAsText method of the AudienceManager class. To use this method you would do something like the following:
1: string[] globalAudienceIDs = new string[] {"e4687e64-c9d8-4860-bbc3-ec036bf9915d"};
2: string[] dlDistinguishedNames = new string[] {"cn=group1,cn=users,dc=spdev,dc=com", "cn=group2,cn=users,dc=spdev,dc=com"};
3: string[] sharePointGroupNames = new string[] {"Demo Members", "Demo Owners"};
4:
5: string result = AudienceManager.GetAudienceIDsAsText(globalAudienceIDs, dlDistinguishedNames, sharePointGroupNames);
What's cool is that there is a reciprocal method called GetAudienceIDsFromText which will give you the IDs based on the string.
In my particular case I wasn't able to use these and had to know the format of the string because I needed to supplement the GUIDs with the named value so that I could do an import in a different farm.
Note that this same format is used for the Audience field of a publishing page.
Retarget Grouped Listings Web Parts
When we did our test upgrade I found that "almost" every single page that had a Grouped Listings Web Part (which is basically just a content query web part) had its source changed from showing items from the "Listings" list to instead show all items from the site. Unfortunately there were too many of them to change manually and the gl-retargetcontentquerywebpart command that I had didn't have sufficient options to restrict the changes to do what I needed (change the web part to use the "Listings" list as the source). So I decided to just create a new command using the gl-retargetcontentquerywebpart code as the basis: gl-retargetgroupedlistingswebpart. The code that actually does the retargetting of the web part is the same code used by the gl-retargetcontentquerywebpart command - I've just wrapped the core method with the appropriate looping and filtering constructs:
1: private static void RetargetGroupedListings(SPWeb web, SPList list, SPFile file, string webPartTitle)
2: {
3: bool wasCheckedOut = true;
4:
5: if (!Utilities.EnsureAspx(file.Url, true, false))
6: return; // We can only handle aspx and master pages.
7:
8: if (file.CheckOutStatus == SPFile.SPCheckOutStatus.None)
9: {
10: file.CheckOut();
11: wasCheckedOut = false;
12: // If it's checked out by another user then this will throw an informative exception so let it do so.
13: }
14: else if (!Utilities.IsCheckedOutByCurrentUser(file.Item))
15: {
16: // We don't want to mess with files that are checked out to a different user so skip to the next.
17: return;
18: }
19: bool modified = false;
20:
21: SPLimitedWebPartManager manager = null;
22: try
23: {
24: manager = web.GetLimitedWebPartManager(file.ServerRelativeUrl, PersonalizationScope.Shared);
25: foreach (WebPart webPart in manager.WebParts)
26: {
27: #pragma warning disable 612
28: ListingSummary cqwp = webPart as ListingSummary;
29: #pragma warning restore 612
30:
31: if (cqwp == null)
32: continue;
33:
34: if (cqwp.DisplayTitle.ToLowerInvariant() == webPartTitle.ToLowerInvariant())
35: {
36: Guid existingGuid = Guid.Empty;
37: if (!string.IsNullOrEmpty(cqwp.ListGuid))
38: existingGuid = new Guid(cqwp.ListGuid);
39:
40: if (!existingGuid.Equals(list.ID))
41: {
42: RetargetContentQueryWebPart.AdjustWebPart(cqwp, list, web);
43:
44: manager.SaveChanges(cqwp);
45: modified = true;
46: }
47: }
48: webPart.Dispose();
49: }
50:
51: }
52: finally
53: {
54: if (manager != null)
55: {
56: manager.Web.Dispose();
57: manager.Dispose();
58: }
59:
60: if (modified)
61: file.CheckIn("Checking in changes to page layout due to retargeting of grouped listings web part.");
62: else if (!wasCheckedOut)
63: file.UndoCheckOut();
64:
65: if (modified && !wasCheckedOut)
66: {
67: file.Publish("Publishing changes to page layout due to retargeting of grouped listings web part.");
68: if (file.Item.ModerationInformation != null)
69: file.Approve("Auto-approving changes to page layout due to retargeting of grouped listings web part.");
70: }
71: }
72: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-retargetgroupedlistingswebpart
stsadm -o gl-retargetgroupedlistingswebpart
Retargets grouped listings web parts upgraded from V2 (points web parts with the specified title to the "Listings" list)
Parameters:
-url <url to search>
-scope <Farm | WebApplication | Site | Web | Page>
[-title <title of web parts to search for (default is "Grouped Listings")>]
[-publish]
Here's an example of how to retarget all the grouped listings web parts with a title of "Grouped Listings" to the appropriate "Listings" list in a web application:
SPLimitedWebPartManager.Dispose() bug
I found what I believe to be a bug and figured I’d pass along my findings. If you’re working with the SPLimitedWebPartManager object and call Dispose() on it there’s a minor problem – the Dispose() method does not dispose of the SPWeb object that gets loaded up – if you’re only using one instance this isn’t much of a problem but if you’re doing a lot of looping then you’ll eventually run out of memory on the server.
I have an stsadm command that I created which replaces content within web parts throughout a site – eventually the command fails because there’s not enough memory – I solved my issues by calling manager.Web.Dispose() (where manager is an SPLimitedWebPartManager object) right before calling manager.Dispose(). I also noticed that it doesn’t dispose of the web parts in the SPLimitedWebPartCollection object though this didn’t seem to be causing me any problems but I think it’s because I was already disposing of them when I was finished processing.
Replace Web Part Content
If you've read some of my earlier posts you'll know that I've been working on 3 commands to enable replacing content throughout the site in order to deal with the broken links I've encountered as a result of moving so many sites around for our upgrade.
The first command was gl-replacefieldvalues and the second command was gl-applyupgradeareaurlmappings. This third command, gl-replacewebpartcontent, is focused on replacing content in various built-in web part types. The code for the command, and even the usage of the command, is similar in many respects to the gl-replacefieldvalues command. There are a series of methods used to handle the different scope options that can be passed in and then a primary method which handles the processing of the various files.
Once the web parts have been identified on the page this primary method calls another method based on the web part type. These methods that take in a web part as a parameter are fairly similar in structure with the core difference being what properties are evaluated and modified as each web part type is considered independently. I'm currently only considering the following web parts: ContentEditorWebPart, PageViewerWebPart, ImageWebPart, SiteDocuments, SummaryLinkWebPart, DataFormWebPart, and ContentByQueryWebPart. The last two are only really helpful when the value you are replacing is a List ID - I use the methods that handle these two web part types in my gl-repairsitecollectionimportedfromsubsite command, gl-convertsubsitetositecollection command, and my gl-moveweb command in order to retarget web parts that were hard coded to point to a specific List ID which would have changed as a result of the move.
There's quite a bit of code for this command but most of it is more or less cookie cutter so I'll only show a portion of it. The following code shows the primary decision maker method which determines which web part method should be called. I've also included one method showing how I handle the ContentEditorWebPart - remember that each web part will have it's own way of setting its content.
1: /// <summary>
Set Web Part State
As part of my upgrade I needed to be able to delete some web parts (closed or open) and close and open some web parts. In many respects this command, gl-setwebpartstate, is basically the twin to the gl-movewebpart command which I recently posted about. In fact, I created that command as a template for this command - the only difference from a code standpoint is that instead of calling MoveWebPart I'm calling either DeleteWebPart, CloseWebPart, or OpenWebPart (based on a user provided parameter). Everything else is exactly the same so once I had the gl-movewebpart command done creating this command took me less than 5 minutes. Below is the bit of code that differs from the gl-movewebpart code (I won't show the rest as it's identical to the gl-movewebpart command):
1: if (Params["delete"].UserTypedIn)
2: manager.DeleteWebPart(wp);
3: else if (Params["close"].UserTypedIn)
4: manager.CloseWebPart(wp);
5: else if (Params["open"].UserTypedIn)
6: manager.OpenWebPart(wp);
7:
8: if (!Params["delete"].UserTypedIn)
9: manager.SaveChanges(wp);
The syntax of the command I created can be seen below.
C:\>stsadm -help gl-setwebpartstate
stsadm -o gl-setwebpartstate
Opens, Closes, Adds, or Deletes a web part on a page.
Parameters:
-url <web part page URL>
{-id <web part ID> |
-title <web part title>}
{-delete |
-open |
-close |
-add}
{[-assembly <assembly name>]
[-typename <type name>] |
[-webpartfile <web part file if adding>]}
[-zone <zone ID>]
[-zoneindex <zone index>]
{[-properties <comma separated list of key value pairs: "Prop1=Val1,Prop2=Val2">] |
[-propertiesfile <path to a file with xml property settings (<Properties><Property Name="Name1">Value1</Property><Property Name="Name2">Value2</Property></Properties>)>]}
[-publish]
|
Here’s an example of how to close a web part on a given page:
stsadm -o gl-setwebpartstate -url "http://intranet/hr/pages/default.aspx" -title "Grouped Listings" -close -publish
Update 12/10/2007: I've updated this command to now support the adding of web parts to a page. In doing this I also added the ability to set the zone and zone ID of a web part (thus encapsulating the gl-movewebpart command - I needed this in order to know where to add the part to so I figured I'd just allow the user to provide the same information for any other changes). I also added the ability to set properties of the web part. This is done using a comma separated list of key value pairs for simple data or an XML file with any encoded data. Note that only primitive data types are supported so if you try to set a property that requires a complex data type it will error out. Also - if you only wish to set the properties of an existing web part then simply pass in a command that matches the current state of the web part (so if the web part is open already then use the "-open" parameter and then pass in any desired properties).
Here's an example of how to add a web part to a page using simple properties:
stsadm -o gl-setwebpartstate -url "http://intranet/testweb1/default.aspx" -title "Table of Contents" -add -assembly "Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -typename "Microsoft.SharePoint.Publishing.WebControls.TableOfContentsWebPart" -zone "Left" -zoneindex 0 -properties "ShowPages=false,LevelsToShow=3" -publish
Here's an example of how to add a web part to a page using an XML file containing properties:
stsadm -o gl-setwebpartstate -url "http://teamsites/pages/default.aspx" -title "I need to..." -add -assembly "Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -typename "Microsoft.SharePoint.Portal.WebControls.TasksAndToolsWebPart" -zone "MiddleRightZone" -zoneindex 0 -propertiesfile "c:\webpartprops.xml" -publish
The webpartprops.xml file will look like this:
<Properties> <Property Name="TasksAndToolsWebUrl">/SiteDirectory</Property> <Property Name="FilterFieldValue">Top Tasks</Property> <Property Name="FilterCategory">TasksAndTools</Property> <Property Name="TasksAndToolsListName">Sites</Property> <Property Name="Xsl"><xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" version="1.0" exclude-result-prefixes="xsl ddwrt slwrt msxsl" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:slwrt="http://schemas.microsoft.com/WebParts/v3/SummaryLink/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:tnt="urn:schemas-microsoft-com:sharepoint:TasksAndToolsWebPart" > <xsl:param name="tasksAndtools_IsRTL" /> <xsl:param name="tasksAndTools_Width" /> <xsl:template match="/"> <xsl:call-template name="MainTemplate"/> </xsl:template> <xsl:template name="MainTemplate" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"> <xsl:variable name="Rows" select="/dsQueryResponse/NewDataSet/Row"/> <xsl:variable name="RowCount" select="count($Rows)"/> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse:collapse; margin:0px;"> <tr style="margin-top:3px;margin-bottom:1px;height:28px;border:0px;"> <td style="padding-left:4px; white-space:nowrap ;"> <xsl:if test="string-length($tasksAndTools_Width) != 0"> <select id="TasksAndToolsDDID" class="ms-selwidth" style="width:{$tasksAndTools_Width}" size="1" title="Choose a task that you need to perform" > <option selected="true" value="0">Choose task</option> <xsl:call-template name="MainTemplate.body"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </select> </xsl:if> <xsl:if test="string-length($tasksAndTools_Width) = 0"> <select id="TasksAndToolsDDID" class="ms-selwidth" size="1" title="Choose a task that you need to perform"> <option selected="true" value="0">Choose task</option> <xsl:call-template name="MainTemplate.body"> <xsl:with-param name="Rows" select="$Rows"/> </xsl:call-template> </select> </xsl:if> </td> <xsl:if test="$tasksAndtools_IsRTL = true()"> <td style="padding-right:5px; padding-left: 14px;white-space:nowrap ;"> <a id="TasksAndToolsGo" accesskey="G" title="Go" href="javascript:TATWP_jumpMenu()"> <img title="Go" alt="Go" border="0" src="/_layouts/images/icongo01RTL.gif" style="border-width:0px;" onmouseover="this.src='/_layouts/images/icongo02RTL.gif'" onmouseout="this.src='/_layouts/images/icongo01RTL.gif'"/> </a> </td> </xsl:if> <xsl:if test="$tasksAndtools_IsRTL = false()"> <td style="padding-right:14px; padding-left: 5px;white-space:nowrap ;"> <a id="TasksAndToolsGo" accesskey="G" title="Go" href="javascript:TATWP_jumpMenu()"> <img title="Go" alt="Go" border="0" src="/_layouts/images/icongo01.gif" style="border-width:0px;" onmouseover="this.src='/_layouts/images/icongo02.gif'" onmouseout="this.src='/_layouts/images/icongo01.gif'" /> </a> </td> </xsl:if> </tr> </table> </xsl:template> <xsl:template name="MainTemplate.body" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"> <xsl:param name="Rows"/> <xsl:for-each select="$Rows"> <xsl:variable name="GroupStyle" select="'auto'"/> <option style="display:{$GroupStyle}" value="{ddwrt:EnsureAllowedProtocol(substring-before(@URL, ', '))}" > <xsl:value-of select="@Title"/> </option> </xsl:for-each> </xsl:template> </xsl:stylesheet></Property> </Properties>
Update 7/8/2008: I've added a new parameter, -webpartfile, which can be used to effectively import an exported web part file. Just use it in conjunction with the -add option (do not use the -assembly or -typename parameters). I also adjusted the code so that if it fails to add the web part using the object model it will revert to the web service - this is to account for some web parts (like the KPI web part) that require a valid SPContext object.
Retarget Content Query Web Part
As I was going through my upgraded and moved sites I discovered that the "Grouped Listings" web parts were not working on my webs that I'd migrated from a subsite to a site collection or moved from one site collection to another. I didn't even notice they weren't working at first because when viewing a published page they were simply not showing anything (no title, or message about no results being returned or anything). When I went to edit a page I saw that the Grouped Listings web part was on the page but there was a message stating that the query returned no items. When I went to edit the web part instead of getting the nice toolbar I got taken to an error page which stated that the list did not exist.
After digging further I discovered that the web part was using the list ID (GUID) of the previously migrated list and not the new list that was created as a result of the move. The problem though was that the browser wouldn't let me retarget the web part to the correct list. So I had one of three options to solve the problem: delete the web part and replace it with something different; export the web part, change the GUID in the exported XML, import the changed web part and add it to the page, then delete the original web part; or programmatically manipulate the existing web part to point it to the correct list.
If you've read anything on this blog then you'll know which route I took. Programmatically fixing the web part was the most obvious choice to me because I could not only make the code stand alone so that it could be used to retarget any list but I could also re-use the code in my gl-moveweb and gl-convertsubsitetositecollection commands so that those commands would automatically adjust any detected Grouped Listings web parts.
It's important to note that the "Grouped Listings" web part that I'm referring to here is actually a ListingSummary web part which inherits from the ContentByQueryWebPart. This web part is created during an upgrade - in SPS 2003 there's a built-in list called Listings which is consumed by the Grouped Listings web part. During the upgrade this built-in list is converted to a standard list of the same name. But now it's not built-in so it can be edited like any other list. There's also a new content type added called "Listing" and the Listings list inherits from this content type.
Because the Grouped Listings web part is a bit "special" I decided to add some extra code into the command to help "repair" a content query web part that is pointed to a Listings list. That way if you delete the "Grouped Listings" web part but want to get something similar back you can add a new Content Query web part and point it to the Listings list and then run this command to set the properties that you simply cannot set via the browser. Other than this "special" code that I added the command itself basically just allows you to point a content query web part to a different list or site within the site collection. The core code is shown below:
1: /// <summary>
2: /// Adjusts the web part.
3: /// </summary>
4: /// <param name="web">The web.</param>
5: /// <param name="wp">The web part.</param>
6: /// <param name="manager">The web part manager.</param>
7: /// <param name="listUrl">The list URL.</param>
8: /// <param name="listType">Type of the list (list template).</param>
9: /// <param name="siteUrl">The site URL.</param>
10: internal static void AdjustWebPart(SPWeb web, WebPart wp, SPLimitedWebPartManager manager, string listUrl, string listType, string siteUrl)
11: {
12: ContentByQueryWebPart cqwp = wp as ContentByQueryWebPart;
13: if (cqwp == null)
14: throw new SPException("Web part is not a Content Query web part.");
15:
16: if (listUrl != null)
17: {
18: SPList list = Utilities.GetListFromViewUrl(web, listUrl);
19: if (list == null)
20: throw new SPException("Specified List was not found.");
21:
22: if (list.ContentTypes["Listing"] != null)
23: {
24: // The list is a special list - it was upgraded from v2 and corresponds
25: // to the grouped listings web part so we need to set some additional
26: // properties that cannot be set via the browser.
27:
28: ApplyListTypeChanges(web, cqwp, "Links");
29:
30: cqwp.AdditionalGroupAndSortFields = "Modified,Modified;Created,Created";
31: cqwp.DataColumnRenames = "SummaryTitle,Title;Comments,Description;URL,LinkUrl;SummaryImage,ImageUrl";
32: cqwp.SortByFieldType = "Number";
33: cqwp.ChromeType = PartChromeType.None;
34: cqwp.CommonViewFields = "SummaryTitle,Text;Comments,Note;URL,URL;SummaryImage,URL;SummaryIcon,URL;SummaryType,Integer;_TargetItemID,Note";
35: cqwp.FilterType1 = "ModStat";
36: cqwp.Xsl = "<xsl:stylesheet xmlns:x=\"http://www.w3.org/2001/XMLSchema\" version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xmlns:cmswrt=\"http://schemas.microsoft.com/WebPart/v3/Publishing/runtime\" exclude-result-prefixes=\"xsl cmswrt x\" > <xsl:import href=\"/Style Library/XSL Style Sheets/Header.xsl\" /> <xsl:import href=\"/Style Library/XSL Style Sheets/ItemStyle.xsl\" /> <xsl:import href=\"/Style Library/XSL Style Sheets/ContentQueryMain.xsl\" /> </xsl:stylesheet>";
37: cqwp.FilterValue1 = "Approved";
38: cqwp.ShowUntargetedItems = false;
39: cqwp.FilterField1 = list.Fields["Approval Status"].Id.ToString();
40: cqwp.Filter1ChainingOperator = ContentByQueryWebPart.FilterChainingOperator.And;
41: cqwp.ItemLimit = -1;
42: cqwp.SortBy = list.Fields["Order"].Id.ToString();
43: cqwp.SortByDirection = ContentByQueryWebPart.SortDirection.Asc;
44: cqwp.GroupByDirection = ContentByQueryWebPart.SortDirection.Asc;
45: cqwp.Description = "Show listings in the portal.";
46: cqwp.GroupBy = "SummaryGroup";
47: cqwp.Hidden = false;
48: }
49: cqwp.WebUrl = list.ParentWeb.ServerRelativeUrl;
50: cqwp.ListGuid = list.ID.ToString();
51: cqwp.ListName = list.Title;
52: }
53: else if (siteUrl != null)
54: {
55: try
56: {
57: SPWeb web2 = web.Site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(siteUrl)];
58: if (!web2.Exists)
59: throw new SPException();
60: }
61: catch (ArgumentException)
62: {
63: throw new SPException(siteUrl + " either does not exist is is invalid or does not belong to the web part's container site collection.");
64: }
65: catch (SPException)
66: {
67: throw new SPException(siteUrl + " either does not exist is is invalid or does not belong to the web part's container site collection.");
68: }
69: catch (FileNotFoundException)
70: {
71: throw new SPException(siteUrl + " either does not exist is is invalid or does not belong to the web part's container site collection.");
72: }
73:
74: cqwp.WebUrl = siteUrl;
75: cqwp.ListGuid = string.Empty;
76: cqwp.ListName = string.Empty;
77: }
78: else
79: {
80: cqwp.WebUrl = string.Empty;
81: cqwp.ListGuid = string.Empty;
82: }
83:
84: if (listType != null)
85: ApplyListTypeChanges(web, cqwp, listType);
86:
87:
88: manager.SaveChanges(cqwp);
89: }
90:
91: /// <summary>
92: /// Applies the list type changes.
93: /// </summary>
94: /// <param name="web">The web.</param>
95: /// <param name="cqwp">The content query web part.</param>
96: /// <param name="listType">Type of the list.</param>
97: private static void ApplyListTypeChanges(SPWeb web, ContentByQueryWebPart cqwp, string listType)
98: {
99: if (listType == null)
100: return;
101:
102: SPListTemplateCollection listTemplates = web.Site.RootWeb.ListTemplates;
103:
104: SPListTemplate template = listTemplates[listType];
105: if (template == null)
106: throw new SPException("List template (type) not found.");
107:
108: cqwp.BaseType = string.Empty;
109: cqwp.ServerTemplate = Convert.ToString((int)template.Type, CultureInfo.InvariantCulture);
110:
111: bool isGenericList = template.BaseType == SPBaseType.GenericList;
112: bool isIssueList = template.BaseType == SPBaseType.Issue;
113: bool isLinkList = template.Type == SPListTemplateType.Links;
114: cqwp.UseCopyUtil = !isGenericList ? isIssueList : (isLinkList ? false : true);
115: }
The syntax of the command I created can be seen below.
C:\>stsadm -help gl-retargetcontentquerywebpart
stsadm -o gl-retargetcontentquerywebpart
Retargets a Content Query web part (do not provide list or site if you wish to show items from all sites in the containing site collection).
Parameters:
-url <web part page URL>
{-id <web part ID> |
-title <web part title>}
[-list <list view URL>]
[-listtype <list type (template) to show items from>]
[-site <show items from this site and all subsites>]
[-allmatching (if title specified and more than one match found, adjustall matches)
[-publish]
Here’s an example of how to retarget all web parts titled "Grouped Listings":
stsadm -o gl-retargetcontentquerywebpart -url "http://intranet/hr/pages/default.aspx" -title "Grouped Listings" -list "http://intranet/hr/lists/summary links/allitems.aspx" -allmatching
Note that you can specify a list type in the event that you are trying to piont the web part to a completely different type of list (this may not work smoothly if your list has completely different content types and fields as the web part will still attempt to display the fields that it was originally setup to display).
I've updated the gl-moveweb and gl-repairsitecollectionimportedfromsubsite commands (used by gl-convertsubsitetositecollection) so that they will attempt to repair the Grouped Listings web parts.