Saturday, October 13, 2007

Copy SPListItem.Version (SPListItemVersion) Part 1

7/13/2008 - Going through and cleaning up code samples in popular posts. The old tool I used stunk. At least this is better. I apologize for the inconvenience.

10/14/2009 - I have created a new blog posting with updates as there were some issues with this original posting. Please go here.

1. Introduction

In MOSS you may have a requirement to take list items and move them to another location. A simple scenario would be that you have an item in a list and when a specific value is set you want to move that item to another location. One problem that you will run into is when you copy the item from one list to another is that you will want to ensure that the version history of the item is moved with the item. This article will discuss how you should go about fixing this problem; as well as a quick blurb on how this would be implemented in K2.net BlackPearl.

2. Available Methods and Approach

There are several available methods apart of SPListItem that you may be tempted to use. Specifically there are the CopyTo() and CopyFrom() methods. After some research online and using with Reflector to dive into the System.Microsoft.SharePoint neither of these methods can be used because the file name must be passed in has part of the URL parameters for the methods. With a standard list item object the file will be null making these methods useless unless you are working with a document object. As well, these methods do not support the copying of the version history.

The only viable solution is to loop over all of the values in SPListItemVersionCollection and set them into a new SPListItem. The Restore() method of SPListItemVersion cannot be used as it will take a specific version and restore it as a new version within the current SPListItem object. While looping over each SPListItemVersion the call Update() of the new SPListItem object will be called to rebuild the version history into the new object.

3. Solution Outline

An outline of the solution is as follows:

  • Create an Event Handler for the ItemUpdated event.
  • Create a new SPListItem object that will be the item that will be archived.
  • Loop over the Versions property of the source SPListItem. When looping over the item you must loop going backwards as the last item in the SPListItemVersionCollection is the first version saved.
  • Set each field from SPListItemVersion and call update on the new SPListItem (if there are 50 versions, then update will be called 50 times).
    • Note that index zero is always the latest version that a user would edit through SharePoint.
  • Then move the file attachments to the new SPListItem.
    • Note that attached documents to a SPListItem are not versioned. If a document needs to be versioned, it is best to put that document in a document library that is versioned controlled and then link the document to the SPListItem.
  • Deploy the Event Handler as a Feature.

4. Code

4.1 Feature

This article will not discuss the particulars of creating Features but shows them here for reference. NOTE – Change the GUID

<FeatureId="FB7D9BC1-95EA-43fc-85A7-AE6B0123E397"
Title="Item Archive Event"
Description="This event will archive an item when it is closed"
Version="1.0.0.0"
Scope="Web"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifestLocation="Elements.xml" />
</ElementManifests>
</Feature>

4.2 Elements of Feature

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<ReceiversListTemplateId="100">
<Receiver>
<Name>ArchiveItemEventHandler</Name>
<Type>ItemUpdated</Type>
<SequenceNumber>1000</SequenceNumber>
<Assembly>
Events, Version=1.0.0.0,culture=neutral,
PublicKeyToken=419549f693d50b9c
</Assembly>
<Class>Events.ArchiveItemEventHandler</Class>
</Receiver>
</Receivers>
</Elements>

4.3 EventHandler Class

public classArchiveItemEventHandler : SPItemEventReceiver {

public overridevoid ItemUpdated(SPItemEventProperties properties) {
ArchiveItem(properties);
base.ItemUpdated(properties);
}
}

4.4 Create Folder

This solution is based on moving items to an archive folder which is dynamically generated every month. The code below creates a new folder in SharePoint based on the current month.


private SPListItem GetArchiveFolder(SPList list) {
//Get the folder for the current month
string folderName = System.DateTime.Now.Month.ToString() +
System.DateTime.Now.Year.ToString();

SPListItem destinationFolder = null;

foreach (SPListItem folder in list.Folders) {
if (folder.Name == folderName) {
destinationFolder = folder;
break;
}
}

if (destinationFolder == null) {

//Create new folder
destinationFolder = list.Items.Add(
list.RootFolder.ServerRelativeUrl,
SPFileSystemObjectType.Folder, null);

if (destinationFolder != null) {
destinationFolder["Name"] =
"Archive Closed Records (" + folderName + ")";
destinationFolder.Update();
}
}

return destinationFolder;
}

4.5 Copy the Versions into a new Item

This code is the meat of the solution. Couple of more notes:

  • A new archive item SPListItem is created in the archive folder location.
  • The DisableEventFiring() and EnableEventFiring() methods wrap the code. This is needed because the there will be an Update() call for every version.
  • Note again that the looping of the versions starts with the last item in the collection and works backwards.
  • There is code to accommodate Created, Created By, Modified and Modified By. They are read only fields. If they are not set the values will be lost and set with the system account and the time in which the code executed.
  • After all of the versions have been updated into the new item, the attachments are moved into the new item.
  • Finally the source item is deleted.

That is essentially it. The biggest thing to walk away with is that the properties.ListItem.Version[x][y] is two dimensional and that is what gets you access to all of the field values in the version collection.


private void MoveItem(SPListItem sourceItem,
SPListItem destinationFolder,
string newItemLocation) {

//Create a new item
SPListItem archiveItem = destinationFolder.ListItems.Add(
newItemLocation,
sourceItem.FileSystemObjectType);

DisableEventFiring();

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

if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments)) {
archiveItem[sourceField.Title] = version[sourceField.Title];
}
else if (sourceField.Title == "Created"
sourceField.Title == "Created By"
sourceField.Title == "Modified"
sourceField.Title == "Modified By") {

archiveItem[sourceField.Title] = version[sourceField.Title];
}
}

//update the archive item and
//loop over the the next version
archiveItem.Update();
}

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

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

archiveItem.Update();

EnableEventFiring();

//Now delete the current item.
sourceItem.Delete();
}

5. Referencs

10 comments:

Robin Meuré said...

Thnx for the reference ;)

LeNational said...

Thanks for the exemple. Just a point about the DateTime. You need to convert the DateTime to localtime because Sharepoint give you the Datetime in Greenwich format.

if (version[sourceField.Title] != null && version[sourceField.Title].GetType().Name == "DateTime")
archiveItem[sourceField.Title] = Convert.ToDateTime(version[sourceField.Title]).ToLocalTime();
else
archiveItem[sourceField.Title] = version[sourceField.Title];

mlh said...

Appreciated the sample a lot, thanks!
In my case it does not work anyway, my item comes from a simple custom list, has versionning and content type.
The first update always raise an exception.
The only wy I can make it work is when I'm outside event handlers.
One comment
your treatment is occuring before the base.ItemUpdated(properties) but you just deleted the item !?

lgamiz said...

Thanks a lot for the great example.

i appreciate if somebody can help me with these two problems:
1) I became always a error with the two event methos DisableEventFiring(); and EnableEventFiring(); (right not are als comments)
2) How can i set the CheckInComments because i tried to set it with if(sourceField.Title == "_CheckInComments"){
archiveItem[sourceField.Title] = version[sourceField.Title];
}
but if i use the method Update archiveItem.Update(); a compilation error is displayed "the checkInComments is readonly"

With the method archiveItem.CheckIn(string Comments) can i do that, but i must use it with the method Checkout which creates a new Version.

Jason Apergis said...

mlh - all I can say is this is the code in production and it works.


lgamiz - if I understand you correctly you are getting a runtime error when trying to set the check in comments? the other fields I am setting like Created, Modified, etc. are read only fields too.

Todd said...

What is the archiveItem object?

public classArchiveItemEventHandler : SPItemEventReceiver { public overridevoid ItemUpdated(SPItemEventProperties properties) { ArchiveItem(properties); base.ItemUpdated(properties); }}


There is no archiveItem declared?

Jason Apergis said...

Hi Todd,

The archiveItem object is created in the MoveItem() method.

As for the classArchiveItemEventHandler class I understand there is a little disconnect between going from the ArchiveItem() method to the MoveItem() method. However the code is accesible to me anymore and that is why I blogged it :-) Regardless, the real point of the blog it so show the MoveItem() method and how to go through the version history and move it into a new item in the correct order...

Jason

Majid Baig said...

to stop event firing, what if we use SystemUpdate() rather Update() for an item

Majid

Jason Apergis said...

From what I understand - calling SystemUpdate will not prevent events from being raised (http://msdn.microsoft.com/en-us/library/ms461526.aspx).

I think in the case that I was developing this - I had to call DisableEventFiring and EnableEventFiring because I had other event handlers on the list. Trying to recall this from almost two years ago, you see I am calling a method called MoveItem from the ItemUpdated method. Let's say I also had an OnItemAdded event handler on the list, in this case I would want to block that event handler from being raised.

Jason Apergis said...

I made some important updates associated to this blog here - http://www.k2distillery.com/2009/10/copy-splistitemversion.html