Propagating Document Library Items In SharePoint Variations (in the same way as Pages)

The Problem

 

When my users see Sharepoint Variations in action, they’re usually pretty impressed. Alas, when they then realise that only their publishing pages are being propagated out, and not their Documents they tend to deflate pretty quickly. “Why aren’t the contents of Document Libraries published out in the same way?” they cry!

 

Instead of just offering tea and sympathy, I thought it’d be rather cool to develop something to add Document Library propagation to Variations sites. It’s not wildly difficult to do as it turns out.

 

So how do Variations get propagated out to Target Variations? Anyone digging into the workings of Variations in MOSS 2007 will have come across The Relationships List. If you’re not familiar with it, visit http://YourSite/Relationships%20List/AllItems.aspx  and you’ll see that it maintains a list of the relationships between the Source Variation Sites and Pages, and the equivalents in the target variations. (Gary Lapointe goes into a little more detail about the Relationships List here; http://stsadm.blogspot.com/2008/04/fun-with-variations.html). Combine this meta-data with some calls to the Content Deployment API and in a very brief nutshell you have it.

 

It would be wonderful to propagate my Document Library items in the same way, but I’m afraid I won’t be offering that in this posting. I have no desire to start tinkering with Relationships List to add my own meta data, or creating a Relationships list of my own. Things can be much simpler than that as detailed below.

 

So, what we will look at is a method of duplicating documents from a Document Library in the Source Variation to an equivalent Document Library in the Target Variation. The code looks for an equivalent location to place the items in the Target Variation. Unlike variations perhaps, if an equivalent location (Document Library in this case) isn't at the target we don't create one - though you code extend this code easily to do that.

 

This isn’t as sophisticated as how Variations do their stuff, but this capability will significantly sweeten the Variations pill for your clients.

 

 

Code Explanation

 

This code will;

 

Obtain a list of Variations, and identify the Source Variation.

Take a copy of the Source Item in the Source Document Library.

Place that Item in the equivalent Document Library in each of the Target Variations.

Manage versioning of the item in the same way it’s done for Variation Pages – create a new minor version.

 

If there is no equivalent library the item won’t be propagated.

 

There's then a brief commentary on how I deployed this, which determines when and how the code is run. (Use of Features, Event Receivers, and CustomActions).

 

Just one word of warning; the code I’m posting here isn’t the same code I put into Production. That code was much more defensively coded but wasn’t suitable to clearly illustrate the concepts involved. You will have to re-work the following code for a Production environment. But hey, you’re professionals so you know that already right?

 

Looking at the method that retrieves Variation Labels;

 

 

We pass in a url just to get a reference to the site collection, and a Stack will hold our variation labels because it’s simple and light.

 

 

private string GetVariationLabels(string startingUrl, out Stack stack)

 

 

Get a SPWeb instance that we can work with.

 

 

using (SPSite site = new SPSite(startingUrl))

        {

            SPList variationLabels = null;

 

            using (SPWeb rootWeb = site.RootWeb)

            {

 

 

Get an SPList of the Variation labels like so;

 

 

string _VarLabelsListId = rootWeb.AllProperties["_VarLabelsListId"].ToString();

 

//Get that list

variationLabels = rootWeb.Lists[new Guid(_VarLabelsListId)];

 

 

Push the labels onto our stack. If a given label is the source variation, save that as a return value for the method.

 

 

for (int i = 0; i < variationLabels.ItemCount; i++)

{

     SPListItem item = variationLabels.Items[i];

     bool altBool = true;

      //If it's the Source Variation don't add it to the stack

      if (bool.Parse(item["Is Source"].ToString()))

      {

 _SourceVariationLabel = item[SPBuiltInFieldId.Title].ToString();

      }

      else

      {

        stack.Push(item[SPBuiltInFieldId.Title].ToString());

      }

}

 

 

So, we have a stack of Variation labels, what shall we do now?

 

 

Method for pushing a file out to target variations;

 

 

private void DeploySingleFile(string fileUrl)

{

string _SourceVariationLabel;

Stack variationStack = null;

_SourceVariationLabel = GetVariationLabels(fileUrl, out variationStack);

 

 

Basic setup;

 

 

using (SPSite sourceSite = new SPSite(fileUrl))

{

        using (SPWeb sourceWeb = sourceSite.OpenWeb())

        {

          string sourceDocLibName = string.Empty;

          string strCheckinComment = "File Automatically Migrated From Source Variation ";

 

          SPFile sourceFile = sourceWeb.GetFile(fileUrl);

 

 

This is only for Document Libraries. Other types of library can be done, but some of the method calls are slightly different and not covered in this code;

 

 

if (sourceFile.Item.ParentList.BaseType == SPBaseType.DocumentLibrary)    

          

 

 

For each of those variation labels, get a target url. This is a bit of a hack, but no more elegant solution presented itself at time of writing (there is a much better way, ping me if you want to know how). Please do post an alternative way of doing this if you know of one. You might also be surprised by the code at this point – why is the url almost the same for the document libraries? Shouldn’t they all have a url sporting various names like Documents, Dokument, Documenten etc? Well in a word… no. Default Document libraries in MOSS have an identical url (except for the variation label) in all variations regardless of their title. This is good because it allows this solution to not require any mapping records between the various document libraries. Perfect KIS. You might want to follow the same convention with any document libraries you create in your variations.

 

 

foreach (string varLabel in variationStack.ToArray())

{

//Replace variation label in URL

      string strDestURL = fileUrl.Replace("/" + _SourceVariationLabel + "/", "/" + varLabel + "/");

 

 

Now get hold of the Files Collection in that target variation.

 

 

using (SPSite targetSite = new SPSite(strDestURL))

{

using (SPWeb targetWeb = targetSite.OpenWeb())

      {

            SPFileCollection collFiles = null;

collFiles = targetWeb.GetFolder(sourceDocLibName).Files;

 

 

If we’ve found a target file collection to shoot for, prepare a copy of our source file.

 

 

if (collFiles != null)

{

targetWeb.AllowUnsafeUpdates = true;

      byte[] binFile = sourceFile.OpenBinary();

SPFile targetFile = null;

 

 

Now, the final act. Add that file to the target File Collection. If it’s already there do a check-out and use .SaveBinary to update the file. If it doesn’t exist, calling .Add on the file collection will do it. In production, implement this choice better than with a try/catch.

 

 

try

{

targetFile = collFiles[strDestURL];

      //The file already exists so we need some version handling instead of a straight .add

      if (targetFile.CheckOutStatus != SPFile.SPCheckOutStatus.None)

      {

            targetFile.UndoCheckOut();

}

      targetFile.CheckOut();

      targetFile.SaveBinary(binFile);

      targetFile.Update();

 

     targetFile.CheckIn(strCheckinComment, SPCheckinType.MinorCheckIn);

}

catch (System.Exception)

{

//No existing file item exists of this url so just add it

targetFile = collFiles.Add(strDestURL, binFile, true, strCheckinComment, false);

targetFile.Update();

}

 

 

The code is included at the end of the post.

 

 

Triggering Options

 

 

There are two major triggering options for this code depending how and when you want it to execute. If you want documents to propagate when they are published for instance, you would create an Event Receiver for the ItemUpdating method of SPItemEventReceiver. To check whether this updated item had actually been published you’d look at something like SPItemEventProperties.

 

If Moderation is turned on you can look at;

 

 

properties.ListItem.ModerationInformation.Status == SPModerationStatusType.Approved [would signify a publish].

 

Or if not you can check;

 

properties.ListItem.Level == SPFileLevel.Published

 

 

 

In my own case, the users wanted to manually trigger document propagation as well as an automatic propagation on publish. I used <CustomAction> elements in a feature to add menu items to the action menu of the file, and the action menu of the document library. The users see something like this;

 

 

 

How that was done is beyond the scope of this article. However this article will help you achieve the same result;

 

 

http://msdn.microsoft.com/en-us/library/ms473643.aspx

 

Final Thoughts

 

There's nothing very complicated about the code presented here, but the potential for what the code can achieve is an eye opener. This posting talks about moving document library items from a Source Variation to a Target Variation. However the same principle could be applied to move items between any variations, and indeed back from Target to Source. (I can think of scenarios where a user could ask for that, and I bet you can too).

 

Further, this code adds to a File Collection. If we removed the item instead, we could implement the fabled 'Cascade Delete' feature that doesn't come out of the box with Sharepoint Variations, but that clients seem to want.

 

Finally, there's only a little tweaking needed to do some of the things I mention here with Publishing Pages rather than Document Library items, though extra coding will be needed to keep the Variations meta data sweet. Have a play with the code, and let me know what you come up with.

 

[Update 20-01-2009] This article has become the most visited on the blog over recent months . If you're finding this technique and the code useful, why not leave a comment below? It would be great to get some feedback on how this code is being used, and what improvements could be made.

 

 

The Code

 

 

    private void DeploySingleFile(string fileUrl)

    {

        string _SourceVariationLabel;

        Stack variationStack = null;

 

        _SourceVariationLabel = GetVariationLabels(fileUrl, out variationStack);

 

        using (SPSite sourceSite = new SPSite(fileUrl))

        {

            using (SPWeb sourceWeb = sourceSite.OpenWeb())

            {

                string sourceDocLibName = string.Empty;

                string strCheckinComment = "File Automatically Migrated From Source Variation ";

 

                SPFile sourceFile = sourceWeb.GetFile(fileUrl);

 

                //Supports Document Libraries Only

                if (sourceFile.Item.ParentList.BaseType == SPBaseType.DocumentLibrary)

                {

                    sourceDocLibName = sourceFile.ParentFolder.Name;

 

                    foreach (string varLabel in variationStack.ToArray())

                    {

                        //Replace variation label in URL

                        string strDestURL = fileUrl.Replace("/" + _SourceVariationLabel + "/", "/" + varLabel + "/");

 

                        using (SPSite targetSite = new SPSite(strDestURL))

                        {

                            using (SPWeb targetWeb = targetSite.OpenWeb())

                            {

                                SPFileCollection collFiles = null;

 

                                collFiles = targetWeb.GetFolder(sourceDocLibName).Files;

                               

                                if (collFiles != null)

                                {

                                    targetWeb.AllowUnsafeUpdates = true;

                                    byte[] binFile = sourceFile.OpenBinary();

 

                                    SPFile targetFile = null;

                                    try

                                    {

                                        targetFile = collFiles[strDestURL];

                                        //The file already exists so we need some version handling instead of a straight .add

                                        if (targetFile.CheckOutStatus != SPFile.SPCheckOutStatus.None)

                                        {

                                            targetFile.UndoCheckOut();

                                        }

                                        targetFile.CheckOut();

                                        targetFile.SaveBinary(binFile);

                                        targetFile.Update();

 

                                        targetFile.CheckIn(strCheckinComment, SPCheckinType.MinorCheckIn);

                                    }

                                    catch (System.Exception)

                                    {

                                        //No existing file item exists of this url so just add it

                                        targetFile = collFiles.Add(strDestURL, binFile, true, strCheckinComment, false);

                                        targetFile.Update();

                                    }

                                }

                            }

                        }

                    }

                }

            }

        }

    }

 

 

    private string GetVariationLabels(string fileUrl, out Stack stack)

    {

        stack = new Stack();

 

        string _SourceVariationLabel = string.Empty;

 

        using (SPSite site = new SPSite(startingUrl))

        {

            SPList variationLabels = null;

 

            using (SPWeb rootWeb = site.RootWeb)

            {

                //Get List ID for Variation Labels

                string _VarLabelsListId = rootWeb.AllProperties["_VarLabelsListId"].ToString();

 

                //Get that list

                variationLabels = rootWeb.Lists[new Guid(_VarLabelsListId)];

 

                //Iterate through the values

                for (int i = 0; i < variationLabels.ItemCount; i++)

                {

                    SPListItem item = variationLabels.Items[i];

                    bool altBool = true;

                    //If it's the Source Variation we don't want to add it to the stack

                    if (bool.Parse(item["Is Source"].ToString()))

                    {

                        stack.Push(item[SPBuiltInFieldId.Title].ToString());

                    }

                    else

                    {

                        _SourceVariationLabel = item[SPBuiltInFieldId.Title].ToString();

                    }

                }

            }

        }

        return _SourceVariationLabel;

    }