Wednesday, October 14, 2009

Copy SPListItem.Version (SPListItemVersion) Part 3

Background and Considerations

A while back I wrote a blog that discussed the issues with copying SPListItems from one list to another. However I recently needed to create a utility and thought my old blog would solve the problem – I am unhappy to say it did not. It definitely unlocks the issue with copying SPListItems with versions however I just found a couple shortcomings of what I wrote. Let’s try again.

Here are some considerations I had to understand before starting to build this.

  • The SPListItem CopyTo() and CopyFrom() methods do not work after doing some research with Reflector.
  • You will need to need to loop over the versions backwards and add the versions of the list items in the destination list.
  • Moving documents is different than moving list items.
  • Recursively looping over items within a SPList or SPDocumentLibrary is not straight forward. You usually want to maintain the folder structure when moving items from one list to another. You cannot simply loop over all items in the SPList nor does a SPFolder object have a collect of items within it. Only easy way of achieving this is to use a CAML query to get all the items for a specific folder.
  • If you need to preserve the Created and Modified time stamps on the version items, you need to set the times correctly because they are stored as GMT in the SharePoint database.
  • If you want to move items cleanly into a new or existing list, I recommend writing code that will first remove all the items from the destination list, then remove all the content types destination list and finally add the needed content types back into the destination list. There are numbers of reasons why to do this. It is possible to write a routine to reconcile the content types from the source list to the destination list however that can be come complicated. The important thing to know is that if a column is missing in the destination list, the movement of the SPListItem or SPDocument item will fail. The code I have written is not dependant on the content type ID which is a good thing. This is because if the content types are defined within the SharePoint UI a unique GUID is created for that content type. If you are moving items across SharePoint servers, you cannot be guaranteed that the Content Type ID will be the same, but the column names and types should be the same.

Create Copy Folders Structure

I created a method called MoveFolderItems which will recreate the folder structure in a new library. All you need to do initiate it is something like the following.

MoveFolderItems(sourceList, sourceList.RootFolder, destList, destList.RootFolder);

As you can see in this method, it first gets all the items for a specified folder. Then it checks to see if the item is another folder or not. If so, it will create a new folder, otherwise it will move over the item depending.

        private static void MoveFolderItems(SPList sourceList, SPFolder sourceFolder, SPList destList, SPFolder destFolder)
{
//Query for items in the source folder
SPQuery query = new SPQuery();
query.Folder = sourceFolder;
SPListItemCollection queryResults = sourceList.GetItems(query);

foreach (SPListItem existingItem in queryResults)
{
if (existingItem.FileSystemObjectType == SPFileSystemObjectType.Folder)
{
Console.WriteLine(existingItem.Name);

//Create new folder item
SPListItem newSubFolderItem = newSubFolderItem = destList.Items.Add(destFolder.ServerRelativeUrl,
SPFileSystemObjectType.Folder, null);

//Set folder fields
foreach (SPField sourceField in existingItem.Fields)
{
if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
newSubFolderItem[sourceField.Title] = existingItem[sourceField.Title];
}
}

//Save the new folder
newSubFolderItem.Update();

if (newSubFolderItem.ModerationInformation != null)
{
//Update Folder Status
newSubFolderItem.ModerationInformation.Status = SPModerationStatusType.Approved;
newSubFolderItem.Update();
}

//Get the source folder and the new folder created
SPFolder nextFolder = sourceList.ParentWeb.GetFolder(existingItem.UniqueId);
SPFolder newSubFolder = destList.ParentWeb.GetFolder(newSubFolderItem.UniqueId);

//Recursive call
MoveFolderItems(sourceList, nextFolder,
destList, newSubFolder);
}
else
{
//Move the item
Console.WriteLine(existingItem.Name);

if (sourceList.BaseTemplate == SPListTemplateType.DocumentLibrary)
{
MoveDocumentItem(existingItem, destFolder);
}
else {
MoveItem(existingItem, destFolder);
}
}
}
}

Move SPListItem

Here is the code for the SPList item with its history. First we create the list item. Then we loop over the versions backwards and add each version into the destination list.

            private static void MoveItem(SPListItem sourceItem, SPFolder destinationFolder) {
//Create a new item
SPListItem newItem;

if (destinationFolder.Item != null)
{
newItem = destinationFolder.Item.ListItems.Add(
destinationFolder.ServerRelativeUrl,
sourceItem.FileSystemObjectType);
}
else {
SPList destinationList = destinationFolder.ParentWeb.Lists[destinationFolder.ParentListId];
newItem = destinationList.Items.Add(
destinationFolder.ServerRelativeUrl,
sourceItem.FileSystemObjectType);
}

//loop over the soureitem, restore it
for (int i = sourceItem.Versions.Count - 1; i >= 0; i--) {
//set the values into the new item
foreach (SPField sourceField in sourceItem.Fields) {
SPListItemVersion version = sourceItem.Versions[i];

if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
newItem[sourceField.Title] = version[sourceField.Title];
}
else if (sourceField.Title == "Created"
sourceField.Title == "Modified")
{
DateTime date = Convert.ToDateTime(version[sourceField.Title]);
newItem[sourceField.Title] = sourceItem.Web.RegionalSettings.TimeZone.UTCToLocalTime(date);
}
else if (sourceField.Title == "Created By"
sourceField.Title == "Modified By")
{
newItem[sourceField.Title] = version[sourceField.Title];
}
}

//update the new item with version data
newItem.Update();
}

//Get the new item again
SPList list = destinationFolder.ParentWeb.Lists[destinationFolder.ParentListId];
newItem = list.GetItemByUniqueId(newItem.UniqueId);
newItem["Title"] = sourceItem["Title"];
newItem.SystemUpdate(false);

if (sourceItem.Attachments.Count > 0)
{
//now get the attachments, they are not versioned
foreach (string attachmentName in sourceItem.Attachments)
{
SPFile file = sourceItem.ParentList.ParentWeb.GetFile(
sourceItem.Attachments.UrlPrefix + attachmentName);

newItem.Attachments.Add(attachmentName, file.OpenBinary());
}

newItem.Update();
}
}

Move Document

As I mentioned earlier, moving a document is a little bit different. Here is the code that will copy a document, metadata and versions over to a new library.

       private static void MoveDocumentItem(SPListItem sourceItem, SPFolder destinationFolder)
{
//loop over the soureitem, restore it
for (int i = sourceItem.Versions.Count - 1; i >= 0; i--)
{
Hashtable htProperties = new Hashtable();

//set the values into the new item
foreach (SPField sourceField in sourceItem.Fields)
{
SPListItemVersion version = sourceItem.Versions[i];

if (version[sourceField.Title] != null)
{
if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
htProperties[sourceField.Title] = Convert.ToString(version[sourceField.Title]);
}
else if (sourceField.Title == "Created"
sourceField.Title == "Modified")
{
DateTime date = Convert.ToDateTime(version[sourceField.Title]);
htProperties[sourceField.Title] = sourceItem.Web.RegionalSettings.TimeZone.UTCToLocalTime(date);
}
else if (sourceField.Title == "Created By"
sourceField.Title == "Modified By")
{
htProperties[sourceField.Title] = Convert.ToString(version[sourceField.Title]);
}
}
}

//Get the version of the document
byte[] document;
if (i == 0)
{
document = sourceItem.File.OpenBinary();
}
else
{
document = sourceItem.File.Versions.GetVersionFromLabel(
sourceItem.Versions[i].VersionLabel).OpenBinary();
}

//Create the new item. Overwriting it will treat is as a
//new item.
SPFile newFile = destinationFolder.Files.Add(
destinationFolder.Url + "/" + sourceItem.File.Name,
document,
htProperties,
true);

newFile.Item["Created"] = htProperties["Created"];
newFile.Item["Modified"] = htProperties["Modified"];
newFile.Item.UpdateOverwriteVersion();
}

}

6 comments:

Steve Phillips said...

This is an excellent post, thanks very much.

Jason Apergis said...

Yeah and it took me three times over a two year period to get it absolutely correct. Doh!

Jason

Broschat said...

Jason, I came across your column while researching the difference between files in list items and files in libraries. Although this post does not address that, might I hear a word or two on the subject?

We have lists that contain n number of file attachments, and we want to copy a selected attachment into an existing library.

Thanks!

Jason Apergis said...

You could take this code and modify it so that you are looking over items moving the attachments directly into a document library. Hope it helps as a good starting place.

Paul said...

This was a very helpful post, thanks. I am having one small problem with the MoveItem method that I hope you might be able to assist me with.

The list items that I am trying to move (to an archive list on the same site) have some fields with a field type of 'Computed', which cause the following error to be thrown on the call to newItem.Update();

Invalid data has been used to update the list item. The field you are trying to update may be read only.

As this list will be an archive of bugs that having been submitted in sharepoint it is important that we retain these field values as set in the source list. Do you have any advice on how to go about achieving this?

Jason Apergis said...

Paul,

I hate to say - I wrote this a long time ago and have touched for some time. This error seems to be a common developer error when I searched on it.

Here is something that may provide you a clue - http://blogs.msdn.com/b/sridhara/archive/2007/04/01/invalid-data-has-been-used-to-update-the-list-item-the-field-you-are-trying-to-update-may-be-read-only-when-updating-begin-end-fields-in-an-event-list.aspx

Thanks,
Jason