Posts tagged ‘attachments’

October 21, 2010

Versioning attachments in a SharePoint list – an implementation

Further to the post on the 14th of October, here is a complete implementation of versioning attachments in a SharePoint list as a Visual Studio 2010 project.

Once the .wsp has been built and deployed, it can be activated at the web scope. Once this is done, on all lists an item called “List attachment versioning” appears on the List Settings page. When accessed this allows the user to switch attachment versioning on or off. This in turn will create a hidden document library and hidden fields as necessary to support the attachment versioning.

Please do try it and let me know what you think.

Download the solution here.

Update: 17/11/2011 – Error fixed where the solution only works on a root site.

Advertisements
October 14, 2010

Versioning attachments in a SharePoint list

One drawback that SharePoint lists have is that attachments are not versioned along with other changes to a list item when list versioning is enabled. This means that certain circumstances are made more complex than they need to be in grouping multiple documents that are actually related to a list item, rather than classified by the meta data within the list item.

In order to overcome this in SharePoint 2010 there are a few things we must do. We need to attach receivers to the ItemAdding, ItemUpdating, ItemAttachmentAdding and ItemAttachmentDeleting methods. These will be used to push the attachments to a document library. Since we will use a document library to store the files, we will need to provision a document library. Finally we will need a custom number field we can use to store the current version of the ListItem. We need to do this because there is no way from the properties object in a receiver method to discover whether we are involved in a rollback. Therefore we can use a custom number field which we increment in the ItemAdding and ItemUpdating methods to store which version we are at. When a rollback occurs this field would also be attempted to be rolled back. We can detect this in the ItemUpdating receiver and therefore pull back documents which were at this version.

So to start with we initially create a Site Column with a Type of “Number”, and include this in our Content Type and List Definition. In addition we can create a ListInstance from this content type and a ListInstance of a document library, where the documents will be persisted. We will also need a custom Site Column of Type “Text” to hold the version numbers the attachment is associated with. We declare this with our normal Field and ListInstance syntax, using ContentTypeBinding to bind our content types to the lists we have created.

Next we need to create the receiver, as defined above. We can do this with a class like the following:

    /// <summary>
    /// Handles the redirection of attachments to a document library
    /// </summary>
    public class RedirectAttachmentsItemEventReceiver : SPItemEventReceiver
    {
        /// <summary>
        /// Local HTTP Context cache
        /// </summary>
        private HttpContext context;

        private string targetDocumentLibrary;
        private string targetLookupField;

        /// <summary>
        /// HTTP Context is available in creation
        /// </summary>
        public RedirectAttachmentsItemEventReceiver()
            : base()
        {
            context = HttpContext.Current;
        }

        /// <summary>
        /// Parses the receiver data string into private variables
        /// </summary>
        /// <param name="receiverData"></param>
        private void ParseReceiverData(string receiverData)
        {
            this.targetDocumentLibrary = receiverData.Split(';')[0];
            this.targetLookupField = receiverData.Split(';')[1];
        }

        /// <summary>
        /// Handles the adding of an attachment to the list
        /// Adds it into the document library
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAttachmentAdding(SPItemEventProperties properties)
        {
            base.ItemAttachmentAdding(properties);
            int attachmentNumber = 0;
            if (int.TryParse(SPContext.GetContext(context).Web.Properties["attachmentNumber"], out attachmentNumber))
            {
                attachmentNumber++;
                SPContext.GetContext(context).Web.Properties["attachmentNumber"] = attachmentNumber.ToString();
                ParseReceiverData(properties.ReceiverData);
                string fileName = properties.AfterUrl.Split('/')[4];
                //Put the item into the document library
                using (SPWeb web = properties.OpenWeb())
                {
                    SPList documentLibrary = web.Lists[targetDocumentLibrary];
                    SPFolder targetFolder;
                    try
                    {
                        targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                    }
                    catch
                    {
                        targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                    }
                    SPFile document;
                    try
                    {
                        document = targetFolder.Files[fileName];
                    }
                    catch (ArgumentException)
                    {
                        document = targetFolder.Files.Add(fileName, context.Request.Files[attachmentNumber - 1].InputStream);
                    }
                    if (document.Item["VersionLabel"] != null)
                        document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString() + ";" + properties.ListItem.Versions[0].VersionLabel + ";";
                    else
                        document.Item["VersionLabel"] = ";" + properties.ListItem.Versions[0].VersionLabel + ";";
                    document.Item[targetLookupField] = new SPFieldLookupValue(properties.ListItem.ID, properties.ListItem.Title);
                    document.Item.Update();
                }
            }
        }

        /// <summary>
        /// Handles the deleting of an attachment from the list
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAttachmentDeleting(SPItemEventProperties properties)
        {
            base.ItemAttachmentDeleting(properties);
            ParseReceiverData(properties.ReceiverData);
            int nextVersion = (int)float.Parse(properties.ListItem["CustomVersion"].ToString()) + 1;
            string fileName = properties.BeforeUrl.Split('/')[4];
            //Put the item into the document library
            using (SPWeb web = properties.OpenWeb())
            {
                SPList documentLibrary = web.Lists[targetDocumentLibrary];
                SPFolder targetFolder;
                try
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                }
                catch
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                }
                SPFile document = targetFolder.Files[fileName];
                if (document.Item["VersionLabel"] != null)
                    document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString().Replace(";" + nextVersion.ToString() + ".0;", "");
                document.Item.Update();
            }
        }

        /// <summary>
        /// Set the custom version property
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAdding(SPItemEventProperties properties)
        {
            base.ItemAdding(properties);
            ParseReceiverData(properties.ReceiverData);
            properties.AfterProperties["CustomVersion"] = 1;
            SPContext.GetContext(context).Web.Properties["attachmentNumber"] = "0";
        }

        /// <summary>
        /// Update the custom version property
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            base.ItemUpdating(properties);
            SPContext.GetContext(context).Web.Properties["attachmentNumber"] = "0";
            ParseReceiverData(properties.ReceiverData);
            int currentVersion = (int)float.Parse(properties.ListItem["CustomVersion"].ToString());
            int newVersion;
            if (properties.AfterProperties["CustomVersion"] == null)
                newVersion = currentVersion;
            else
                newVersion = (int)float.Parse(properties.AfterProperties["CustomVersion"].ToString());
            int nextVersion = currentVersion + 1;
            using (SPWeb web = properties.OpenWeb())
            {
                SPList documentLibrary = web.Lists[targetDocumentLibrary];
                SPFolder targetFolder;
                try
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders[properties.ListItemId.ToString()];
                }
                catch
                {
                    targetFolder = documentLibrary.RootFolder.SubFolders.Add(properties.ListItemId.ToString());
                }
                if (currentVersion > newVersion)
                {
                    EventFiringEnabled = false;
                    //Clear the list items attachments
                    List<string> attachmentsToDelete = new List<string>();
                    foreach (string attachmentName in properties.ListItem.Attachments)
                    {
                        attachmentsToDelete.Add(attachmentName);
                    }
                    foreach (string attachmentName in attachmentsToDelete)
                    {
                        properties.ListItem.Attachments.Delete(attachmentName);
                    }
                    //Pull attachments for this version
                    SPQuery query = new SPQuery();
                    query.ViewAttributes = "Scope='Recursive'";
                    query.Query = String.Format("<Where><And><Contains><FieldRef Name='VersionLabel' /><Value Type='Text'>;{0}.0;</Value></Contains><Eq><FieldRef Name='{1}' /><Value Type='Lookup'>{2}</Value></Eq></And></Where>", newVersion, targetLookupField, properties.ListItem.Title);
                    SPListItemCollection items = documentLibrary.GetItems(query);
                    foreach (SPListItem item in items)
                    {
                        properties.ListItem.Attachments.Add(item.File.Name, item.File.OpenBinary());
                        item["VersionLabel"] = item["VersionLabel"].ToString() + ";" + nextVersion.ToString() + ".0;";
                        item.SystemUpdate(false);
                    }
                    properties.ListItem.SystemUpdate(false);
                    EventFiringEnabled = true;
                }
                else
                {
                    //Update all attachments
                    foreach (string fileName in properties.ListItem.Attachments)
                    {
                        SPFile document = targetFolder.Files[fileName];
                        document.Item["VersionLabel"] = document.Item["VersionLabel"].ToString() + ";" + nextVersion.ToString() + ".0;";
                        document.Item.Update();
                    }
                }
            }
            properties.AfterProperties["CustomVersion"] = nextVersion;
        }
    }

In the class above we can see we are using the ReceiverData field which we will define below, when attaching the event receiver, to store the document library and the name of the lookup field in the content type that we will be using to link back. We are creating a folder per ID of the item in the master list, and using this folder to store all attachments against any version of this item. We are tracking the version on the master list, and can, using the ListItem values and the AfterProperties values deduce from this whether we are doing a rollback or not.

Finally we need to attach this to our list and let it know which document library to point at using the following syntax:

  <Receivers ListUrl="Lists/My List">
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAttachmentAdding</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemUpdating</Type>
      <SequenceNumber>10001</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAdding</Type>
      <SequenceNumber>10002</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
    <Receiver>
      <Name>RedirectAttachmentsItemEventReceiver</Name>
      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
      <Class>Ebenezer.RedirectAttachments.EventReceivers.RedirectAttachmentsItemEventReceiver</Class>
      <Type>ItemAttachmentDeleting</Type>
      <SequenceNumber>10003</SequenceNumber>
      <Data>My List Attachments;My List</Data>
    </Receiver>
  </Receivers>

The advantage of this method is that no changes are needed to the out of the box edit/new forms in order for the attachments to be correctly versioned and updated with rollbacks.