ItemAdded Event on Document Library & the file.. has been modified by.. on.. error.

I am sure most of us would have faced this issue when an ItemAdded eventreciever is attached to a Document Library, and in that we try to update a few non-default fields (say custom columns added on the Library) that are meant to be displayed on EditForm.aspx in updated state. An example would be to rename the item (“Name” field) to append a custom string(say, SPUser’s display name) to it , as soon as the file is uploaded.

In this post, I would like to share my findings about the issue, and a workaround which I have recently shared with a customer :

Issue :

As you know that asynchronous events (such as ItemAdded), though running under the same process, run under a different thread, which is spawned by SPEventManager internal class and are en-queued and executed based on a call-back mechanism. Therefore, asynchronous events require some time to complete execution as compared to synchronous ones and the time also depends on the code that goes in corresponding eventhandlers. Therefore it is recommended not to use heavy calls inside asynchronous events – but depending on the memory status of w3wp.exe and load on the system – we can get into the situation (more details below)– even when doing a small operation , such as changing “Name” or for that matter any other field which is to be displayed on EditForm.aspx.

To elaborate further – let’s see what is happening when we change the name of item in the ItemAdded EventHandler:

  • As soon as a file is uploaded – a item corresponding to SPFile is initiated and it’s version is set to 1.
  • At this point two operations occur on separate threads –

a)Initiation of ItemAdded event from unmanaged code (SPRequest internal class)

b)Redirect to EditForm.aspx (only when Document Library has non-default columns added to it) which will load the ListItem information from SPContext.

  • Based, on which completed earlier, we will get the corresponding details , that is :

a)If ItemAdded (it will increment internal version to 2)completed before EditForm.aspx load –- we will get the correctly updated fields on EditForm.aspx.

b)If ItemAdded hasn’t finished execution or is still in en-queued state – the ItemContext loaded on EditForm.aspx is the version 1 (because Version1 is the only one that exists)

At this point, if click on “OK” or “Check-In” (if “Required Documents to be checked out before they can be edited” is selected), we get this exception :

The file <filename> has been modified by <user> on <datetime>.            

And this in ULS logs :

Application error when access /sites/abc/doclib1/Forms/EditForm.aspx, Error=The file abc.csv has been modified by domain\user1 on <date>

at Microsoft.SharePoint.Library.SPRequestInternalClass.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish) at Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish)       

which is imperative,as we already have a newer version of the item available.

Now, above error can be eliminated by using SystemUpdate(false) instead of Update() on the ListItem in ItemAdded receiver. But the ItemContext(fields) loaded on EditForm may still point to the older version. So, if we have field that need to be displayed on EditForm.aspx in updated state, using SystemUpdate() may not suffice.

Workaround:

The broad idea is to wait for ItemAdded to finish before the ListFieldIterator control residing on EditForm.aspx loads (It’s the ListFieldIterator which displays the Item’s fields ).

Here are the two ways, this can be done :

Option1:

So, I proceeded to create a custom WebControl , the code of which is below :

<CODE-SNIPPET>

 public class EditFormControl : WebControl
    {
        protected override void  OnInit(EventArgs e)
        {
            if (HttpContext.Current.Request != null &&
                SPContext.Current.List != null)
            {
                TimeSpan gapSinceCreate = new TimeSpan(1);

                do
                {
                    LoopPeriod(ref gapSinceCreate);
                } while ((gapSinceCreate.Seconds < 3));
            }
        }
        private TimeSpan LoopPeriod(ref TimeSpan t)
        {
            using (SPSite site = new SPSite(SPContext.Current.Site.ID))
            using (SPWeb web = site.OpenWeb(SPContext.Current.Web.ID))
         {
   SPList list = web.Lists[SPContext.Current.ListId];
   SPListItem item = list.GetItemById(SPContext.Current.ItemId);

   DateTime timeCreated = (DateTime)item["Created"];
   t = new TimeSpan(DateTime.Now.ToUniversalTime().Ticks - timeCreated.ToUniversalTime().Ticks);
                     return t;
         }
        }
       
    }
</CODE-SNIPPET >

As you can see that this control will wait in the do-while loop , till it has been 3 seconds past the ItemCreation date (“created” field). This is based on the assumption that all queued asynchronous events would have finished within 3 seconds depending on the system load – the value can be incremented\decremented based on environment.

Next, we can deploy the custom control on a specific Library's EditForm.aspx.

Steps to Deploy :

After building the project,strong-naming it, deploying the dll to GAC place the <safe control> entry in web.config :

 <SafeControl Assembly="EditFormControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=58dbbcbf7aa28c26" Namespace="EditFormControl" TypeName="*" Safe="True" />

- Open the Library’s EditForm.aspx in SPD and Register the assembly by placing this Register tag after the “<%@ Register Tagprefix="SharePoint"” tag:

 <%@ Register TagPrefix="EditFormLoop" Assembly="EditFormControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=58dbbcbf7aa28c26" Namespace="EditFormControl"%>

- Deploy the control inside” PlaceHolderMain” before “ListFormWebPart” (needless to say, the sequence is important)

 <asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
   <EditFormLoop:EditFormControl2 ID="waitForItemAdded1" runat="server" />

With this change in place, you can effectively test the EventHandler and if no queued event takes more than 3 seconds to finish, this solution should hold through 100% of the time.

For the scenario, where you do not want to hard-limit the no. of seconds to wait, here’s Option2.

Option2:

So, instead of putting a hard-limit of 3 seconds for EditForm.aspx to wait for queued eventhandlers to finish, we can have a custom hidden field in the DocumentLibrary (lets, call it “ItemAddedCol”).This field will have a default value of “False” and will be accessible only to EventHandler code

  • It can be added through UI (single line of text - with default value “False” and make SPlist.Fields[“ItemAddedCol”].Hidden=true and then Update().
  • We can create a custom Document Library with this Field and specify its Hidden property and default value.

Next, this field will be updated in EventHandler code whenever ItemAdded fires because we made a provision for this in the EventHandler , something like this:

 if(i.Fields.ContainsField("ItemAddedCol"))
i["ItemAddedCol"] = true.ToString();
i.Update();

Here is the code for WebControl for Option2 :

< CODE-SNIPPET >

 public class EditFormControl2 : WebControl
    {
        protected override void OnInit(EventArgs e)
        {
if (HttpContext.Current.Request != null &&
                SPContext.Current.List != null)
            {
                bool ItemAddedColumn = false;

                do
                {
                   ItemAddedColumn = LoopPeriod();
                } while (!ItemAddedColumn);
            }
        }
        private bool LoopPeriod()
        {
            using (SPSite site = new SPSite(SPContext.Current.Site.ID))
            using (SPWeb web = site.OpenWeb(SPContext.Current.Web.ID))
            {
                SPList list = web.Lists[SPContext.Current.ListId];
                SPListItem item = list.GetItemById(SPContext.Current.ItemId);

                bool flag = bool.Parse(item["ItemAddedCol"].ToString());
                return flag;
            }
        }

    }

</ CODE-SNIPPET >

So, we can use EditFormControl2, instead of EditFormControl in 'Steps To Deploy' listed above.

The only disadvantage that I see of Option2 above is that , if by any chance ItemAdded fails to update the “ItemAddedCol” value to “True” (due to some exception), then the EditFormControl2 may get stuck into an infinite loop and may lead to Out of memory issues.

I think, a combination of both Options can also be tried upon to make sure all situations are covered.