深入理解SharePoint中的Event Receiver功能

来源:互联网 发布:战舰世界炮弹数据 编辑:程序博客网 时间:2024/05/07 08:25

原文地址(请自备云梯):点击打开链接

这篇博客讨论了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 …


我们之前有例子讲到多个异步event receivers执行期间切换线程浪费时间的问题,其实也可以用同样的办法解决,即每添加一个文档之后就强制等待event receiver执行完毕之后再添加下一个文件。这可以使用SPSynchronousReceiver.WaitForItemAddedReceivers()方法实现:

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页面的问题。

0 0
原创粉丝点击