Second Life of a Hungarian SharePoint Geek

September 2, 2018

How to delete events from a SharePoint calendar without messing up the Recycle Bin

Filed under: Bugs, Calendar, SP 2013 — Tags: , , — Peter Holpar @ 09:01

Recently we detected that a lot of deleted calendar entries appeared in the Recycle Bin of a SharePoint site. They were all originated from a specific calendar instance, and deleted by a custom application (written in C#) that is scheduled to run regularly to purge old entries.

Although we could have deleted the items using the bulk method described here, in this case we deleted the entries one by one. To get IDs of the “master” entries (entries that are not recurring event exception) we should delete, we run a CAML query like the below one, filtering on the EventType field of the items, just as described in my former post:

<Where>
    <And>
    <Lt>
        <FieldRef Name=’EndDate’ />
        <Value Type=’DateTime’>2018-01-01 00:00:00</Value>
    </Lt>
    <Lt>
        <FieldRef Name=’EventType’/>
        <Value Type=’Integer’>2</Value>
    </Lt>
    </And>
</Where>

You can read more about the meaning of the various EventType values in this blog post.

As I wrote in my post, we should (at least, theoretically, see explanation later) delete only the main entries, as all of the related entries (the recurring event exceptions, deleted and changed instances of the series) are deleted automatically by the system when you delete the main entry.

Having the IDs from the CAML query, we deleted the entries by iterating through the collection of IDs and invoked the the DeleteItemById method of the SPListItemCollection method, like:

foreach (int itemIDToDelete in itemIDsToDelete)
{
    calendarList.Items.DeleteItemById(itemIDToDelete);
}

As you probably know, you can delete an item from code by invoking the Delete method of the SPListItem instance of the item (in this case the item is deleted immediately, without being recycled), or recycle it by calling the Recycle method of the SPListItem instance (in this case the item is simply moved to the Recycle Bin and you can restore it later if you wish). I should point out, that both of these methods call internally the private DeleteCore method of the SPListItem class, using the parameter value DeleteOp.Delete in the first case and DeleteOp.Recycle in the second case.

The DeleteItemById method invokes the Delete method however, so it should not miss up the Recycle Bin, as it obviously did in our case.

public void DeleteItemById(int id)
{
    this.GetItemById(id).Delete();
}

So what was the problem? After a short investigation and running a few tests, we found, that only recurring event exceptions got moved to the Recycle Bin when the system delete them automatically, the main entries were deleted permanently. It means, that the Delete method of the SPListItem class is buggy and the same is true for the DeleteItemById method, at least I don’t consider this behavior to be some kind of hidden feature.

How to solve the problem? If you have a lot of recurring event exceptions in your calendar, and don`t want to pollute your Recycle Bin, the best you can do to create some kind of extension method that deletes the related entries (recurring event exceptions) explicitly, not letting the system to delete them automatically.

I’ve created two extension methods, as shown below:

  1. public static void DeleteItemByIdIncludingRecurringEventExceptions(this SPListItemCollection items, int id)
  2. {
  3.     items.GetItemById(id).DeleteIncludingRecurringEventExceptions();
  4. }
  5.  
  6. public static void DeleteIncludingRecurringEventExceptions(this SPListItem item)
  7. {
  8.     if (item == null)
  9.     {
  10.         throw new ArgumentNullException("item");
  11.     }
  12.  
  13.     if (!item.ContentTypeId.IsChildOf(SPBuiltInContentTypeId.Event))
  14.     {
  15.         throw new ArgumentException(string.Format("Item must have a content type of Event ({0}) or a content type derived from that", SPBuiltInContentTypeId.Event), "item");
  16.     }
  17.  
  18.     // we need to perform the check only if the item is a main entry of a recurring event series
  19.     // in this case, EventType should be 1, see
  20.     // https://aspnetguru.wordpress.com/2007/06/01/understanding-the-sharepoint-calendar-and-how-to-export-it-to-ical-format/
  21.     var eventType = item["EventType"];
  22.     if ((eventType is int) && ((int)eventType == 1))
  23.     {
  24.         SPList list = item.ParentList;
  25.         int itemId = item.ID;
  26.  
  27.         // querying recurring event exceptions that belong to the current item
  28.         SPQuery query = new SPQuery();
  29.         SPListItemCollection itemsToDelete = null;
  30.         query.Query = String.Format(@"<Where><And><Gt><FieldRef Name='EventType'/><Value Type='Integer'>2</Value></Gt><Eq><FieldRef Name='MasterSeriesItemID'/><Value Type='Integer'>{0}</Value></Eq></And></Where>", itemId);
  31.         itemsToDelete = list.GetItems(query);
  32.  
  33.         //
  34.         List<int> itemIDsToDelete = itemsToDelete.Cast<SPListItem>().Select(i => i.ID).ToList();
  35.  
  36.         itemIDsToDelete.ForEach(i =>
  37.             {
  38.                 SPListItem subItem = list.GetItemById(i);
  39.                 try
  40.                 {
  41.                     subItem.Delete();
  42.                 }
  43.                 catch (Exception ex)
  44.                 {
  45.                     // error when deleting a recurring event exception
  46.                     // as a possible workaround, convert it to a standard event and try to delete it again
  47.                     subItem["EventType"] = 0;
  48.                     subItem.Update();
  49.                     subItem.Delete();
  50.                 }
  51.             });
  52.     }
  53.     // finally, delete the main entry as well
  54.     item.Delete();
  55. }

You can use the DeleteItemByIdIncludingRecurringEventExceptions method in place of the DeleteItemById method and DeleteIncludingRecurringEventExceptions method in place of the Delete method. We search for the related items by using the MasterSeriesItemID field value in the CAML query. You can use these methods only for items having the Event content type or a custom content type derived from it.

You can use the methods like this:

var list = web.Lists["Calendar"];          
list.Items.DeleteItemByIdIncludingRecurringEventExceptions(10);

Note, that we also had some corrupted recurring event exceptions in our calendar, probably created automatically by a faulty application. Although we could get a reference for the items itself (for example, by calling the GetItemById method), and change its properties if we wished, we got the exception below if we tried to delete them from code, or even if we only wanted to display the items from the All Events view in the browser.

Item does not exist. The page you selected contains an item that does not exist. It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>
SPRequest.DeleteItem: UserPrincipalName=i:0).w|s-1-5-21-3634847118-2359816030-2114994487-3414, AppPrincipalName= ,bstrUrl=http://YourServer/Web/SubWeb ,bstrListName={A38F8D71-F481-4A93-85B8-AC42BB2BE6EC} ,lID=4596 ,dwDeleteOp=3 ,bUnRestrictedUpdateInProgress=False
System.Runtime.InteropServices.COMException: Item does not exist. The page you selected contains an item that does not exist. It may have been deleted by another user.<nativehr>0x81020016</nativehr><nativestack></nativestack>, StackTrace:    at Microsoft.SharePoint.SPListItem.DeleteCore(DeleteOp deleteOp)     at Microsoft.SharePoint.SPListItem.Delete()     at CallSite.Target(Closure , CallSite , Object , Int32 )     at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)     at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)     at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)     at System.Management.Automation.Interpreter.Ente…
…rTryCatchFinallyInstruction.Run(InterpretedFrame frame)     at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)     at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)     at System.Management.Automation.DlrScriptCommandProcessor.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess)     at System.Management.Automation.DlrScriptCommandProcessor.Complete()     at System.Management.Automation.CommandProcessorBase.DoComplete()     at System.Management.Automation.Internal.PipelineProcessor.DoCompleteCore(CommandProcessorBase commandRequestingUpstreamCommandsToStop)     at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecuteEnumerate(Object input, Hashtable errorResults, Boolean enumerate)     at S…
…ystem.Management.Automation.Runspaces.LocalPipeline.InvokeHelper()     at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()     at System.Management.Automation.Runspaces.PipelineThread.WorkerProc()     at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)     at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)     at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)     at System.Threading.ThreadHelper.ThreadStart()
 

The simplest workaround I’ve found to delete those entries was to set their EventType field value to 0, as they would be master entries, not recurring event exceptions. After this change, it was possible to delete the items. This kind of hack is also included in the DeleteIncludingRecurringEventExceptions extension method above.

Although the code I provided here seems to do the job and not pollute the Recycle Bin any more, if you have a really large number of items to delete, for performance reasons I still would prefer the bulk deletion of the events, or you should write your own solution to select all of the recurring event exceptions in the first step, and deleting them before you delete the main entries in the second step. I don’t think it would be a great idea to run a separate CAML query for each recurring events in your calendar.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: