Thursday 18 September 2014

Programatically overriding SharePoint record locks using Records.BypassLocks() in c#

Records are good - they are immutable and cannot be changed by users.

Record Centres are also good - they provide a repository for records that users can submit their documents to, to be transformed into records.

Event receiver rules that automatically commit as records any document submitted to a list are also good, because as soon as content is placed in that list it becomes a records, and so immutable.

However...

What if something goes wrong?  Perhaps a users submits a document that should not be locked as a record ans should be removed, or perhaps the wrong meta data is attached to the record?

In these cases an admin has to temporarily suspend the records management features of SharePoint in order to rectify the fault.  This is a big deal since SharePoint admins have a different role to server admins, and so normally would not have the authority (or indeed the know-how) to do this, whereas the server admins would not be able to understand the records and content.  In my experience the only people who know both how the system works, and what it is supposed to do, are the developers!  And we do NOT want to do admin, because among other things they somehow get paid more than we do...

Furthermore, if you are automatically declaring documents as records when they enter the list, you will not be able to "undeclare" the record from the SharePoint front end without first disabling this feature - quite a risk if you end up having to do this a lot.

This is why I like to create my own tools to fill the gaps, and in this case we programmers can turn to a little known feature of the Object Model to help us:   Records.BypassLocks().

This neat little method, part of the  Microsoft.Office.RecordsManagement.RecordsRepository namespace, allows use to define a delegate function that be be run even on locked down records.  Wrap it in a security delegate and you have the makings of a class that can be used to provide users (preferably power users and admins) with tools to manage their records centre WITHOUT suspending record handling functions.

Remember - this allows you to override records handling, so only use this if you really need to.  However if you have found this blog and read this far, you probably really need to!

Here is a basic method that shows you how to use Records.BypassLocks() in a simple record cancelling scenario.  In this example I pass in URL to the offending record, open a security delegate (in order to ensure that the process has permission to perform this task), and then use BypassLocks to edit the meta-data on the document to change the name and title and set the Status to "CANCELLED".

Please not that I have present this a method inside a class with no constructor - I will leave that up to you (simple copy the methods into whatever class you are working in if this confuses you).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Utilities;
using Microsoft.Office.RecordsManagement.RecordsRepository;  //You are going to need to include the dll in teh references
using System.Web;

namespace BusinessLayer
{

   public void Cancelrecord(string documentURL)
   {
        // Pass in the document URL in a format such as:
        //string documentURL = "http://test_reocrds/records/wrong_file.pdf";  

        //Run this in a security delegate

        SPSecurity.RunWithElevatedPrivileges(delegate() {

                using (SPSite site = new SPSite(SPContext.Current.Web.Url))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        web.AllowUnsafeUpdates = true;
                        try
                        {

                            SPListItem target = web.GetListItem(documentURL);

                            Records.BypassLocks(target, delegate(SPListItem item)
                            {         //Perform any action on the item. 
                                       CancelItem(item);
                            });

                        }
                        catch (Exception ex)  // it all throws up to here
                        {
                             throw ex;
                        }
                        finally
                        {

                            web.AllowUnsafeUpdates = false;

                        }
                    }
                }
            });
       }

        //Set item name to start with CANCELLED and set status to CANCELLED

        public void CancelItem(SPListItem item)
        {
            try
            {        

                // Update the name of the file

                item.File.CheckOut();
                item.TrySetValue("Name", String.Format("CANCELLED {0}", item.Name));
                item.TrySetValue("Title", String.Format("CANCELLED {0}", item.Title));
                item.TrySetValue("Status", "CANCELLED");  //

                item.Update();

                //This records library is set to use check in/out and versioning so it records any updates such as this

                item.File.CheckIn("CANCELLED", SPCheckinType.MinorCheckIn);

            }
            catch (Exception ex)
            {
                // Permissions failed.  Undo checkout
                item.File.UndoCheckOut();
                throw ex;
            }
        }
    }

}


 In my example above I use an Extensions class to add useful methods to SPListItem class to get or set values.  Here is the class:
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
namespace BusinessLayer

{

    public static class Extensions

    {

        public static T TryGetValue(this SPListItem listItem, string fieldInternalName)
        {
            if (!String.IsNullOrEmpty(fieldInternalName) &&
                listItem != null &&
                listItem.Fields.ContainsField(fieldInternalName))
            {
                object untypedValue = listItem[fieldInternalName];
                if (untypedValue != null)
                {
                    var value = (T)untypedValue;

                    return value;
                }
            }

            return default(T);
        }

        public static bool TrySetValue(this SPListItem listItem, string fieldInternalName, T value)
        {
            try
            {
                if (!String.IsNullOrEmpty(fieldInternalName) &&
                    listItem != null &&
                    listItem.Fields.ContainsField(fieldInternalName))
                {
                    listItem[fieldInternalName] = value;
                }

                return true;
            }
            catch
            {
                throw;
            }
        }
    }
}