Crear un receptor de eventos de complemento en Complementos de SharePoint

Es útil si primero tiene conocimientos de los complementos de SharePoint hospedados por el proveedor y para que haya desarrollado algunos que vayan al menos un poco más allá del nivel "Hola mundo". Vea Introducción a la creación de complementos de SharePoint hospedados por el proveedor.

Además, debe estar familiarizado con la sección Controlar eventos en los complementos de SharePoint.

Obtener más ejemplos de código

Si trabaja con el ejemplo continuo de este artículo, tendrá un ejemplo de código terminado. Los siguientes son algunos otros ejemplos. No todos siguen la arquitectura descrita en este artículo. Puede haber más de una forma buena de diseñar un receptor de eventos de complemento; tenga en cuenta también que las instrucciones que ofrece Microsoft pueden evolucionar con el tiempo.

Agregar un receptor de eventos instalado por el complemento

  1. En Visual Studio, abra el proyecto para la Complemento de SharePoint hospedada por proveedor. (Si agrega un controlador de evento de complemento a un complemento hospedado por SharePoint, Office Developer Tools para Visual Studio la convertirá en una aplicación hospedada por proveedor).

  2. En el Explorador de soluciones, seleccione el nodo del complemento de SharePoint.

  3. En la ventana Propiedades, establezca el valor de Complemento de identificador instalado en True.

    Eventos de aplicación en la ventana de propiedades

    Office Developer Tools para Visual Studio realizará las siguientes acciones:

    • Agregue un archivo denominado AppEventReceiver.svc que contenga código C# (o VB.NET) esquelético. Este es el servicio que controla el evento del complemento.

    • Agregue la siguiente entrada a la sección Propiedades del archivo AppManifest.xml: <InstalledEventEndpoint>~remoteAppUrl/AppEventReceiver.svc</InstalledEventEndpoint>. Esta entrada registra el receptor de evento de complemento a SharePoint.

      Nota:

      El token ~remoteAppUrl es el mismo que se usa para la aplicación web remota en el complemento de SharePoint hospedado por el proveedor. Office Developer Tools para Visual Studio asume que el domino de la aplicación web y el controlador evento es el mismo. En el caso poco frecuente en el que no lo es, debe reemplazar manualmente el token ~remoteAppUrl por el dominio real del servicio.

    • Cree un proyecto web si el proyecto de complemento de SharePoint aún no tiene uno. Las herramientas también garantizan que un manifiesto de complemento esté configurado para un complemento hospedado por el proveedor. También agregan páginas, scripts, archivos CSS y otros artefactos. Si el único componente remoto que necesita el complemento es el servicio web de control de eventos, puede eliminarlos del proyecto. También debe asegurarse de que el elemento StartPage del manifiesto del complemento no apunte a una página que haya eliminado.

  4. Si la granja de sharePoint de prueba no está en el mismo equipo que ejecuta Visual Studio, configure el proyecto para la depuración mediante el Microsoft Azure Service Bus. Para obtener más información, vea Depurar y solucionar problemas de un receptor de eventos remotos en un complemento de SharePoint.

  5. Si hay un método ProcessOneWayEvent en el archivo AppEventReceiver.svc, su implementación solo debe contener la línea throw new NotImplementedException(); porque este método no puede usarse en un controlador de eventos de complemento.

    Los controladores de eventos de complemento deben devolver un objeto que le indica a SharePoint si finalizar o revertir el evento y el método ProcessOneWayEvent no devuelve nada.

  6. El archivo incluye un método que tiene un ProcessEvent aspecto similar al siguiente. (También puede haber un bloque de código que ilustra cómo obtener un contexto de cliente. Elimínelo o anótelo).

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

    Tenga en cuenta lo siguiente en relación con este código:

    • El objeto SPRemoteEventProperties se envía al servicio web del controlador como mensaje SOAP que contiene información contextual de SharePoint, incluida una propiedad EventType que identifica al evento.

    • El objeto SPRemoteEventResult que devuelve el controlador contiene una propiedad Status cuyos valores posibles son SPRemoteEventServiceStatus.Continue, SPRemoteEventServiceStatus.CancelNoError y SPRemoteEventServiceStatus.CancelWithError. El valor predeterminado de la propiedad Status es Continue, que indica a SharePoint que finalice el evento. Los otros dos valores indican a SharePoint que haga lo siguiente:

      • Ejecute el controlador hasta tres veces más.
      • Cancele el evento y revierta los cambios realizados como parte del evento si sigue obteniéndose un estado de cancelado.
  7. Inmediatamente debajo de la línea donde se declara la variable result, agregue la siguiente estructura de modificador para identificar el evento que se controla.

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

    Nota:

    Si tiene controladores para los eventos AppInstalled, AppUpdated y AppInstalling , cada uno obtiene su propia dirección URL registrada en el manifiesto del complemento. Por lo tanto, puede tener puntos de conexión diferentes para ellos, pero en este artículo (y en Office Developer Tools para Visual Studio) se supone que tienen exactamente el mismo punto de conexión; es por eso que el código debe determinar qué evento lo llamó.

  8. Como se explica en Incluir la lógica de reversión y la lógica de "acciones realizadas" en los controladores de eventos de complemento, si se producen errores en la lógica de instalación, casi siempre tendrá que anular la instalación del complemento; además, necesitará que SharePoint revierta las acciones de instalación y las acciones realizadas por el controlador.

    Una forma de lograr estos objetivos consiste en agregar el código siguiente en el bloque case del evento de AppInstalled.

    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;
    

    Nota:

    Mueva el código de instalación que tarda más de 30 segundos en el complemento mismo. Puede agregarlo a la lógica de “primera ejecución” que se ejecuta la primera vez que se inicia el complemento. El complemento puede mostrar un mensaje que dice algo como "Estamos preparando las cosas para usted". Como alternativa, el complemento puede pedir al usuario que ejecute el código de inicialización.

    Si la lógica de "primera ejecución" no es factible para el complemento, otra opción es hacer que el controlador de eventos inicie un proceso asincrónico remoto y, a continuación, devuelva inmediatamente un objeto SPRemoteEventResultcon status establecido en Continue. Un punto débil de esta estrategia es que, si el proceso remoto falla, no tiene forma de indicar a SharePoint que revierta la instalación del complemento.

  9. Como se explica en Estrategias de arquitectura del controlador de eventos de complemento, se prefiere la estrategia de delegación de controladores, aunque no es posible en todos los escenarios. En el ejemplo de este artículo, le mostramos cómo implementar la estrategia de delegación de controlador a la hora de agregar una lista a la web de hospedaje. Para obtener información sobre cómo crear un controlador de eventos AppInstalled similar que no use la estrategia de delegación de controladores, vea el ejemplo SharePoint/PnP/Samples/Core.AppEvents.

    La siguiente es la nueva versión del bloque case de AppInstalled. Tenga en cuenta la lógica de inicialización que aplica todos los eventos que van más allá del bloque switch. Dado que la misma lista instalada se quita en el controlador AppUninstalling, la lista se identifica allí.

    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. Agregue el método de creación de listas a la clase AppEventReceiver como método private con el siguiente código. Tenga en cuenta que la clase TokenHelper tiene un método especial optimizado para obtener un contexto de cliente para un evento de complemento. Pasar false para el último parámetro garantiza que el contexto sea para la web de hospedaje.

    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. La lógica de reversión es básicamente una lógica de control de excepciones y el modelo de objetos del lado cliente (CSOM) de SharePoint tiene un objeto ExceptionHandlingScope que permite al servicio web delegar el control de excepciones al servidor de SharePoint (vea Procedimiento para usar el ámbito de control de excepciones).

    Agregue el código siguiente al bloque if del fragmento de código anterior.

    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. Existe solo una llamada a SharePoint (ExecuteQuery) en el fragmento de código anterior, pero lamentablemente no nos alcanza. Todo objeto al que se haga referencia en nuestro ámbito de excepción primero debe cargarse en el cliente.

    Agregue el código siguiente encima del constructor del objeto 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. El código para crear una lista web de host entra en el bloque StartTry , pero el código debe comprobar primero si la lista ya se ha agregado (como se explica en Incluir lógica de reversión y lógica "ya hecha" en los controladores de eventos del complemento). La lógica if-then-else se puede delegar en el servidor de SharePoint mediante la clase ConditionalScope (vea How to: Use Conditional Scope).

    Agregue el código siguiente en el bloque 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. El bloque StartCatch debe deshacer la creación de la lista, pero primero debe comprobar que se haya creado la lista, porque puede haberse producido una excepción en el bloque StartTry antes de que termine de crear la lista.

    Agregue el código siguiente al bloque StartCatch.

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

    Sugerencia

    SOLUCIÓN DE PROBLEMAS: Para probar si el bloque StartCatch se especifica cuando debería, necesita una manera de iniciar una excepción en tiempo de ejecución en el servidor de SharePoint. El uso de un valor throw o dividiendo por cero no funcionará porque provoca excepciones del lado cliente antes de que el tiempo de ejecución del cliente pueda incluso agrupar el código y enviarlo al servidor (con el método ExecuteQuery ).

    En cambio, agregue las siguientes líneas al bloque StartTry. El tiempo de ejecución en el cliente acepta esto, pero genera una excepción en el servidor, que es lo que usted necesita.

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

    clientContext.Load(fakeList);

Todo el método TryCreateList debe ser similar a lo siguiente. (El bloque StartFinally es necesario incluso cuando no se usa).

    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;
    }

Sugerencia

DEPURACIÓN: Independientemente de si usa la estrategia de delegación de controladores, al recorrer el código con el depurador, tenga en cuenta que, en cualquier escenario en el que el controlador devuelva un estado de cancelación, SharePoint volverá a llamar al controlador, hasta tres veces más. Por lo tanto, el depurador recorre el código hasta cuatro veces.

Sugerencia

ARQUITECTURA DE CÓDIGO: Dado que puede instalar componentes en la web del complemento con marcado declarativo fuera del controlador, normalmente no querrá usar ninguno de los 30 segundos que el controlador tiene disponible para interactuar con la web del complemento. Si lo hace, recuerde que el código requiere un objeto ClientContext por separado para la web de complemento. Esto significa que la web del complemento y la web host son componentes diferentes, tanto como una base de datos SQL Server es diferente de cada uno de ellos. Por lo que un método que llama a la web de complemento está en el bloque try del bloque AppInstalled case, tal como el método TryCreateList en el ejemplo que continúa. Sin embargo, el controlador no necesita revertir acciones realizadas en la web de complemento. Si encuentra un error, solo necesita cancelar el evento, ya que SharePoint elimina toda la web de complemento si el evento se cancela.

Crear un receptor de eventos de desinstalación de complemento

  1. Establezca la propiedad Desinstalación del complemento de controlador del proyecto en True. Las herramientas no crean otro archivo de servicio web si ya existe, pero agregan un elemento UninstallingEventEndpoint al manifiesto del complemento.

  2. El código del bloque de casos AppUninstalling debe quitar los artefactos del complemento que no son necesarios después de quitar el complemento de la papelera de reciclaje de la segunda fase, que es lo que desencadena el evento. Sin embargo, siempre que sea posible, debe "retirar" los componentes en lugar de eliminarlos totalmente. Esto se debe a que debe restaurarlos si el evento de desinstalación tiene que revertirse. Si esto sucede, el complemento sigue en la papelera de reciclaje de la segunda fase y un usuario podría restaurarlo y empezar a usarlo de nuevo. Simplemente volver a crear un componente eliminado en la lógica de reversión podría ser suficiente para permitir que el complemento vuelva a funcionar, pero se perderían los datos o la configuración del componente.

    Esta estrategia es relativamente fácil para los componentes de SharePoint, ya que SharePoint tiene una papelera de reciclaje desde la que se pueden restaurar las cosas y hay API de CSOM para acceder a ella. Los pasos posteriores de este procedimiento muestran cómo hacerlo. Para otras plataformas, es posible que se necesiten técnicas diferentes. Por ejemplo, si desea retirar una fila de una tabla de SQL Server en el controlador de desinstalación de complementos, un procedimiento almacenado de T-SQL en el controlador puede agregar una columna IsDeleted a la tabla y establecerla en True para la fila. Si el procedimiento encuentra un error, la lógica de reversión restablece el valor a False. Si el procedimiento se completa sin errores, justo antes de que devuelva una marca de operación correcta, puede establecer un trabajo de temporizador para eliminar la fila más adelante.

    A veces es necesario conservar los datos, como las listas, incluso una vez eliminado el complemento; pero como ejemplo para este artículo, a continuación se muestra un controlador de eventos de desinstalación que elimina la lista que se creó con el controlador de eventos instalado.

    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. Agregue el método de aplicación auxiliar para reciclar la lista. Tenga en cuenta lo siguiente sobre este código:

    • El código recicla la lista, en lugar de eliminarla de forma permanente. Esto hace que sea posible restaurarla, con sus datos, si falla el evento, que es lo que hace el bloque StartCatch. Por lo tanto, si el método es correcto y se completa el evento, el complemento se elimina de forma permanente de la papelera de reciclaje de la segunda etapa, pero la lista aún se encuentra en la papelera de reciclaje de la primera etapa.

    • El código prueba la existencia de la lista antes de reciclarla porque un usuario ya podría haberla reciclado en la interfaz del usuario de SharePoint. De modo similar, el código de reversión comprueba la existencia de la lista en la papelera de reciclaje antes de restaurarla, porque un usuario ya podría haberla restaurado o movido a la papelera de reciclaje de la segunda etapa.

    • Existen dos ámbitos condicionales que prueban la existencia de una lista al comprobar si hay una referencia a ella en null. Pero ambos tienen un bloque if interno que comprueba la nullidad del mismo objeto por segunda vez. El externo prueba, con bloques de ámbito condicionales, la ejecución en el servidor, pero también se necesitan las pruebas de nulidad interna. Esto se debe a que el tiempo de ejecución del cliente se mueve a través del código línea a línea para crear el mensaje XML que el método ExecuteQuery envía al servidor. Cuando se alcanzan las referencias a los objetos foundList y recycledList, una u otra de estas líneas produce una excepción de referencia nula, a menos que estén dentro de las comprobaciones de nulidad interna.

      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;
      }
      

Para depurar y probar un receptor de eventos de desinstalación de complemento

  1. Abra las páginas siguientes en distintas ventanas o pestañas:

    • Contenidos del sitio
    • Configuración del sitio: Papelera de reciclaje (_layouts/15/AdminRecycleBin.aspx?ql=1)
    • Papelera de reciclaje: Papelera de reciclaje de segundo nivel (_layouts/15/AdminRecycleBin.aspxView=2&?ql=1)
  2. Seleccione F5 y confíe en el complemento cuando se le solicite. Se abre la página de inicio del complemento. Si solo va a probar el controlador de desinstalación, puede cerrar esta ventana del explorador. Pero si va a depurar el controlador, déjelo abierto. Cerrarla finalizará la sesión de depuración.

  3. Actualice la página Contenido del sitio y, cuando aparezca el complemento, elimínelo.

  4. Actualice la página Configuración del sitio: Papelera de reciclaje. El complemento aparece como el elemento superior. Active la casilla que aparece al lado y haga clic en Eliminar selección.

  5. Actualice la página Papelera de reciclaje: Papelera de reciclaje de la segunda etapa. El complemento aparece como el elemento superior. Active la casilla que aparece al lado y haga clic en Eliminar selección. SharePoint llama inmediatamente al controlador de desinstalación del complemento.

Crear un receptor de eventos de actualización de complemento

Para obtener más información sobre cómo crear un controlador de eventos de actualización de complemento, vea Crear un controlador para el evento de actualización de complementos de SharePoint.

Restricciones de hospedaje y URL en receptores de eventos de complementos de producción

El destinatario del evento remoto puede estar hospedado en la nube o en un servidor local que no se use también como servidor de SharePoint. La dirección URL de un receptor de producción no puede especificar un puerto en concreto. Esto significa que se debe usar el puerto 443 para HTTPS (recomendado) o el puerto 80 para HTTP. Si usa HTTPS y el servicio del receptor se hospeda en el entorno local, pero el complemento está en SharePoint Online, el servidor host debe tener un certificado de confianza pública de una entidad de certificación. (Un certificado autofirmado solo funciona si el complemento está en una granja de SharePoint local).

Vea también