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.
Comments
Anonymous
December 26, 2009
it's really good solution, but my problem is how to get a session variable or the HttpContext or the SPContext from inside Itemadded event, it's always null becos the event is asynchronous event, even I tried to get the context from a class variable and assign it in the constructor but also it's null. do you have any solution for this ? thanks a lot.Anonymous
December 26, 2009
You can try the senario when uploading multiple documents and need to change the document name ( append the current loged in user to the doc name ), you will find the SPContext and HttpContext.Current is null. but if you upload one document ( not using the multiple document option ) then the context is not nullAnonymous
February 09, 2010
I think the tag to add the webcontrol to the page should be: <EditFormLoop:EditFormControl ID="waitForItemAdded1" runat="server" /> instead of: <EditFormLoop:EditFormControl2 ID="waitForItemAdded1" runat="server" />Anonymous
September 21, 2010
...but that isn’t a solution since it only delays it for a bit, if your processes are running a bit longer then it just delays the error Any case, this at least put me on the right track on the problem I was having. What we had: There is a Form library and on this form library there was an associated InfoPath form. This meant that if you add an XML file into the library, the list would take the values from the XML file and populate the columns / fields for the listitem The XML that we are sending in are from a Third party and unchangeable, so we had to run some XML changes when a new file came in. I added this on the ItemAdded event. When you manually upload a new file, SharePoint 2010 gives you a nice silverlight / ajax screen to pick your file, after picking this file, the ItemAdded event fires, I change the XML and then save it back to the list with SaveBinary() which in turns calls an Update() In the mean time the screen opens up the EditForm.aspx page to allow you to change the fields, when you click on the Save button it also goes and runs an Update() , now sometimes by pure coincidence, these two updates clash and since there is concurrency measures in place you get the the “file.. has been modified by.. on.. error” I tried locking the file, checking it out etc etc etc but in the end I realised that I needn’t wrestle the process, and that the Save dialog actually updates the item again, so I can just pick up the ItemUpdated event and then do my work in peace and quiet… All in all, SharePoint 2010 has very nice AJAX features but no accomodation in the code for the asynchronous calls flying around…which almost begs the question, can it really handle concurrency?Anonymous
November 25, 2010
The comment has been removedAnonymous
February 21, 2011
In that case, use the ItemAdded event, but bind it synchronously. Add the below element under your <Receiver> element. <Synchronization>Synchronous</Synchronization>Anonymous
February 16, 2015
Thanks for taking the time and effort to write this. I've been working with an event receiver for two days and couldn't understand why it works one minute and crashes the next. It's so obvious now: sometimes my event isn't ready when the form is displayed and it crashes on check in. Now when I know what's causing the problem, I can fix it. Thanks a lot!Anonymous
December 16, 2015
Best solution: blogs.msdn.com/.../sharepoint-event-receivers-making-asynchronous-event-synchronous-to-avoid-save-conflict-error.aspx To avoid this error the ItemAdded event can be made Synchronous by doing the following: <Elements xmlns="schemas.microsoft.com/.../"> <Receivers ListUrl="ListName"> <Receiver> <Name>EventReceiver1ItemAdded</Name> <Type>ItemAdded</Type> <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly> <Class>EventReceiverProject1.EventReceiver1.EventReceiver1</Class> <SequenceNumber>1000</SequenceNumber> <Synchronization>Synchronous</Synchronization> </Receiver> </Receivers> </Elements> In the highlighted text: •ListURL contains the list name for which the event receiver needs to be activated. It can also have ListTemplateId, that would mean that the event receiver is activated for all the lists/libraries created with the template ID. •Synchronization node has the value "Synchronous", which will make the ItemAdded event Synchronous.