サンドボックス ソリューションのイベント レシーバーを、リモート イベント レシーバーに置き換える方法

こんにちは。SharePoint サポートの森 健吾 (kenmori) です。

2019.02.18 追記 事前注意
リモート イベント レシーバーは構造上、SharePoint からアプリケーションに対して、イベント通知をリトライしません。
イベント通知失敗による処理漏れを防ぐため、必ず変更ログを使用してイベント通知を補完する実装が必要です。また、この方法を利用し、HTTP 調整の一つの大きな要因となる並列多重実行を抑える必要があります。

導入

コード付きサンドボックス ソリューションを代替のソリューションに置き換える開発を実施されている方が多いと考えております。サンドボックス ソリューションの変換先としては、以下のようなガイダンスが記載されています。

タイトル : サンドボックス ソリューションの変換ガイダンス アドレス : https://msdn.microsoft.com/ja-jp/pnp_articles/sandbox-solution-transformation-guidance

上記ガイダンスにからリンクしていくと、以下のページにイベント レシーバーの代替としてリモート イベント レシーバーの使用が紹介されています。

タイトル : SharePoint でリモート イベント レシーバーを使用する
アドレス : https://msdn.microsoft.com/ja-jp/pnp_articles/use-remote-event-receivers-in-sharepoint

今回の投稿では、上記内容を少しわかりやすく説明することと、既存のサンドボックス ソリューション、カスタムリスト定義に近い形で置き換えやすいサンプルに仕上げましたので、その内容をご提案します。

なお、リモート イベント レシーバーという仕組みを使う上で運用的に重要な点が 1 点あります。リモート イベント レシーバーは SharePoint Online から直接ネットワーク接続できる場所にイベント処理用の外部 Web サイトを用意する必要があります。この時点で、Azure Web App などのクラウド ベースの Web サイトや、インターネット公開したオンプレミスの Web サイトが必要になり、構成変更追加コストが生じる点についてはあらかじめご了承ください。

 

リモート イベント レシーバーへの置き換えを理解するポイント

サンドボックスのイベント レシーバーをプロバイダーホスト型アドインのリモート イベント レシーバーに置き換える上で技術的なポイントが 2 点あります。

remoteeventreceiver01-2

1) アプリ イベント レシーバー (AppEventReceiver.svc) エンドポイントの使用

アプリ イベント レシーバー エンドポイントは、ホスト サイト (HostWeb) 上でアドインのインストール、アンインストールなどに関連するイベントを処理するために生成されます。このエンドポイントを他のアイテム イベントなどにも使用することができます。

2) アプリ コンテキストで生成された ClientContext オブジェクトを使用して、イベント レシーバーを関連付ける。

SharePoint Online では、リモート イベント レシーバー エンドポイントに対してはホスト サイト側のユーザー コンテキスト情報が送信できるような実装になっています。そのための条件として、プロバイダー ホスト アドイン側で ContextToken から生成した ClientContext のインスタンス (CSOM) を使用してリストにリモート イベント  レシーバーを関連付ける (List.EventReivers.Add) ことが条件となります。

上記ガイダンスのページでは、AppInstalled イベントでリモート イベント レシーバーを関連付けるサンプルが紹介されており、この方法が推奨された方法となります。 その他、プロバイダー ホスト アプリの画面処理や、コンテキスト付きで呼び出されたリスト イベント レシーバーなどで関連付けても、コンテキスト付きでアイテム イベント レシーバーが呼び出せます。

 

 

実践

それでは、実際にプロバイダー ホスト型アドインを開発していきます。投稿をコンパクトにするため、プロバイダー ホスト型アドインの開発経験者が前提の記載となりますが、ご了承ください。

1. プロバイダー ホスト型アドインの追加

1-1. Visual Studio で SharePoint アドイン プロジェクトを作成し、プロバイダー ホスト型アドインを作成します。

1-2. ソリューション エクスプローラで、プロバイダー ホスト アドイン プロジェクトを選択し、プロパティ シートで "アプリのハンドルがインストールされました" を True にします。

remoteeventreceiver02

以下のように、外部ホスト Web サイト内にアプリ イベント レシーバー エンドポイントが自動生成されます。

remoteeventreceiver03

1-3. AppEventReceiver.svc.cs を開き、ProcessEvent にロジックを実装します。

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

    switch (properties.EventType)
    {
        // 必須イベント
        case SPRemoteEventType.AppInstalled:
             HandleAppInstalled(properties);
             break;
         case SPRemoteEventType.ListAdded:
             HandleListAdded(properties);
             break;
         // カスタム イベント
         case SPRemoteEventType.ItemAdding:
             HandleItemAdding(properties, result);
             break;
     }
     return result;
}

1-4. アプリのインストール イベント (AppInstalled) を実装します。
・既存リスト (ListTemplateId=10011) に対してアイテム イベント レシーバーを取り付けます。
・新規リストに対して、該当リスト (ListTemplateId = 10011) へのアイテム イベント レシーバーを取り付けるための、リスト イベント レシーバーを取り付けます。

 // 定数群
private const string RECEIVER_LIST_ADDED = "RERRecommended_ListAdded";
private const string RECEIVER_ITEM_ADDING = "RERRecommended_ItemAdding";
private const string RECEIVER_ITEM_ADDED = "RERRecommended_ItemAdded";
private const int ListTemplateId = 10011;

private void HandleAppInstalled(SPRemoteEventProperties properties)
{
    using (ClientContext clientContext = TokenHelper.CreateAppEventClientContext(properties, false))
    {
        if (clientContext != null)
        {
            // 新規リスト作成時のアイテム イベント レシーバー関連付け処理
            AssociateListEventsToHostWeb(clientContext);
            // 既存リストへのアイテム イベント レシーバー関連付け処理
            AssociateItemEventsToExistingList(clientContext);
        }
    }
}


public void AssociateListEventsToHostWeb(ClientContext clientContext)
{
    Web hostWeb = clientContext.Web;
    clientContext.Load(hostWeb, web => web.EventReceivers);
    clientContext.ExecuteQuery();

    bool rerExists = false;
    if (null != hostWeb)
    {
        foreach (var rer in hostWeb.EventReceivers)
        {
            if (rer.ReceiverName == RECEIVER_LIST_ADDED)
            {
                rerExists = true;
            }
        }
    }
    if (!rerExists)
    {
        EventReceiverDefinitionCreationInformation receiver = new EventReceiverDefinitionCreationInformation();
        receiver.EventType = EventReceiverType.ListAdded;
        receiver.ReceiverUrl = OperationContext.Current.RequestContext.RequestMessage.Headers.To.ToString();
        receiver.ReceiverName = RECEIVER_LIST_ADDED;
        receiver.Synchronization = EventReceiverSynchronization.Synchronous;

        hostWeb.EventReceivers.Add(receiver);
        clientContext.ExecuteQuery();
    }
}

public void AssociateItemEventsToExistingList(ClientContext clientContext)
{
    clientContext.Load(clientContext.Web.Lists,
    lists => lists.Include(
    list => list.Title,
    list => list.EventReceivers).Where(list => list.BaseTemplate == ListTemplateId));
    clientContext.ExecuteQuery();

    foreach (List list in clientContext.Web.Lists)
    {
        // アイテム イベント レシーバーの関連付け
        AssociateItemEventsToHostList(clientContext, list);
    }
}

1-5.  リスト イベント  (新規リスト作成時に、条件付きでアイテム イベント レシーバーを取り付け) の処理を実装します。

 private void HandleListAdded(SPRemoteEventProperties properties)
{
    using (ClientContext clientContext = TokenHelper.CreateRemoteEventReceiverClientContext(properties))
    {
        if (clientContext != null)
        {
            List hostList = clientContext.Web.Lists.GetById(properties.ListEventProperties.ListId);
            clientContext.Load(hostList, lst => lst.BaseTemplate, lst => lst.EventReceivers);
            clientContext.ExecuteQuery();
            if (hostList.BaseTemplate == ListTemplateId)
            {
                // アイテム イベント レシーバーの関連付け
                AssociateItemEventsToHostList(clientContext, hostList);
            }
        }
    }
}

1-6. 既存リストと新規リストに対して呼び出されるアイテム イベント レシーバーの取り付け処理を実装します。
アイテム更新イベント・削除イベントなどを取り付ける場合は、このコードを増やしてください。

 public void AssociateItemEventsToHostList(ClientContext clientContext, List hostList)
{
    bool rerExists = false;
    if (null != hostList)
    {
        foreach (var rer in hostList.EventReceivers)
        {
            if (rer.ReceiverName == RECEIVER_ITEM_ADDED)
            {
                rerExists = true;
            }
        }
    }
    if (!rerExists)
    {
        // ItemAdding
        EventReceiverDefinitionCreationInformation receiver1 = new EventReceiverDefinitionCreationInformation();
        receiver1.EventType = EventReceiverType.ItemAdding;
        receiver1.ReceiverUrl = OperationContext.Current.RequestContext.RequestMessage.Headers.To.ToString();
        receiver1.ReceiverName = RECEIVER_ITEM_ADDING;
        receiver1.Synchronization = EventReceiverSynchronization.Synchronous;
        hostList.EventReceivers.Add(receiver1);

        // ItemAdded
        EventReceiverDefinitionCreationInformation receiver2 = new EventReceiverDefinitionCreationInformation();
        receiver2.EventType = EventReceiverType.ItemAdded;
        receiver2.ReceiverUrl = OperationContext.Current.RequestContext.RequestMessage.Headers.To.ToString();
        receiver2.ReceiverName = RECEIVER_ITEM_ADDED;
        receiver2.Synchronization = EventReceiverSynchronization.Asynchronous;
        hostList.EventReceivers.Add(receiver2);

        clientContext.ExecuteQuery();
    }
}

1-7. カスタムの ItemAdding イベント (同期) を実装します。手順 1-3. に記載の通り HandleItemAdded の呼び出しはすでにコーディングされています。

 private void HandleItemAdding(SPRemoteEventProperties properties, SPRemoteEventResult result)
{
    try
    {
        using (ClientContext clientContext = TokenHelper.CreateRemoteEventReceiverClientContext(properties))
        {
            if (properties.ItemEventProperties.AfterProperties["Title"] != null &&
              !properties.ItemEventProperties.AfterProperties["Title"].ToString().ToLower().StartsWith("test"))
            {
                throw new Exception("Title should start with 'TEST'.");
            }
            else
            {
                result.Status = SPRemoteEventServiceStatus.Continue;
            }
        }
    }
    catch (Exception ex)
    {
        result.Status = SPRemoteEventServiceStatus.CancelWithError;
        result.ErrorMessage = ex.Message;
    }
}
 

1-8. カスタムの ItemAdded イベント (非同期) を実装します。ProcessOneWayEvent に追記します。

 public void ProcessOneWayEvent(SPRemoteEventProperties properties)
{
    // Asynchronous イベントはこちら
    switch (properties.EventType)
    {
        case SPRemoteEventType.ItemAdded:
            HandleItemAdded(properties);
            break;
    }
}

private void HandleItemAdded(SPRemoteEventProperties properties)
{
    using (ClientContext clientContext = TokenHelper.CreateRemoteEventReceiverClientContext(properties))
    {
        if (clientContext != null)
        {
            List photos = clientContext.Web.Lists.GetById(properties.ItemEventProperties.ListId);
            ListItem item = photos.GetItemById(properties.ItemEventProperties.ListItemId);
            clientContext.Load(item);
            clientContext.ExecuteQuery();

            item["Description"] += "\nUpdated by RER " + System.DateTime.Now.ToLongTimeString();
            item.Update();
            clientContext.ExecuteQuery();
        }
    }
}

1-9. プロバイダー ホスト型アドインのプロジェクト内にある AppManifest.xml にて、[アクセス許可] をクリックし、Web に FullControl など (今回のサンプルでは Read でも可) を付与します。

remoteeventreceiver04

1-10. 外部ホスト Web サイトを公開し、プロバイダー ホスト型アドインの *.app ファイルを生成します。
一般的な展開手順 (例. Azure Web App への展開) は以下のサイトにまとめておりますので、ご参考にしてください。

タイトル : プロバイダー ホスト型アドインを Azure Web App として展開する方法
アドレス: https://blogs.technet.microsoft.com/sharepoint_support/2016/08/27/how-to-deploy-provider-hosted-addin-web-to-azure-web-app/

なお、確認のポイントとして、出来上がりの AppManifest.xml をコードで開くと以下のようになります。

remoteeventreceiver05

2. サンドボックス ソリューション側の変更点

イベント レシーバーを実装している Elements.xml にて、Assembly や Class などのReceiver の子要素を削除し、代わりに以下のように Url 子要素を指定します。

2-1. 既存のサンドボックス ソリューションをコピーして、開きます。
2-2. イベントレシーバーの定義をすべて削除します。

remoteeventreceiver_sandbox

2-3. サンドボックス ソリューションのプロジェクトのプロパティにて、"パッケージにアセンブリを含める" を False に指定します。

remoteeventreceiver07-2

2-5. [ビルド] – [配置] をクリックして、コードなしのソリューション パッケージ (*.wsp) を生成します。

上記のように既存のサンドボックス ソリューションからコードを削除して、レシーバー URL をアドインのリモート レシーバーに指定することで、既存のソリューションを置き換えることができます。

動作確認

サンプルコードの実装通り、TEST で始まらないタイトルを持つアイテムの投稿をブロックすることができました。

remoteeventreceiver08

※ 当該アドインをアンインストール時に、このアドインによって取り付けられたイベントを削除する必要がある場合は、アンインストール イベントも実装いただく必要があります。今回の記事では、割愛しています。

まとめ

上記実装方法としては、既存のサンドボックス ソリューションの置き換えやすさを考慮した 1 つの例となります。仕組みを一から考え直す場合は、前半にて記載した 2 つのポイントのみ認識を合わせれば、色々なパターンの実装が可能と考えております。是非、ご参考にしていただき、お役立てください。