在 SharePoint 加载项中创建加载项事件接收器

如果已事先了解提供程序托管的 SharePoint 加载项,并且已开发出一些至少比“Hello World”更高级的程序,将会很有帮助。 请参阅开始创建提供程序托管的 SharePoint 加载项

此外,应熟悉在 SharePoint 加载项中处理事件

获取更多代码示例

如果您看完本文中的后续示例,将会有一个完成的代码示例。 以下是一些其他示例。 它们不都遵循本文中所述的体系结构。 可以有多个构建外接事件接收器的好方法,也要牢记 Microsoft 指南也会随时间的推移而更新。

添加加载项安装事件接收器

  1. 在 Visual Studio 中,打开提供程序托管的 SharePoint 外接程序的项目。 (如果将外接程序事件处理程序添加到 SharePoint 托管的外接程序,则适用于 Visual Studio 的 Office 开发人员工具将其转换为提供程序托管的应用程序。)

  2. 在“解决方案资源管理器”中,选择 SharePoint 加载项的节点。

  3. 在“属性”窗口中,将“处理已安装的加载项”的值设置为“True”

    属性窗口中的应用程序事件

    Visual Studio 的 Office 开发人员工具将执行下列操作:

    • 添加一个名为 AppEventReceiver.svc 的文件,它包含一些框架式 C#(或 VB.NET)代码。 这是用于处理加载项事件的服务。

    • 将下面的项添加到 AppManifest.xml 文件的“属性”部分:<InstalledEventEndpoint>~remoteAppUrl/AppEventReceiver.svc</InstalledEventEndpoint>。 此项将加载项事件接收器注册到 SharePoint。

      注意

      ~remoteAppUrl 令牌同样也用于提供程序托管的 SharePoint 加载项中的远程 Web 应用程序。 Visual Studio 的 Office 开发人员工具假定 Web 应用程序的域和事件处理程序相同。 极少数情况下会有不同,需要手动将令牌 ~remoteAppUrl 替换为服务的实际域。

    • 如果 SharePoint 加载项还没有 Web 项目,则创建一个。 这些工具还会确保为某个提供程序托管的加载项配置加载项清单。 此外,它们还添加页面、脚本、CSS 文件和其他项目。 如果加载项需要的唯一远程组件是事件处理 Web 服务,则可以从项目中删除这些内容。 还应确保加载项清单中的 StartPage 元素不指向已删除的页。

  4. 如果测试 SharePoint 场不是在运行 Visual Studio 的同一台计算机上,请使用 Microsoft Azure 服务总线配置用于调试的项目。 有关详细信息,请参阅在 SharePoint 加载项中调试远程事件接收器并排除故障

  5. 如果 AppEventReceiver.svc 文件中有一个 ProcessOneWayEvent 方法,它的实现应仅包含 throw new NotImplementedException(); 行,因为此方法不能在加载项事件处理程序中使用。

    加载项事件处理程序需返回一个对象,用于告诉 SharePoint 是继续完成还是回滚事件,且 ProcessOneWayEvent 方法不返回任何内容。

  6. 文件包含 ProcessEvent 方法,如下所示。 (可能还会有一个代码块来说明如何获取客户端上下文。将其删除或注释掉。)

    public SPRemoteEventResult ProcessEvent(SPRemoteEventProperties properties)
    {
        SPRemoteEventResult result = new SPRemoteEventResult();
    
        return result;
    }
    

    关于此代码,请注意以下几点:

    • SPRemoteEventProperties 对象发送至处理程序 Web 服务作为 SOAP 消息,其中包含 SharePoint 中的一些上下文信息,包括用于标识事件的 EventType 属性。

    • 处理程序返回的 SPRemoteEventResult 对象包含 Status 属性,该属性的可能值是 SPRemoteEventServiceStatus.ContinueSPRemoteEventServiceStatus.CancelNoErrorSPRemoteEventServiceStatus.CancelWithErrorStatus 属性的默认值是 Continue,它指示 SharePoint 继续完成事件。 其他两个值告诉 SharePoint:

      • 运行处理程序最多三次。
      • 如果它仍然收到取消状态,则取消该事件,并回滚作为事件的一部分完成的任何操作。
  7. 在声明 result 变量的行后,立即添加下面的开关结构,以标识正在处理的事件。

    switch (properties.EventType)
    {
        case SPRemoteEventType.AppInstalled:
            break;
        case SPRemoteEventType.AppUpgraded:
            break;
        case SPRemoteEventType.AppUninstalling:
            break;
    }
    

    注意

    如果有面向 AppInstalledAppUpdatedAppInstalling 事件的处理程序,它们会各自在加载项清单中注册其自己的 URL。 因此 ,你可以 为它们使用不同的终结点,但本文 (和适用于 Visual Studio 的 Office 开发人员工具) 假设它们具有相同的终结点;这就是为什么代码需要确定哪个事件称为它的原因。

  8. 将回滚逻辑和“已完成”逻辑包括在加载项事件处理程序中中所述,如果安装逻辑出错,你几乎总是希望取消加载项安装,希望 SharePoint 回滚安装过程中所执行的操作,并回滚处理程序执行的操作。

    实现这些目的的一种办法是,将以下代码添加到 AppInstalled 事件的 case 中。

    case SPRemoteEventType.AppInstalled:
    try
    {
        // Add-in installed event logic goes here.
    }
    catch (Exception e)
    {
        result.ErrorMessage = e.ErrorMessage;
        result.Status = SPRemoteEventServiceStatus.CancelWithError;
    
        // Rollback logic goes here.
    }
    break;
    

    注意

    将时间超过 30 秒的代码安装转移到加载项中。 可以将其添加到加载项首次运行时所执行的“首次运行”逻辑。 加载项可以显示一条消息,如“我们正在为你准备好一切”。或者,加载项可以提示用户运行初始化代码。

    如果“首次运行”逻辑在加载项中不可行,另一个选项是让事件处理程序启动远程异步进程,然后立即返回 SPRemoteEventResult 对象,其中 Status 设置为 Continue。 此策略的一个缺陷是,如果远程进程失败,将无法告知 SharePoint 回滚加载项安装。

  9. 加载项事件处理程序体系结构策略中所述,处理程序委派策略为首选策略,尽管它不一定适用于所有情况。 在后续示例中,我们将介绍在将列表添加到主机 Web 时如何实现处理程序委派策略。 有关如何创建不使用处理程序委派策略的类似 AppInstalled 事件处理程序的信息,请参阅示例 SharePoint/PnP/Samples/Core.AppEvents

    下面是 AppInstalled case 块的新版本。 请注意,适用于所有事件的初始化逻辑不仅仅是 switch 块。 由于安装的同一个列表会在 AppUninstalling 处理程序中被删除,所以在此处对该列表进行标识。

    SPRemoteEventResult result = new SPRemoteEventResult();
    String listTitle = "MyList";
    
    switch (properties.EventType)
    {               
        case SPRemoteEventType.AppInstalled:
    
    try
    {
            string error = TryCreateList(listTitle, properties);
            if (error != String.Empty)
            {
                throw new Exception(error);            
            }
    }
        catch (Exception e)
    {
            // Tell SharePoint to cancel the event.
            result.ErrorMessage = e.Message;
            result.Status = SPRemoteEventServiceStatus.CancelWithError;               
        }
            break;
        case SPRemoteEventType.AppUpgraded:
        break;
        case SPRemoteEventType.AppUninstalling:
        break;
    }                      
    
  10. 使用以下代码将列表创建方法添加至 AppEventReceiver 类作为一个 private 方法。 请注意, TokenHelper 类具有一个经过优化用以获取外接程序事件的客户端上下文的特定方法。 为最后的参数传递 false 可确保该上下文适合主机 Web。

    private string TryCreateList(String listTitle, SPRemoteEventProperties properties)
    {    
        string errorMessage = String.Empty;          
    
        using (ClientContext clientContext =
            TokenHelper.CreateAppEventClientContext(properties, useAppWeb: false))
        {
            if (clientContext != null)
            {
            }
        }
        return errorMessage;
    }
    
    
  11. 回滚逻辑基本上算是异常处理逻辑,SharePoint CSOM(客户端对象模型)有一个 ExceptionHandlingScope,能够让 Web 服务将异常处理委派给 SharePoint 服务器。(请参阅如何:使用异常处理范围。)

    将以下代码添加到前面代码段中的 if 块。

    ExceptionHandlingScope scope = new ExceptionHandlingScope(clientContext); 
    
    using (scope.StartScope()) 
    { 
        using (scope.StartTry()) 
        { 
        }         
        using (scope.StartCatch()) 
        {                                 
        } 
        using (scope.StartFinally()) 
        { 
        } 
    } 
    clientContext.ExecuteQuery();
    
    if (scope.HasException)
    {
        errorMessage = String.Format("{0}: {1}; {2}; {3}; {4}; {5}", 
            scope.ServerErrorTypeName, scope.ErrorMessage, 
            scope.ServerErrorDetails, scope.ServerErrorValue, 
            scope.ServerStackTrace, scope.ServerErrorCode);
    }
    
  12. 在前面的代码段中,只有一个对 SharePoint (ExecuteQuery) 的调用,但遗憾的是,现在不能只处理这一个。 每一个将在我们的异常作用域中进行引用的对象都必须首先被加载到客户端。

    ExceptionHandlingScope 构造函数的上面添加以下代码。

    ListCollection allLists = clientContext.Web.Lists;
    IEnumerable<List> matchingLists =
        clientContext.LoadQuery(allLists.Where(list => list.Title == listTitle));
    clientContext.ExecuteQuery();
    
    var foundList = matchingLists.FirstOrDefault();
    List createdList = null;
    
  13. 创建主机 Web 列表的代码将加入到 StartTry 块中,但该代码必须首先检查该列表是否已完成添加(如将回滚逻辑和“已完成”逻辑包括在加载项事件处理程序中中所述)。 可通过使用 ConditionalScope 类将 If-then-else 逻辑委派给 SharePoint 服务器(请参阅如何:使用条件范围)。

    将以下代码添加到 StartTry 块中。

    ConditionalScope condScope = new ConditionalScope(clientContext, 
            () => foundList.ServerObjectIsNull.Value == true, true);
    using (condScope.StartScope())
    {
        ListCreationInformation listInfo = new ListCreationInformation();
        listInfo.Title = listTitle;
        listInfo.TemplateType = (int)ListTemplateType.GenericList;
        listInfo.Url = listTitle;
        createdList = clientContext.Web.Lists.Add(listInfo);                                
    }
    
  14. StartCatch 块应撤销创建列表,但需要首先检查是否已创建列表,因为在完成列表创建之前,有可能 StartTry 块中已引发了异常。

    将以下代码添加到 StartCatch 块中。

    ConditionalScope condScope = new ConditionalScope(clientContext, 
            () => createdList.ServerObjectIsNull.Value != true, true);
    using (condScope.StartScope())
    {
        createdList.DeleteObject();
    } 
    

    提示

    疑难解答:要测试 StartCatch 块在应输入时是否已输入,需要一种方法在 SharePoint 服务器上引发运行时异常。 使用 throw 或被零除将不起作用,因为它们在客户端运行时(使用 ExecuteQuery 方法)绑定代码并将其发送到服务器之前引发客户端异常。

    相反,将以下行添加到 StartTry 块。 客户端运行时接受此代码,但它会导致服务器端异常,这正是你想要的。

    List fakeList = clientContext.Web.Lists.GetByTitle("NoSuchList");

    clientContext.Load(fakeList);

整个 TryCreateList 方法应如下所示。 (需要提供 StartFinally 块,即使不使用也需要提供。)

    private string TryCreateList(String listTitle, SPRemoteEventProperties properties)
    {    
        string errorMessage = String.Empty;  

        using (ClientContext clientContext = 
            TokenHelper.CreateAppEventClientContext(properties, useAppWeb: false))
        {
            if (clientContext != null)
            {
                ListCollection allLists = clientContext.Web.Lists;
                IEnumerable<List> matchingLists = 
                    clientContext.LoadQuery(allLists.Where(list => list.Title == listTitle));
                clientContext.ExecuteQuery();
                var foundList = matchingLists.FirstOrDefault();
                List createdList = null;

                ExceptionHandlingScope scope = new ExceptionHandlingScope(clientContext); 
                using (scope.StartScope()) 
                { 
                    using (scope.StartTry()) 
                    { 
                        ConditionalScope condScope = new ConditionalScope(clientContext, 
                                () => foundList.ServerObjectIsNull.Value == true, true);  
                        using (condScope.StartScope())
                        {
                            ListCreationInformation listInfo = new ListCreationInformation();
                            listInfo.Title = listTitle;
                            listInfo.TemplateType = (int)ListTemplateType.GenericList;
                            listInfo.Url = listTitle;
                            createdList = clientContext.Web.Lists.Add(listInfo);
                        }
                    } 
                    
                    using (scope.StartCatch()) 
                    { 
                        ConditionalScope condScope = new ConditionalScope(clientContext, 
                                () => createdList.ServerObjectIsNull.Value != true, true);
                        using (condScope.StartScope())
                        {
                            createdList.DeleteObject();
                        }    
                    } 

                    using (scope.StartFinally()) 
                    { 
                    } 
                } 
                clientContext.ExecuteQuery();

                if (scope.HasException)
                {
                        errorMessage = String.Format("{0}: {1}; {2}; {3}; {4}; {5}", 
                        scope.ServerErrorTypeName, scope.ErrorMessage, 
                        scope.ServerErrorDetails, scope.ServerErrorValue, 
                        scope.ServerStackTrace, scope.ServerErrorCode);
                }
            }
        }
        return errorMessage;
    }

提示

调试:无论是否使用处理程序委派策略,在使用调试程序逐步执行代码时,请记住,无论哪种情况处理程序返回取消状态,SharePoint 都会再次调用处理程序,至多三次。 因此调试程序会循环执行代码,至多四次。

提示

代码体系结构:由于可以在除处理程序之外使用声明性标记在加载项 Web 上安装组件,所以通常不会想要占用处理程序可用来与加载项 Web 进行交互的 30 秒中的任何一秒。 但如果这样做了,请记住,代码需要一个单独的 ClientContext 对象用于加载项 Web。 这意味着加载项 Web 和主机 Web 是不同的组件,就好比 SQL Server 数据库不同于它们中的任何一个一样。 因此,调用加载项 Web 的一个方法在 AppInstalled case 块的 try 块中,就像后续示例中的 TryCreateList 方法一样。 但是,处理程序 不需要 回滚在加载项 Web 上执行的操作。 如果遇到错误,只需要取消事件,因为取消事件后,SharePoint 会删除整个加载项 Web。

创建加载项卸载事件接收器

  1. 将项目的“处理正在卸载的加载项”属性设置为“True”。 如果已存在一个 Web 服务文件,这些工具会再创建另一个 Web 服务文件;但它们会将一个 UninstallingEventEndpoint 元素添加到加载项清单中。

  2. 在将加载项从第二阶段回收站中删除之后,AppUninstalling case 块中的代码应删除不需要的加载项项目,因为它们会触发该事件。 但是,有时可能需要“停用”组件,而不是完全删除它们。 这是因为,如果需要回滚卸载事件,则需要将其还原。 如果发生这种情况,加载项仍会保留在第二阶段回收站中,并且用户可以将其还原并重新开始使用它。 只要用回滚逻辑重新创建已删除的组件可能足以使加载项再次工作,但组件中的任何数据或配置设置都将丢失。

    该策略对 SharePoint 组件来说相对容易,因为 SharePoint 的回收站中的内容可以还原,并且还有 CSOM API 可对其进行访问。 此过程的后续步骤会演示操作方法。 对于其他平台,或许需要用到其他技术。 例如,如果你想停用加载项卸载处理程序的 SQL Server 表中的某一行,该处理程序中的 T-SQL 存储过程可以向表中添加 IsDeleted 列,并针对该行将其设置为“True”。 如果该过程遇到错误,回滚逻辑会将值重置为“False”。 如果过程成功完成且未出现任何错误,在它返回成功标志之前,可以设置一个计时器作业在以后删除该行。

    有时,即使是在删除了该加载项之后,也想要保留数据(如列表);但是,本文中的以下示例是一个卸载事件处理程序,它删除使用已安装事件处理程序创建的列表。

    case SPRemoteEventType.AppUninstalling:
    
    try
    {
        string error = TryRecycleList(listTitle, properties);
        if (error != String.Empty)
        {
            throw new Exception(error);
        }
    }
    catch (Exception e)
    {
        // Tell SharePoint to cancel the event.
        result.ErrorMessage = e.Message;
        result.Status = SPRemoteEventServiceStatus.CancelWithError;
    }
    break;
    
  3. 添加用来回收列表的帮助程序方法。 请注意以下关于此代码的内容:

    • 该代码回收列表,而不是永久删除列表。 这样一来,如果事件失败了,就可以还原列表,包括列表中的数据,而这也正是 StartCatch 块所做的事情。 所以,如果该方法成功,并且该事件完成,会将该外接程序从第二阶段回收站中永久删除,但该列表仍然位于第一阶段回收站中。

    • 在执行回收之前,代码会测试列表是否存在,因为用户可能已在 SharePoint UI 中对其进行了回收。 同样,在执行还原之前,回滚代码会检查回收站中的列表是否存在,因为用户可能已对其进行了还原,或将其移动到第二阶段回收站中。

    • 有两个条件范围,它们通过检查对列表的引用是否为 null 来测试列表是否存在。 但这两个范围都有一个内部 if 块,可用来测试再次无效的同一个对象。 包含条件范围块的外部测试在服务器上运行,而内部无效测试仍是需要的。 这是因为,客户端运行时通过逐行执行代码来创建 ExecuteQuery 方法将发送至服务器的 XML 消息。 当遇到对 foundListrecycledList 对象的引用时,这些代码行的个别行会引发“空引用”异常,除非它们被保存在内部无效检查中。

      private string TryRecycleList(String listTitle, SPRemoteEventProperties properties)
      {
          string errorMessage = String.Empty;
      
          using (ClientContext clientContext = 
              TokenHelper.CreateAppEventClientContext(properties, useAppWeb: false))
          {
              if (clientContext != null)
              {
                  ListCollection allLists = clientContext.Web.Lists;
                  IEnumerable<List> matchingLists = 
                      clientContext.LoadQuery(allLists.Where(list => list.Title == listTitle));
                  RecycleBinItemCollection bin = clientContext.Web.RecycleBin;
                  IEnumerable<RecycleBinItem> matchingRecycleBinItems = 
                      clientContext.LoadQuery(bin.Where(item => item.Title == listTitle));        
                  clientContext.ExecuteQuery();
      
                  List foundList = matchingLists.FirstOrDefault();
                  RecycleBinItem recycledList = matchingRecycleBinItems.FirstOrDefault();    
      
                  ExceptionHandlingScope scope = new ExceptionHandlingScope(clientContext);
                  using (scope.StartScope())
                  {
                      using (scope.StartTry())
                      {
                          ConditionalScope condScope = new ConditionalScope(clientContext, 
                              () => foundList.ServerObjectIsNull.Value == false, true);
                          using (condScope.StartScope())
                          {
                              if (foundList != null)
                              {
                                  foundList.Recycle();
                              }
                          }
                      }
                      using (scope.StartCatch())
                      {
                          ConditionalScope condScope = new ConditionalScope(clientContext, 
                              () => recycledList.ServerObjectIsNull.Value == false, true);
                          using (condScope.StartScope())
                          {
                              if (recycledList != null)
                              {
                                  recycledList.Restore(); 
                              }
                          }
                      }
                      using (scope.StartFinally())
                      {
                      }
                  }
                  clientContext.ExecuteQuery();
      
                  if (scope.HasException)
                  {
                      errorMessage = String.Format("{0}: {1}; {2}; {3}; {4}; {5}", 
                          scope.ServerErrorTypeName, scope.ErrorMessage, 
                          scope.ServerErrorDetails, scope.ServerErrorValue, 
                          scope.ServerStackTrace, scope.ServerErrorCode);
                  }
              }
          }
          return errorMessage;
      }
      

调试和测试加载项卸载事件接收器

  1. 在单独的窗口或选项卡中打开以下页面:

    • 网站内容
    • 网站设置 - 回收站 (_layouts/15/AdminRecycleBin.aspx?ql=1)
    • 回收站 - 第二阶段回收站 (_layouts/15/AdminRecycleBin.aspxView=2&?ql=1)
  2. 出现提示时,选择 F5 并信任加载项。 加载项的起始页将打开。 如果仅打算测试卸载处理程序,可以关闭此浏览器窗口。 但是,如果要调试处理程序,请将其保持打开状态。 关闭它将结束调试会话。

  3. 刷新“网站内容”页,并在出现该加载项时,将其删除。

  4. 刷新“网站设置 - 回收站”页。 加载项显示为顶部项。 选择它旁边的复选框,然后单击“删除所选内容”

  5. 刷新“回收站 - 第二阶段回收站”页。 加载项显示为顶部项。 选择它旁边的复选框,然后单击“删除所选内容”。 SharePoint 会立即调用加载项卸载处理程序。

创建加载项更新事件接收器

有关创建加载项更新处理程序的详细信息,请参阅在 SharePoint 加载项中创建更新事件的处理程序

生产加载项事件接收器的 URL 和托管限制

远程事件接收器可以托管在云中或还未用作 SharePoint 服务器的本地服务器上。 生产接收器的 URL 不能指定特定端口。 这意味着必须为 HTTPS 使用我们建议的端口 443,或为 HTTP 使用端口 80。 如果使用的是 HTTPS 且接收器服务托管在本地,但加载项位于 SharePoint Online 上,则托管服务器必须具有来自证书颁发机构的公开受信任的证书。 (只有在加载项位于本地 SharePoint 场时,自签名证书才可用。)

另请参阅