原文地址(请自备云梯):点击打开链接
这篇博客讨论了SharePoint中Event Receiver的工作原理,并通过讨论,解决了一个上传文档的时候常见的问题。
我们知道Event Receivers分为两种,一种是同步的Event Receiver(Synchronous,例如ItemAdding与ItemUpdating),另外一种是异步的Event Receiver(Asynchronous,例如ItemAdded与ItemUpdated)。Event Receiver是在托管代码中实现的,因此使用SPRequest这个非托管代码来调用Event Receiver的时候,是使用ISPEventManager这个COM接口来调用的,这个接口定义在Microsoft.SharePoint.SPEventManager类中。
[ComImport, SuppressUnmanagedCodeSecurity, InterfaceType((short)1), Guid(“BDEADF0F-C265-11D0-BCED-00A0C90AB50F”)]
public interface ISPEventManager
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void ExecuteItemEventReceivers(
ref byte[] userToken, ref objecteventReceivers, ref ItemEventReceiverParams itemEventParams,
out objectchangedFields, outEventReceiverResult eventResult, out stringerrorMessage);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void EnqueueItemEventReceivers(ref byte[] userToken, ref objecteventReceivers, ref ItemEventReceiverParams itemEventParams);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void ExecuteListEventReceivers(
ref byte[] userToken, ref objecteventReceivers, ref ListEventReceiverParams ListEventParams,
outEventReceiverResult eventResult, out stringerrorMessage);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void EnqueueListEventReceivers(ref byte[] userToken, ref objecteventReceivers, ref ListEventReceiverParams ListEventParams);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void ExecuteWebEventReceivers(
ref byte[] userToken, ref objecteventReceivers, ref WebEventReceiverParams webEventParams,
outEventReceiverResult eventResult, out stringerrorMessage);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void EnqueueWebEventReceivers(ref byte[] userToken, ref objecteventReceivers, ref WebEventReceiverParams webEventParams);
}
这个接口包含两种方法,一种是EnqueueXXXEventReceivers,包含EnqueueItemEventReceivers, EnqueueListEventReceivers和EnqueueWebEventReceivers,这类方法是用来处理异步的event receivers的。另一种是ExecuteXXXEventReceivers,包含ExecuteItemEventReceivers,ExecuteListEventReceivers和ExecuteWebEventReceivers这种方法是用来处理同步的event receivers的。
Event Receivers,无论是同步还是异步,都是运行在触发他们执行的进程中,例如,用户通过webpart向文档库中添加一个文档,那么event receiver会在w3wp进程中运行。如果用户通过一个winform程序MyWinformApp.exe上传文档,那么event receiver会在MyWinformApp.exe进程中运行。这里需要注意的是异步event receiver,有时候异步的event receiver需要一定时间来完成,如果使用MyWinformApp.exe上传文档之后立即关闭,但是这时候event receiver还没有执行完,那么event receiver会被中断,或者压根儿就没有执行。这种情况在w3wp进程中也是一样的,如果在event receivers运行的时候重启IIS,那么event reciever会被终止,除非使用iisreset /noforce命令,这样的话虽然会等待event receivers执行完成再重启iis,但是即使这样,如果你想在event receivers保存对item的修改,这些修改将会丢失。
所以,重启IIS(不使用/noforce)会中断所有event receivers的执行,而使用/noforce来重启iis,虽然不会中断event receivers的执行,但是对SharePoint对象的修改可能不会生效。因此,不要让你的event receiver的运行时间超过一秒钟,否则的话很可能会被中断,而导致整个应用程序的错误!
SharePoint会新起一个线程(来自System.Threading.ThreadPool)来执行异步的event receiver,每触发一个异步的event receiver,就会新起一个线程,并把这个线程添加到执行队列中。如果用户上传了100个文件,触发了100个event receivers,就会有100个线程被加入队列(即调用100次EnqueueItemEventReceivers)。
我们知道在线程之间切换是比较消耗资源的,如果同时有100个线程在运行,会降低进程的执行效率。我见过一个迁移工具,在文档库之间移动文件,移动的同时有一些event receivers被触发。移动几百个文件的时间也就一分钟左右,但是执行event receivers的时间却用了45分钟。90%时间都消耗在了线程之间的切换上,只有10%的时间真正用于执行代码,所以如果在移动文件之后,可以等待event receiver执行完成再移动写一个文件的话,执行时间会缩短,因为省去了线程的切换时间。
我们再研究一下event receiver的重要参数“SPItemEventProperties”,这个参数在触发event receiver的时候被创建,同时会被其他event receiver共享,例如在ItemAdded触发时创建的SPItemEventProperties参数,ItemUpdated也可以使用。需要注意的是,同步的event receivers共享一个SPItemEventProperties,而异步的event receivers共享另一个SPItemEventProperties参数。还有一个有趣的地方是,非托管的SPRequest类会保存在同步event receiver中对这个参数的修改,供之后执行的异步的event receiver使用,也就是可以使用这个参数在event receivers之间传递数据!但是这样做是有限制的,目前只能在ItemAdding和ItemUpdating这两个同步的event receivers中修改AfterProperties集合,然后这些数据可以在异步的ItemAdded和ItemUpdated的AfterProperties集合中使用。
另一个常见的问题是,我们在event receiver中使用SPItemEventProperties.OpenWeb()方法的时候,是否需要显示的释放web。如果我们查看SPItemEventProperties类的代码就会找到答案:
public sealed class SPItemEventProperties : SPEventPropertiesBase, IDisposable
{
…
private SPSite OpenSite()
{
if (((this.m_site == null) && (this.WebUrl != null)) && (this.m_site == null))
{
if (this.m_userToken == null)
{
this.m_site = new SPSite(this.WebUrl);
}
else
{
this.m_site = new SPSite(this.WebUrl, this.m_userToken);
}
this.m_siteCreatedByThis = true;
}
return this.m_site;
}
…
public SPWeb OpenWeb()
{
this.OpenSite();
if (this.m_site == null)
{
return null;
}
return this.m_site.OpenWeb(this.RelativeWebUrl);
}
…
public void Dispose()
{
if (this.m_site != null)
{
while (this.m_siteCreatedByThis)
{
this.m_site.Dispose();
this.m_site = null;
this.m_siteCreatedByThis = false;
break;
}
}
}
}
从代码中可以看到,SPItemEventProperties自己已经实现了IDisposable和Dispose来释放资源,SPEventManager会在event receiver执行完成的时候自动调用dispose方法来释放资源,因此我们不需要显示的释放。
解决问题:在ItemAdded执行完之前弹出EditForm.aspx页面
就是在一个文档库上添加了ItemAdded事件,用来在上传文档的时候修改item的某个属性的值。因为ItemAdded事件是异步的,这样就可能导致该事件没有完成的时候,就弹出EditForm.aspx页面编辑属性,或者在编辑完属性的时候,点击OK无法保存,提示文件正在被修改。为了解决这个问题,我们需要写一些代码,来确保在ItemAdded执行完之后再弹出EditForm.aspx页面。一种方法是,修改EditForm.aspx页面,添加一个web control,用来等待ItemAdded事件的完成,同时我们需要一个特殊的ItemAdded事件来与这个web control一起工作。
这个特殊的ItemAdded事件代码可以从以下链接下载:
SharePointInternals.SynchronousItemAdded.dll
SharePointInternals.SynchronousItemAdded – Source Code
使用方法很简单,只需要创建一个继承SPSynchronousReceiver的event receiver同时重写ItemAddedSynchronously方法即可:
public class SPTestReceiver : SPSynchronousReceiver
{
protected override void ItemAddedSynchronously(SPItemEventProperties properties)
{
// Your code goes here
}
// Your other item receiver overrides go here
}
然后在EditForm.aspx页面中插入WaitForItemAdded这个web control:
<%@Register TagPrefix=”SharePointInternals”
Assembly=”SharePointInternals.SynchronousItemAdded, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d7dbdc19a16aed51″
namespace=”SharePointInternals.WebControls”%>
…
<asp:Content ContentPlaceHolderId=”PlaceHolderMain” runat=”server”>
<SharePointInternals:WaitForItemAdded ID=”waitForItemAdded1″ runat=”server”/>
…
</asp:Content>
只有将WaitForItemAdded添加到EditForm.aspx页面中才能解决问题,如果将WaitForItemAdded添加到ListFormWebPart使用的ListFormTemplate中,这个control确实会确保ItemAdded事件执行完成,但是这个时候,item早已经被加载SPContext中了,因为在加载EditForm.aspx的时候,会使用SPContext.Current.ListItem来加载item的属性,如果加载的时候ItemAdded并没有执行完,那么EditForm.aspx仍旧会显示旧的属性值,也就是ItemAdded事件执行之前的属性值,而不是ItemAdded事件中修改的新的属性值。这个时候如果点击EditForm.aspx上的OK按钮,会报错:
The file … has been modified by … on …
using (SPSite site = new SPSite(“http://server/sites/test”))
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists[“Shared Documents”];
SPFile file = list.RootFolder.Files.Add(fileName, fileBytes);
SPSynchronousReceiver.WaitForItemAddedReceivers(list, file.Item.ID);
}
最后,有一个特殊的event receiver需要特别注意一下:ListItemFileConverted,这个event receiver是在执行SPFile.Convert()方法的时候初始化的,是在托管代码中初始化的,而不像其他的event receivers一样在非托管代码中初始化。这是个异步的event receiver。
总结一下今天的讨论:除了ListItemFileConverted这个event receiver之外,其他的receivers都是在非托管代码中初始化的。当SPRequest这个非托管对象调用了某些触发receivers的方法时,就开始初始化receivers,之后通过ISPEventManager COM 接口调用它们。我们不需要显示的释放properties.OpenWeb()方法返回的web对象,有时候new一个新的SPSite或者SPWeb会是更好的选择。所有异步的receivers都在线程中执行,如果线程过多,会因为频繁切换线程而降低SharePoint的性能。如果重启IIS,会中断event receivers的执行。最后我们使用代码解决了ItemAdded没有执行完就弹出EditForm.aspx页面的问题。