Woohoo! – I finally get to deprecate one of my commands – after creating and publishing 135 commands I discovered today that the August Cumulative Update fixes the issue that my gl-fixpagecontact command sought to address. If you look back at the post for that command you can see that there was a bug with the product which resulted in a user not found exception being thrown when looking at the settings of a publishing page. This happened in couple of different situations but the easiest way to trigger it was to simply delete the user that created the page (or that was assigned as the page contact or that last modified the page) from the site collection. When you did this and then visited the settings page you would get an error similar to the following:
The error would vary slightly depending on whether or not the field was the “Contact”, “CreatedBy”, or “LastModifiedBy” field. I decided to revisit this command due to a comment that was posted on the post related to it which asked if the command could be modified to handle the CreatedBy and LastModifiedBy fields (I was only handling the Contact field). I did some digging and have not yet discovered a way to update these two fields for a document library (if anyone has a way to do this please let me know) so I decided to send an email out to Paul Andrew to see if the bug had already been addressed - I usually pay attention to what fixes went into each update but I guess I missed this because within just a few minutes I got a response from Paul stating that this was indeed fixed with the August update (http://support.microsoft.com/kb/956056 and http://support.microsoft.com/kb/956057). So I decided to go ahead and update my development machine to confirm this and Merry Christmas it worked! After the updates got installed the settings page loaded fine and the invalid fields were set to my system account – now I’m not sure what logic is used here to determine what user is used – I was logged in with my system account so I don’t know if it’s using the logged in user or the system account but regardless, the page loads and that’s the important part.
So I am now officially deprecating this command – I’ll keep it in with the package but I won’t be supporting it further now that I can finally tell people to just install the update! Thank you Paul for your help and quick responses!!!
I'm working on a project which is going to require the creation of about a hundred publishing pages which were going to have to be created by hand. I really didn't want to have to sit there going through the UI to create all those pages so I threw together a new STSADM command that I could use via a script to create the pages: gl-createpublishingpage.
Creating publishing pages via code is pretty simple - you just use the PublishingWeb object's GetPublishingPages() method to return back the collection of pages and then call its Add method passing in the page name and layout. From there it's just a matter of setting the field properties.
The help for the command is shown below:
C:\>stsadm -help gl-createpublishingpage stsadm -o gl-createpublishingpage Creates a new publishing page. Parameters: -url <url to the publishing web within which to create the page> -name <the filename of the page to create (do not include the extension)> -title <the page title> -layout <the filename of the page layout to use> [-fielddata <semi-colon separated list of key value pairs: "Field1=Val1;Field2=Val2"> (use ';;' to escape semi-colons in data values)]
The following table summarizes the command and its various parameters:
|Command Name||Availability||Build Date|
|gl-createpublishingpage||MOSS 2007||Released: 9/4/2008
|Parameter Name||Short Form||Required||Description||Example Usage|
|url||Yes||The URL to the publishing web within which to create the page.||-url http://portal|
|name||n||Yes||The filename of the page to create. It is not necessary to include the extension. Do not use special characters.||-name NewPage
|title||t||Yes||The title of the page. Wrap within quotes if includes spaces.||-title "New Page"
-t "New Page"
|layout||l||Yes||The filename of the page layout to use.||-layout DefaultLayout.aspx
|fielddata||fd||No||Key/Value pairs of metadata to apply to the new page. Use the internal field name and separate multiple pairs with a semi-colon. Use the following format when setting data: "Field1=Val1;Field2=Val2".||-fielddata "PublishingContactEmailemail@example.com;PublishingContactName=First Last"
-fd "PublishingContactEmailfirstname.lastname@example.org;PublishingContactName=First Last"
The following is an example of how to add a new publishing page:
stsadm -o gl-createpublishingpage -url http://portal -name NewPage -title "New Page" -layout DefaultLayout.aspx
Update 12/29/2008: This command has been deprecated – the issues described below have been resolved with the August 2008 Cumulative Update (http://support.microsoft.com/kb/956056 and http://support.microsoft.com/kb/956057).
I've been doing a lot of migration work these days and if you've ever had to migrate content from one farm (or site collection) to another you know that all sorts of things can go wrong (many of which have been documented in one form or another on this blog). One of the issues I've recently come across is related to the contact settings for a publishing page. There are two ways in which this setting can become invalid and in both circumstances there is no way to fix the problem without using either SharePoint Designer or code (the invalid contact results in an error being thrown when trying to view the Page Settings and Schedules page).
The first way to end up with an invalid contact is to simply delete the contact from the list of users in the site collection. Now, whether or not you should ever delete users is a topic for a different post but if you delete the contact that is associated with a page and then try to view the Page Settings and Schedule page you will see the following "User cannot be found" error:
It's pretty frustrating that the page isn't sensitive enough to realize the contact is invalid and therefore either clear the setting or display some warning but at least let me load the page so that I can fix the error without having to crack open SPD.
The second way that this error can manifest itself is via a content migration. For example - you have a valid contact assigned to the page in your test or staging environment and then you migrate the page to your production environment which is on a different domain and thus the page contact does not exist in that domain. This is the scenario that I've been dealing with and because I've been moving pages in bulk I needed a better way than to use SPD every time I migrated a page - I wanted to be able to fix the issues as part of my deployment script. The result is a new command I created called gl-fixpagecontact.
To better understand why the contact can become invalid (even in a migration scenario where your usernames are the same in both domains) it's important to understand how the information is stored. The contact information is stored in the "Contact" (FieldId.Contact) field of the pages SPListItem object and the format is the users ID followed by ";#" (without the quotes) followed by the users display name: 20;#Test User
The ID shown is (obviously) not the SID of the user but rather the ID of the user in the site collections users list and obviously this ID will vary from one farm to another (and even from one site collection to another). Note that if you delete a user from the site collection and then add them back in their ID will be set to the original ID so to fix the first scenario you can simply re-add the user, fix the contact for the page manually and then re-delete the user.
So what does my new command do? It parses the data in the field (splits on ;#) and uses the display name to try and find a principle that matches the principle regardless of the ID. If it can't find a match then it will replace the contact with either the current user or a user provided via the "-contact" parameter. Note that you can also use this command to force all pages to have a contact set - it is not invalid to have an empty contact but your business rules may require that all pages have a contact set. If you don't pass the "-allowempty" flag it will force the contact to be set to either the current user or a named user (via the -contact parameter).
Here's the syntax of the command:
C:\>stsadm -help gl-fixpagecontact stsadm -o gl-fixpagecontact Fixes the Page Contact property of a publishing page or pages if the current contact is invalid. Parameters: -url <url> -scope <WebApplication | Site | Web> [-pagename <the name of the page to update>] [-contact <DOMAIN\name (contact user name to assign to the page - if not provided the current user is used)>] [-allowempty (allow empty contact values)] [-verbose] [-test]
Like many of my commands that do mass updates of data I provide a "-test" parameter so that you can simulate the execution and see what it would have changed without having it actually make changes - this is useful for just identifying your problem pages. Here's a simple example of how you would run this command to fix all pages in a given site collection:
stsadm -o gl-fixpagecontact -url http://demo -scope site -allowempty -verbose
If you want to just fix a single page in a single web you would run the following:
stsadm -o gl-fixpagecontact -url http://demo -scope web -pagename default.aspx -verbose
I've recently done a lot of work with my current client to get SharePoint variations working and as a result I've learned that you need to be very patient and persistent and keep a full bottle of Excedrin nearby. Though this post is not directly related to stsadm extensions the results of my efforts on this project produced a couple of new commands which I'll touch upon here and detail in a follow-up post. Before you begin working on a multilingual site using variations I highly suggest that you read the Building Multilingual Solutions Using 2007 SharePoint Products and Technologies white paper.
Variations in, a nutshell, allow you to present the same pages using different views of the page. Though variations are typically used for multilingual sites they can also be used for other things such as media specific views. For this post I'm going to create multilingual variation with the source variation being English and a single target variation using Korean. The first thing we need to do is make sure that the operating system is ready for our language packs to be installed. If you're using an East Asian language, such as Korean, then you will need to prep the OS, otherwise the OS should already be capable of handling the language pack. To install the East Asian languages on the OS go to Start -> Control Panel -> Regional and Language Options. On the "Languages" tab select the appropriate options and click OK. This will install the necessary language files which will require a reboot when complete.
Now that the OS is prepared you are ready to install the language packs. The install is real straightforward so I won't detail it here. If you don't already have a web application created to host your variation sites you'll want to do that now. Once you've identified your target web application you are ready to create your site collection. It's important to note that the variations publishing feature is scoped at the site collection level - this means that you cannot have a source variation in one site collection and a target variation in another. Also, the variation features are installed with the publishing feature so you must pick a publishing template when creating your initial site collection or activate the publishing features. For your root site collection make sure you pick the language template that your administrators are going to be able to work with as your end-users will never see the root site collection stuff.
To create your variation hierarchy you must first enable variations. This is done by going to the root of the site collection and selecting "Site Actions -> Site Settings -> Modify All Settings -> Variations". You first need to establish the variation root site - typically you'll just put a "/" which will make the root of site collection the variation root but you can make the variation root be a sub-site of the site collection. The other options are fairly straightforward but the "Update Target Web Parts" and "Resources" options may need some clarification. If you choose to update web parts with changes from the source be aware that any translations or changes that you may have made in the target variations will be overwritten with that of the source. So if you're using a content editor web part and translating that content on the target sites then those translations will be overwritten when the page is propagated. This may or may not be what you intend to happen.
One other thing that may require some additional clarification is the Resources option. If you leave the option set to reference existing resources then there's essentially no change to the content - it will simply references images, for example, on the source site. The copy option, however, will copy images to the target variations. Note that if the image in question does not exist in the variations PublishingImages library then it won't be copied. I've not had to deal with other resource types so I can't say what the limitations are for anything other than images.
Once you've established that the site collection is to use variations you now need to create your variation labels. When you initially create your labels you are not actually creating any instances of a site - you are just telling SharePoint how you want your sites to be created. The sites are actually created later when you choose to create the variation hierarchy. The first label you want to create is your source label. It's important to note that once you've established your source you cannot change it later. If you would a custom site template to be available here make sure that the FilterCategories property of your site template is set to PublishingSiteTemplate (you can find information about how to create your own publishing site templates here). The "Label Name" field is what will appear in your URL - so if you specify "en-us" as the label name your url for the variation will look like "http://demo/en-us/". For the "Hierarchy Creation" option you need to specify what you want copied when the label hierarchy is created. The other options on this screen are very straightforward - you just need to specify the language template to use and the local and a name.
For this demo I've setup a source variation with a label name of "EN", "English" as the language template, "English (United States)" as the locale, "US-English" as the name, and "Publishing Site with Workflow" as the site template. I've also chosen to copy the publishing site and all pages during hierarchy creation.
Once your source variation is setup you can set up all of your target variations. Note that you don't have to set the target variations up right away - you can create your hierarchy now and have it just create your source variation, get it looking/working the way you want and then add target variations as you are ready to handle them. I would recommend though that you at least create one target variation so that you can work out the various issues that you're likely to encounter (see below) before you roll out your source variation.
For the purposes of this demonstration I'm going to create one target variation label which I'll name "KO". The language template and locale will be set to "Korean".
Now that you've got your variations created you can go to the site. Notice that if you go to the root of the site collection (in my case "http://demo/" you will be automatically redirected to the variation whose locale matches that of your browser. If SharePoint is unable to find a locale match then it will redirect you to the source variation. The redirect is happening because SharePoint has replaced the welcome page of the root site collection with a new page called "VariationRoot.aspx". It's important to note that the default.aspx page is still present and if a user navigates to that page they will see it so I recommend either deleting this page or adding a redirect web part or something similar to redirect users to the VariationRoot.aspx file so that it can do its thing.
Okay, so you've got your variations set up, now what? Now it's time to create some content (realistically though you're going to want to work on branding and custom page layouts first but as there are tons of posts on how to do this I'm going to assume you've got that covered and are ready for creating content). Editing the page in your source variation is no different than editing any publishing page. However, once you publish the page the changes that you have made will be automatically copied to the variations. Note that the act of copying the page is the result of an event receiver that is hooked up to the Pages library. The event receiver will inspect the page and determine what needs to occur and then trigger a timer job to handle the actual copying of the page from the source library to all target libraries. This timer job uses the Deployment API to copy the pages and if you've read any of my earlier posts you know that this deployment API has numerous limitations (I'll touch upon a couple of problems below). Because a timer job is used you won't see the pages propagated immediately - it could take several seconds to several minutes (I've even see it take several hours to show up). If you'd like the page to be copied immediately then you can trigger it by selecting the "Update Variations" option from the "Tools" menu on the page editing toolbar. If the page doesn't already exist in a variation you can use the "Submit a Variation..." link to submit the page to all variations. If the page already exists in all variations then the "Submit a Variation..." link will effectively just work like the Create Page action. Note that you can also use content deployment jobs to handle the translation of content by external agencies. The XML that results from a content deployment job is a nightmare to work with but it wouldn't take much to provide a custom interface to that could be provided to your translators to work with the content.
Now that you've got your changes made and you've created the variation, how do you get to that other pages? One option is to simply edit the URL but you're users aren't going to know how to do that. Fortunately there's a built in user control that we can enable called the Variation Label Control. The control works via a SharePoint delegate control which is added to the master page of all publishing sites. To enable the control you simply have to un-comment the server control tag in the "12\template\controltemplates\VariationsLabelMenu.ascx" file. Once this has been enabled by un-commenting the server control tag then a drop down menu will appear at the top of the site allowing the user to jump to matching pages in other variations.
It's important to understand how SharePoint relates the various pages to each other because in certain situations this relationship can get corrupted. At the root of the site collection you can find a hidden list called "Relationships List". This list contains entries which map the relationship between the various pages. The behavior of the Variation Label Menu control is driven by this list so if the menu is not listing all pages then most likely it is because the Relationships List became corrupted (for example, an administrator on the Korean side decided to prevent changes from being propagated by deleting the page in the Korean Pages library and recreating it with new translated content).
Let's take a look at this hidden list - in our demo site we'll navigate to "http://demo/Relationships%20List/AllItems.aspx". Once here we immediately see a few items with the title set to nothing (the first thing I usually do is create a new view showing at least the following columns: GroupID, ObjectID, and Deleted. There will always be at least one entry in this list corresponding to the variation home which we set up earlier and defined as "/". This entry, which will be the first item in the list, will always have "F68A02C8-2DCC-4894-B67D-BBAED5A066F9" as it's GroupID and whatever was set as the home path as it's ObjectID. ParentAreaID has been empty in every example I've ever seen and does not appear to be used. The GroupID field is used to group related elements. So each variation label in the hierarchy will have the same GroupID and each page will have the same GroupID as its copied page in each variation. The ObjectID is always the server relative url of the element (so the path to the variation label or page).
It's important to understand though that this is not the only place in which we find this information. SharePoint will also store the GroupID and a backward link to an item in this list within two hidden fields on the page: PublishingVariationGroupID and Publishing VariationRelationshipLinkFieldID, respectively. This information is also replicated in the MetaInfo property bag. If we run my custom gl-exportlistitem2 command we can see what these values are for the default.aspx page:
C:\>stsadm -o gl-exportlistitem2 -url http://demo/en/pages/forms/allitems.aspx -path c:\exportdata\pages_en
The following is a snippet of the Manifest.xml file which results from running the above command (note that I've pulled some elements that were not relevant):
Knowing that this information is stored in two places like this becomes extremely important if you are trying to troubleshoot issues with the variation propagation process. If the data in the Relationships List does not match up with the data in the page or if the related pages do not share the same GroupID then the variation propagation will fail.
Things that can go wrong (and they will)
Now that you hopefully have at least a basic understanding of what variations are and how they work we can begin to discuss where things fall apart (and if your deployment is anything more than demoware then you most likely will run into one or all of these issues). The following issues are what I've personally encountered:
- The Relationships List can get "confused" and have either missing or invalid entries
- The page fields linking the page to the Relationships List can become invalid
- Web parts must inherit from Microsoft.SharePoint.WebPartPages.WebPart or the variation propagation will fail causing the application pool to crash (there is a hotfix for this: http://support.microsoft.com/kb/948947)
- Your pages must have a valid contact or no contact or you will not be able to get into the page settings for the page (this is more of a content deployment problem and not a variation problem)
- Your pages must have a valid page layout URL or the variation propagation will fail causing the application pool to crash (among other things)
- Content types for pages must match
- The variation label menu will only work with variation pages and not system pages (such as a list view page)
- The variation label menu will not preserve querystring values
- If you're working with large sites (several thousand pages and multiple variations) expect propagation times in terms of days (I've not personally dealt with this one but in my small environment with only a few pages it takes close to an hour for the pages to propagate so it's easy to extrapolate out)
- Navigation structure is not preserved from one variation to another
- Root site pages and other content is still available if I know how to get to it
- Custom list definitions and site definitions will need to be localized (use resource files) - definitions reverse engineered using Solution Generator will not be localized by default
- Values in localized choice columns will lose localization settings on propagation
- Only pages in the "Pages" library will be propagated - list items will have to be handled using custom event receivers or workflows
- Page field values are overwritten when a page is propagated
In the following sections I will try to cover each of the above issues and what I did to work around the issue.
1,2 - Relationships List Gets "Confused" and/or Page Fields are Invalid or Missing
There are times when the items in the Relationships List can get out of sync with the pages they are supposed to correspond to or vice-a-versa (either entries could be missing or IDs can be incorrect or mismatched). When this happens you will run into issues with the propagation of page changes to variations. For example, you may receive errors stating that a page already exists when you go to propagate changes to a page. The most common way for this list to get corrupted is when pages are imported from other environments. To fix the data in the Relationships List and the page fields I created a new stsadm command called "gl-fixvariationrelationships". The command essentially loops through all the pages in a variation and tries to find the page with the matching name in the other variations and then rebuilds the data in the Relationships List and the page fields based on its findings. Here's an example of how to run this command:
c:\>stsadm -o gl-fixvariationrelationships -url http://portal -verbose
Update 4/14/2008: I've just finished documenting this command here.
Update 9/2/2008: Tim Dobrinski has a great tool that he's put together for fixing many issues with the relationships list (including addressing sub-sites which I'm not currently dealing with). You can find details about the tool here: http://www.thesug.org/blogs/lsuslinky/Lists/Posts/Post.aspx?List=ee6ea231%2D5770%2D4c2d%2Da99c%2Dc7c6e5fec1a7&ID=21
3 - Web Parts Must Inherit From Microsoft.SharePoint.WebPartPages.WebPart
I consider this particular issue to be a bug with the product because it could have been easily solved. To demonstrate this I created a simple HelloWorld web part which just output some text and inherited from System.Web.UI.WebControls.WebPart. I added this web part to my default.aspx page and published the page. The result is that an InvalidCastException is thrown and the application pool crashes! You can see the error details below - note that you'll see three errors in the event log all saying essentially the same thing.
If I change my web part code to inherit from Microsoft.SharePoint.WebPartPages.WebPart then the variation propagation works just fine. So for your custom web parts the solution is real easy - just inherit from the SharePoint WebPart base class. For third party web parts (the Dundas Chart web parts are a good example of this) you will need to wrap them in a custom web part. The interesting thing to note is that even though the propagation seemed to fail and the application pool crashed I found that in most cases the web part was in fact successfully propagated. The reason for this is because the failure happens in the PublishingPage object's FixWebPartUrlsForVariation method. When this method is called the page and web parts have already been propagated and now the API is simply repairing some URLs within the web parts on the page - so in many cases repairs won't be necessary so everything will work just fine. Microsoft could avoid this bug by simply doing a type check in the FixWebPartUrlsForVariation method before doing the cast.
Note that the only web parts out-of-the-box that are affected by the FixWebPartUrlsForVariation method are those that inherit from IWebPartVariationUpdate which at present only include the ContentByQueryWebPart and the TableOfContentsWebPart.
Update 4/21/2008: There is a hotfix for this issue - http://support.microsoft.com/kb/948947 - thanks to djeeg for adding the comment about this!
4 - Pages Must Have a Valid Contact
There are many ways in which the contact for a page can become invalid. The most common is when a contact is deleted from the site collection. Another way is when a page is imported from one environment to another. Having an invalid contact "shouldn't" prevent the page variation propagation from occurring, so as far as variations are concerned this shouldn't be an issue. However, many shops will choose to have a separate authoring environment and will import the pages into their public environment. In this scenario you are very likely to end up with invalid contacts on your pages. The issue comes when you choose to view the "Page Settings and Schedules" page. If your page has an invalid contact associated with it you will receive a user not found error. Of course the real problem here is that this is the page that allows you to fix the contact so your essentially stuck with no way of fixing the contact. This seems like an obvious bug - the settings page should be able to handle the case of a contact being deleted so that you can assign a valid contact.
To work around these issues I've created another new custom stsadm command which I called "gl-fixpagecontact". This command will identify all pages with an invalid contact and will either attempt to locate a valid contact, assign the current user as the contact, or assign a passed in user as the contact. The contact field for the page is stored as a string in the format of "id;#name". Often times when a page is imported from another environment the name will match but the ID will be different. In this case the command will attempt to locate a principle with a matching name.
Here's an example of how to run this command:
C:\>stsadm -o gl-fixpagecontact -scope site -url http://demo -verbose
5 - Pages Must Have a Valid Page Layout URL
This particular issue can come up in a variety of situations and isn't necessarily limited to variations. There are two ways in which this problem typically manifests itself - the first is when a page is migrated from another site (the entire site may have also been migrated) and the second is when you assign a page layout that exists in your source variation but not in your target variation. To demonstrate the problem I created a new page layout which was just a copy of the WelcomeLinks.aspx page and stored it in the master page gallery located at http://demo/en/_catalogs/masterpage/forms/allitems.aspx - I then changed the /en/default.aspx page to use this new page layout. After approving the page I clicked the "Update Variations" menu item to propagate my page. The result is that 3 error events were logged in the event log and the application pool crashed!
To resolve these issues make sure that you either use only site collection level page layouts or that for every page layout you have in your variation sites you make sure that there is a layout with the same name in every other variation site (the contents can be different - they just need to be named the same). In some cases (particularly when you import the content from another source) you won't be able to reset the page layout using the browser (going to the "Page Settings and Schedules" page will throw a FileNotFoundException). To help fix this problem you can use a command I created a while ago called "gl-fixpublishingpagespagelayouturl". This command has a variety of different options but here's a command of how to explicitly set the layout for a specific page:
C:\>stsadm -o gl-fixpublishingpagespagelayouturl -url http://demo/en -pagename default.aspx -pagelayout "http://demo/_catalogs/masterpage/WelcomeLinks.aspx, http://demo/_catalogs/masterpage/WelcomeLinks.aspx" -verbose -scope page
6 - Page Content Types Must Match
I've not experienced this particular issue but I thought it was worth mentioning here for completeness. Jeremy Jameson gives a great explanation of the problem in his 3 part posts "Dumping MOSS 2007 Variations" so I won't bother re-iterating it here (the gist of it though - use SP1 with custom site definitions if you need to use page content types).
7 - Variations Label Menu Only Works with Publishing Pages
This is rather minor limitation but its one you should be aware of before you decided to use it. If you don't allow any access to system or list pages (/_layouts/viewlsts.aspx or /Documents/Forms/AllItems.aspx for example) then you don't need to worry about this. If you do and you have matching pages/lists throughout all your variations then you'll need to create a custom data source or your own menu if you want the ability to jump between variations.
8 - Variation Label Menu Does Not Preserve Querystring Values
I consider this to be a minor issue as well. If you have a page which, for example, uses a page field filter which takes in values from the querystring and you then use the variation label menu to jump between variations those querystring values will not be preserved. The reason I consider this minor is because I personally consider this menu to be nothing but a waste. If you're asked to use it you really should ask yourself why? Is it for the end users so that they can read the same content in different languages (who does that?)? Or is it for your testers so that they can jump between the various variations easily? I think you'll find that this is a feature that, under most circumstances, will never be used by your end users - so why deploy it?
9 - Long Running Propagation Jobs with Large Sites
This is another one that I don't have personal experience with but Jeremy Jameson explains the problem real well in his previously mentioned post "Dumping MOSS 2007 Variations".
10 - Navigation Structure Doesn't Propagate Between Variations
This isn't actually a bug - it's by design and I completely understand the reasoning behind it. You may have very different navigation needs between different languages and there's no easy way for the Microsoft to anticipate all needs. However, most will want the structure of their global and current navigation to match across all variations. This just another one of those things that you need to anticipate and plan for as you plan out your multilingual sites. One thing that I've used to help keep things in sync is my "gl-enumnavigation" command to export the navigation from the source variation and then let the translators translate the resultant XML and then use my "gl-setnavigationnodes" command to set the navigation for the target variations (at least for an initial site propagation - after that I've just made the changes manually).
11 - Root default.aspx and Other Root Pages Are Still Available
One thing I found that needs some clarification in the Building Multilingual Solutions Using 2007 SharePoint Products and Technologies white paper is its description of the VariationRoot.aspx file:
"After a variation hierarchy is created, the Variations feature replaces the default page of the variation’s home site with a special page called VariationRoot.aspx"
12 - Custom List Definitions Should be Localized
If you're creating custom list definitions then you should plan on making sure those list definitions are localized - set display names, choice values, query parameters, etc., to resource strings. One thing to be aware of is that if you use Solution Generator (part of the Visual Studio Extensions for WSS) to reverse engineer your lists you'll have to set all the fields to use localized strings which could add some time to your development/QA cycle.
13 - Localized Choice Columns Fail on Propagation
If you use any localized list definitions, as mentioned above, be aware that a side effect is that choice column values will be stored using the actual values, not the resource strings. Sjoert Ebben discusses this issue in his post "5 reasons why you should not use variations". Sjoert recommends a couple of solutions, one of which is to create a custom field type which will store the resource string and not the actual value.
14 - Only Pages in the Pages Library will be Propagated to Variations
To some this may seem like a pretty obvious point but I found that many I spoke to about using variations initially were rather surprised that they couldn't enable this functionality for any list. In order to get list values (such as announcements and such) propagated between variations I enabled moderation on the desired lists and created a custom event receiver which, upon approval, copies the list item to the lists with the same name in all variations. You could also use a custom workflow to do the same - it really just depends on how complex your needs are. You'll find that handling modifications to data will require careful consideration. I'm hoping to provide a sample of my event receiver in a follow-up post so be on the lookout for that.
15 - Page Field Values Are Overwritten Upon Propagation
So here's the dilemma - best practice with publishing pages is to use page fields for all content because web part changes are not stored with the history of the page - unfortunately though, when you are using a page field and you approve your page in the source variation the page will propagate to all target variations and all page field values will be overwritten. One solution to this is to add a custom page field where the editor can indicate whether the content change is a minor modification or a new version and then create a custom workflow which will delete the draft in the variation if the page is a minor modification. Some solutions will decide to simply put all content in web parts and just turn off the web part propagation - I still don't recommend this as you will not be able to roll back to a previous version of the page (again, web part changes are stored separately from page data and cannot be rolled back!).
Variations can be a very cool feature that, for simple sites, could give you exactly what you need to get you up and running with a multilingual site. The key with variations, as with just about any SharePoint feature, is careful planning. Make sure you give yourself plenty of time to prototype your solution and, most importantly, get your translators involved early on as you will find that many things will work just fine when everything is in English but as soon as you start translating your content and resource files you will encounter issues. Sometimes these issues can be addressed with simple planning and change control policies but in most cases you'll be looking at custom code and/or workflows and/or site and/or list definitions.
So, if you're going to be working with variations make sure you've got lots of coffee, plenty of headache drugs, a good punching bag, and lots of time to switch to a backup plan
When I did the gradual upgrade I found that several sites had the welcome page set to UpgLandingPgRedir.aspx when it should have been set to default.aspx. I didn't have time to figure out why it was doing this but the fix was easy enough - I just had to change the welcome page back. To do this I created a new command: gl-sitewelcomepage. The code to set this is really simple - basically just get a PublishingWeb instance and set the DefaultPage property to a valid SPFile object:
The syntax of the command can be seen below:
C:\>stsadm -help gl-sitewelcomepage stsadm -o gl-sitewelcomepage Sets the page to be used as the welcome page for the site. Parameters: -site <url of the site to update> -welcomepage <full url to the page to use as the welcome page>Here's an example of how to set the welcome page for a site:
After I had created the gl-sitewelcomepage command I needed a quick way to find all the pages that were pointing to the wrong place. To do this I hacked out this command which essentially just loops through a farm, web application, site collection or web and displays what the current welcome page is set to for each web. The command is called gl-enumwelcomepages. Like the gl-sitewelcomepage command this command is really simple - most of the code is focused on looping through the various objects to get to the PublishingWeb object:
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumwelcomepages stsadm -o gl-enumwelcomepages Lists all the welcome page(s) for a farm, web application, site collection, or web. Parameters: -url <url to search> -scope <Farm | WebApplication | Site | Web>Here's an example of how to list all the welcome pages in a web application:
I wasn't planning on creating this command but then I ran my gl-replacefieldvalues command and forgot to pass the "-publish" switch in so now I was stuck with all these list items that needed to be published and/or approved. When this happened I thought, no problem - Andrew Connell has a PublishAllItems command so I'll just download his stuff and use that rather than create my own. Unfortunately however, Andrew's command didn't take all scenarios into account and thus I was left with the task of creating a new command to do what Andrew's attempted.
For those that are using Andrew's command without issue I decided to name mine "gl-publishitems" so as to not step on his command thus allowing both to be installed. The main problem I had with Andrew's command was that it didn't take into account items that needed approval but which did not have an SPFile object associated with it. There were also issues where he was calling SPListItem.Update() after a checkin which would throw an error because the update call requires the file to be checked out.
Andrew's code also didn't take into account workflows that would need to be canceled as a result of approving an item. And finally his code didn't allow all the scoping options that I needed (Farm, Web Application, Site Collection, Web, or List). The large bulk of the code is just a series of methods with different loops in them to handle the various scoping capabilities (similar to what I did with gl-replacefieldvalues).
The core code itself is considering all the cases in which an item may need to be either checked in, published, and/or approved. I've also added the ability to dump all changes to a log file as well as to run the command in a "test" mode where it will show you what it would publish but not actually make any changes - I'd strongly recommend you use this first to verify all the changes that will be made. The core code is shown below:The syntax of the command I created can be seen below:
C:\>stsadm -help gl-publishitems stsadm -o gl-publishitems Publishes all items at a given scope. Use -test to verify what will be published before executing. Parameters: [-url <url to publish>] -scope <Farm | WebApplication | Site | Web | List> [-quiet] [-test] [-logfile <log file>]
Here's an example of how to publish all items in a given site collection:
stsadm -o gl-publishitems -url "http://intranet/hr" -scope site -logfile "c:\publish.log"
As part of the upgrade I needed to be able to fix some web parts that did not migrate correctly (either during the upgrade itself or as a result of moving a web). Before I started messing around with the web parts though I wanted to be able to see what I was dealing with. So I decided to create this simple command called gl-enumpagewebparts that would enable me to list out in XML all the web parts that are on a given page (open or closed).
One thing that I found that was very interesting was that the web part manager export method treats V2 and V3 web parts very differently. But perhaps the biggest annoyance I found was that I couldn't get the web part zone from the web part instance itself - I had to use the web part manager (SPLimitedWebPartManager) to get it (took me longer than I'd like to admit to figure that out). This command is really quite simple - it takes in an url to a web part page, loads up an SPLimitedWebPartManager (for both the shared and personal views) and then loops through the WebParts collection outputting the results as XML.
I created three separate methods to get the XML details - one is verbose and essentially just uses the built in Export() method to get the XML (you can get these results via the -verbose parameter), another is a bit simpler and is constructed by hand (this is the default) and a third is actually for use by another command that I created which I'll be documenting soon. The core code is shown below:
The syntax of the command I created can be seen below.
C:\>stsadm -help gl-enumpagewebparts stsadm -o gl-enumpagewebparts Lists all the web parts that have been added to the specified page. Parameters: -url <web part page URL> [-verbose]
Here’s an example of how to list all the web parts on a given page and dump to a text file:
stsadm -o gl-enumpagewebparts -url "http://intranet/hr/pages/default.aspx" -verbose > webparts.xml
After running a test upgrade I discovered that one page particular was showing up as un-ghosted (or customized) despite my setting the option to reset all pages to the site definition when I ran the upgrade. I attempted to use the browser to reset the page (in this case http://intranet/sitedirectory/lists/sites/summary.aspx) but that had no affect.
I decided that I needed to get more information about how the un-ghosting process works. The first thing I needed to do was see if there were any other pages with the same problem. To do this I created a command called gl-enumunghostedfiles. I know there are versions of the same command out there already but I found I needed something a bit more capable so that I could search an entire site collection and not just a single web.
After creating this command (detailed below) I was surprised to see that this summary.aspx page was not showing up as un-ghosted at all. When looking for an un-ghosted file you typically just check the CustomizedPageStatus property of an SPFile object. If this returns back as Customized (so much better than saying "un-ghosted" - not sure why this term came up and why I'm proliferating it ) then the page is un-ghosted. If you look internally at how this property is evaluated you'll see that the code is checking for the presense of a property in the Properties collection called "vti_setuppath" - if this property is not null or empty then it checks for another property called "vti_hasdefaultcontent" and if it either doesn't find this or it's set to false then it returns back a value of Customized. You can see this in the code below:
What I found when I looked closer at this page that I knew (based on the shear appearance of the page) was un-ghosted was that the "vti_hasdefaultcontent" property was set to "true" and the "vti_setuppath" property was set to the old 2003 template path. I spent many hours trying to figure out how to re-ghost this page and ended up ultimately unsuccessful. If anyone has any thoughts on this I'd love to hear them (I've tried updating the SetupPath and SetupPathVerion fields in the AllDocs table to point the file to the new template but that just resulted in an unknown error when loading the page - changing the "vti_hasdefaultcontent" property manually also didn't work as the value would not persist and I couldn't find where it's stored in the DB and changing this value in memory would allow the SPRequest.RevertContentStreams() method to be called but that would just throw a file not found exception as it is unable to locate the template file despite copying the file to various suspect locations).
As a result of my efforts though I do have a fairly robust command to re-ghost pages which does seem to work with another issue I encountered. I found that when I imported a list from another site the pages (views) for the list were showing up as un-ghosted but when I tried to use the RevertContentStream method of the SPFile object it had no affect.
After messing around with it for a while I discovered that if I called the internal SPRequest.RevertContentStreams() method directly and did not follow that call up with the call to SPRequest.UpdateFileOrFolderProperties() as the SPFile.RevertContentStream() method does then I can get the file to successfully be re-ghosted. I've got an email out to Microsoft to see if they can explain why this is so but in the mean-time it seems to work. The two commands that I created are detailed further below.
The code for this command is pretty simple - I've basically just got two recursive methods, one for the web sites and another for folders. I allowed a parameter to be passed in to determine whether the code should recurse sub webs or not. The code is shown below:
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumunghostedfiles stsadm -o gl-enumunghostedfiles Returns a list of all unghosted (customized) files for a web. Parameters: -url <web site url> [-recursesubwebs]
The following table summarizes the command and its various parameters:
|Command Name||Availability||Build Date|
|gl-enumunghostedfiles||WSS v3, MOSS 2007||Released: 9/13/2007
|Parameter Name||Short Form||Required||Description||Example Usage|
|url||Yes||URL to analyze.||-url http://intranet/|
|recursesubwebs||recurse||No||If not specified then only the single web will be considered. To recurse the web and all it’s sub-webs pass in this parameter.||-recursesubwebs
Here’s an example of how to return the un-ghosted files for a root site collection and all sub-webs:
stsadm –o gl-enumunghostedfiles -url "http://intranet/" -recursesubwebs
You can see a sample of what running the above command will produce - your results will most likely be very different:
C:\>stsadm –o gl-enumunghostedfiles -url "http://intranet/" -recursesubwebs The following files are unghosted: /News/Pages/Default.aspx /Reports/Pages/default.aspx /SearchCenter/Pages/people.aspx /SearchCenter/Pages/default.aspx /SearchCenter/Pages/peopleresults.aspx /SearchCenter/Pages/results.aspx /SearchCenter/Pages/advanced.aspx /SiteDirectory/Pages/category.aspx /SiteDirectory/Pages/sitemap.aspx /Pages/Default.aspx /FormServerTemplates/Forms/InfoPath Form Template/template.doc /Variation Labels/NewForm.aspx /Variation Labels/EditForm.aspx /Variation Labels/AllItems.aspx /Variation Labels/DispForm.aspx /_catalogs/masterpage/VariationRootPageLayout.aspx /_catalogs/wp/siteFramer.dwp /_catalogs/wp/IViewWebPart.dwp /_catalogs/wp/IndicatorWebPart.dwp /_catalogs/wp/BusinessDataFilter.dwp /_catalogs/wp/KpiListWebPart.dwp /_catalogs/wp/SummaryLink.webpart /_catalogs/wp/ContentQuery.webpart /_catalogs/wp/ThisWeekInPictures.DWP /_catalogs/wp/SearchBestBets.webpart /_catalogs/wp/CategoryWebPart.webpart /_catalogs/wp/QueryStringFilter.webpart /_catalogs/wp/SpListFilter.dwp /_catalogs/wp/WSRPConsumerWebPart.dwp /_catalogs/wp/searchpaging.dwp /_catalogs/wp/contactwp.dwp /_catalogs/wp/searchstats.dwp /_catalogs/wp/UserContextFilter.webpart /_catalogs/wp/owacontacts.dwp /_catalogs/wp/SearchCoreResults.webpart /_catalogs/wp/BusinessDataActionsWebPart.dwp /_catalogs/wp/owainbox.dwp /_catalogs/wp/FilterActions.dwp /_catalogs/wp/AdvancedSearchBox.dwp /_catalogs/wp/BusinessDataAssociationWebPart.webpart /_catalogs/wp/RssViewer.webpart /_catalogs/wp/owatasks.dwp /_catalogs/wp/SearchHighConfidence.webpart /_catalogs/wp/PeopleSearchBox.dwp /_catalogs/wp/SearchActionLinks.webpart /_catalogs/wp/PageContextFilter.webpart /_catalogs/wp/owacalendar.dwp /_catalogs/wp/AuthoredListFilter.webpart /_catalogs/wp/searchsummary.dwp /_catalogs/wp/CategoryResultsWebPart.webpart /_catalogs/wp/owa.dwp /_catalogs/wp/OlapFilter.dwp /_catalogs/wp/BusinessDataDetailsWebPart.webpart /_catalogs/wp/TableOfContents.webpart /_catalogs/wp/BusinessDataListWebPart.webpart /_catalogs/wp/TasksAndTools.webpart /_catalogs/wp/Microsoft.Office.Excel.WebUI.dwp /_catalogs/wp/DateFilter.dwp /_catalogs/wp/TextFilter.dwp /_catalogs/wp/SearchBox.dwp /_catalogs/wp/BusinessDataItemBuilder.dwp /_catalogs/wp/TopSitesWebPart.webpart /_catalogs/wp/PeopleSearchCoreResults.webpart
This command takes what should be a very simple call to SPFile.RevertContentStream() and attempts to handle those odd cases that I outlined above. Therefore the code is a bit of a mess and frankly nothing I'm proud of (mainly because I'm pissed I wasn't able to solve the problem). The code, which uses some reflection in order to utilize some internal objects, is shown below (sorry about the poor formatting - this blog template is less than ideal for code samples):
The syntax of the command can be seen below:
C:\>stsadm -help gl-reghostfile stsadm -o gl-reghostfile Reghosts a file (use force to override CustomizedPageStatus check). Parameters: -url <url to analyze> [-force] [-scope <WebApplication | Site | Web | List | File>] [-recursewebs (applies to Web scope only)] [-haltonerror]
The following table summarizes the command and its various parameters:
|Command Name||Availability||Build Date|
|gl-reghostfile||WSS v3, MOSS 2007||Released: 9/13/2007
|Parameter Name||Short Form||Required||Description||Example Usage|
|url||Yes||URL to analyze. If scope is “File” then URL must point to a valid file within a Web. If scope is “List” then URL must point to a valid List within a Web.||-url "http://intranet/sitedirectory/lists/sites/summary.aspx"|
|force||No||Attempts to force the reghosting of file(s) using internal API method calls (via reflection).||-force|
|scope||No (defaults to File)||The scope to look at when reghosting files. Valid values are “WebApplication”, “Site”, “Web”, “List”, or “File”.||-scope file|
|recursewebs||recurse||No||Applies to “Web” scope only. If a scope of “Web” is not specified then only the single web will be considered. To recurse the web and all it’s sub-webs pass in this parameter.||-recursewebs
|haltonerror||halt||No||If an error occurs then stop processing other files within the specified scope.||-haltonerror
Here’s an example of how to force the reghosting of a file:
stsadm –o gl-reghostfile -url "http://intranet/sitedirectory/lists/sites/summary.aspx" –scope file -force
If I'm able to get any answers to the issues that remain unsolved for me I'll be sure to post them here (especially if I'm able to fix the issues).