Как стать автором
Обновить

Sharepoint 2010 / кастомизация оповещений поиска (search alerts)

Время на прочтение 15 мин
Количество просмотров 2.1K
Хочу рассказать о собственном успешном опыте кастомизации оповещений поиска (search alerts) в Sharepoint 2010 с помощью IAlertNotifyHandler.

Вначале задача показалась тривиальной. Вроде как все просто. В документации MSDN есть статья описывающая сам процесс, есть примеры и на других ресурсах. Но, как оказалось, везде описывается кастомизация любых типов оповещений, кроме поисковых. Так как во всем интернете я не нашел решения — представляю свое (может и не самое оптимальное, но зато рабочее).

Условие


Имеется база данных SQL, поиск по которой средствами Sharepoint реализован с использованием Business Connectivity Services (BCS) и Enterprise Search. Вывод результатов поиска пользователю Sharepoint кастомизирован с помощью соответствующих преобразований XSLT. Таким образом, пользователь при поиске получает результат примерно в таком виде (база рабочая — здесь и далее имена и все такое вычеркнуты).



Средствами Sharepoint пользователи могут для каждого поискового запроса назначать оповещения, которые срабатывают при внесении изменений в базу данных. При срабатывании пользователь получает email примерно следующего вида.



То есть стандартное оповещение не учитывает трансформаций XSLT и выдает пользователю нечитабельную информацию. Поэтому возникает задача преобразовать при формировании текст email в удобочитаемый, желательно такой же как и при поиске.

Стандартное решение кастомизации оповещений



Вначале я кратко опишу стандартное решение, которое не подходит для поисковых оповещений, но части из которого (пункты 2, 5-11) нужны будут потом для запуска моего варианта.

1. Создать dll AlertHandler c классом Class1 реализующим интерфейс IAlertNotifyHandler, в методе OnNotification которого мы можем получить текущий текст оповещения и соответственно его модифицировать.

2. Поместить dll в GAC

3. Сделать копию файла alertTemplates.xml который находится: C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\XML. При этом всегда модифицировать копию файла а не оригинал (это важно, для того что бы потом можно было вернуться к стандартному обработчику и не накосячить).

4. Назвать новый файл (копию alertTemplates.xml) CustomAlertTemplates и сохранить его. Изменить файл следующим образом. Найти блок properties и добавить в этот блок следующие строки:

<NotificationHandlerAssembly>AlertHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d59ecf2a3bd66904</NotificationHandlerAssembly>
<NotificationHandlerClassName>AlertHandler.Class1</NotificationHandlerClassName>
<NotificationHandlerProperties></NotificationHandlerProperties>


* This source code was highlighted with Source Code Highlighter.


Теперь весь блок будет выглядеть примерно так:

<Properties>
<ImmediateNotificationExcludedFields>ID;Author;Editor;Modified_x0020_By;Created_x0020_By;_UIVersionString;ContentType;TaskGroup;IsCurrent;Attachments;NumComments;</ImmediateNotificationExcludedFields>
 <DigestNotificationExcludedFields>ID;Author;Editor;Modified_x0020_By;Created_x0020_By;_UIVersionString;ContentType;TaskGroup;IsCurrent;Attachments;NumComments;</DigestNotificationExcludedFields>
 <NotificationHandlerAssembly>AlertHandler, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d59ecf2a3bd66904</NotificationHandlerAssembly>
 <NotificationHandlerClassName>AlertHandler.Class1</NotificationHandlerClassName>
 <NotificationHandlerProperties></NotificationHandlerProperties>
</Properties>


* This source code was highlighted with Source Code Highlighter.


Примечание: PublicKeyToken можно посмотреть в свойствах dll найдя ее в C:\windows\assembly.

5. Выполнить команду из C:\Program Files\Common Files\Microsoft Shared\web server extensions\14\BIN: stsadm -o updatealerttemplates -filename «C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\XML\customalerttemplates.xml» -url 6. Выполнить команду: stsadm -o setproperty -pn job-immediate-alerts -pv «every 1 minutes» это нужно только для тестирования. Вернуть обратно после тестирования.

7. Убедиться что SharePoint настроен на отправку исходящих email.

8. Убедиться что оповещение включено для например библиотеки документов если вы тестируете на библиотеке документов.

9. Выполнить команду: iisreset

10. Выполнить команду: services.msc

11. перестартовать Windows SharePoint Services Timer.

Проблема стандартного решения



Все замечательно, все работает, но только не для случая поисковых оповещений. Для поисковых оповещений в методе OnNotification(SPAlertHandlerParams ahp) не приходят значения полей ahp.eventData и ahp.body которые собственно и нужны для кастомизации.

В ответ на мои вопросы на форумах social.technet.microsoft.com я получил следующую информацию.

First, you can custom the SharePoint alert using IAlertNotifyHandler as following:
blogs.msdn.com/b/sharepointdeveloperdocs/archive/2007/12/14/how-to-customizing-alert-emails-using-ialertnotificationhandler.aspx
Second, there is no search alert email template for you to modify.
All the Alert template: msdn.microsoft.com/en-us/library/bb802738.aspx

То есть получается, что решения не существует?!.. Но так как я не привык сдаваться без боя, то продолжил собирать по крупицам информацию из интернета. И вот что из этого вышло.

Мой вариант



1. Чтение форумов показало, что для обработки поисковых оповещений, прежде всего, необходимо добавить новый тип AlertTemplate — “OSS.Search”. Добавляем в файл CustomAlertTemplates.

<AlertTemplate Type="Custom" Name="OSS.Search" AlwaysNotify="True" DefaultTitle="Search">
       <EventTypes IsVisible="True">
       <EventType Mask="0x1" Selected="true">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventDiscovered;</EventType>
       <EventType Mask="0x2">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventModified;</EventType>
       <EventType Mask="0x3">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventAll;</EventType>
       </EventTypes>
       <Frequency IsVisible="true" ShowImmediate="false" ShowDaily="true" ShowWeekly="true" ShowTime="false" DefaultFrequency="Daily"/>
       <Filters IsVisible="false"/>
       <Properties>
       <NotificationHandlerAssembly>mySearchAlert, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aa1e89f3cc0ef56b</NotificationHandlerAssembly>
       <NotificationHandlerClassName>mySearchAlert.MySearchAlertHandler</NotificationHandlerClassName>
       <NotificationHandlerProperties></NotificationHandlerProperties>
       <UpdateHandlerAssembly>mySearchAlert, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aa1e89f3cc0ef56b</UpdateHandlerAssembly>
       <UpdateHandlerClassName>mySearchAlert.MySearchUpdateAlertHandler</UpdateHandlerClassName>
       <UpdateHandlerProperties></UpdateHandlerProperties>
       </Properties>
</AlertTemplate>


* This source code was highlighted with Source Code Highlighter.


При этом важно учесть, что для того чтобы вернуться обратно к стандартному обработчику, сохраненного ранее стандартного файла уже недостаточно, поэтому копируем стандартный alertTemplates.xml в файл например в alertTemplates_forRestore.xml и добавляем следующую секцию

<AlertTemplate Type="Custom" Name="OSS.Search" AlwaysNotify="True" DefaultTitle="Search">
 <EventTypes IsVisible="True">
  <EventType Mask="0x1" Selected="true">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventDiscovered;</EventType>
  <EventType Mask="0x2">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventModified;</EventType>
  <EventType Mask="0x3">$Resources:Microsoft.Office.Server.Search,SearchResults_ATEventAll;</EventType>
 </EventTypes>
 <Frequency IsVisible="true" ShowImmediate="false" ShowDaily="true" ShowWeekly="true" ShowTime="false" DefaultFrequency="Daily"/>
 <Filters IsVisible="false"/>
 <Properties>
  <NotificationHandlerAssembly>Microsoft.Office.Server.Search, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</NotificationHandlerAssembly>
  <NotificationHandlerClassName>Microsoft.Office.Server.Search.Query.SearchAlertHandler</NotificationHandlerClassName>
  <NotificationHandlerProperties></NotificationHandlerProperties>
  <UpdateHandlerAssembly>Microsoft.Office.Server.Search, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</UpdateHandlerAssembly>
  <UpdateHandlerClassName>Microsoft.Office.Server.Search.Query.SearchAlertHandler</UpdateHandlerClassName>
  <UpdateHandlerProperties></UpdateHandlerProperties>
 </Properties>
</AlertTemplate>


* This source code was highlighted with Source Code Highlighter.


Бережно храним файл alertTemplates_forRestore.xml — без него откат на стандартный обработчик невозможен.

2. Пораскинув мозгом, я подумал что, раз при использовании стандартного решения, какая-то часть информации к нам все-таки приходит, то можно отталкиваться от нее. И нашел может не самое изящное, но рабочее решение.
  • используя информацию об обрабатываемом оповещении, я создаю программно новое временное поисковое оповещение SearchAlert alert2 = new SearchAlert(s1,q1);
  • создаю поисковый запрос к серверу Sharepoint, используя строку поиска обрабатываемого оповещения — Search.Query.Query query = alert2.CreateSearchQuery
    с помощью Reflection задаю internal свойство AlertInfo нашего запроса query, установив LastUpdateTime в нужное нам значение (чтобы получить только измененные записи с последнего срабатывания оповещения) и получаю результат запроса
  • дополнительную информацию из базы SQL получаю с помощью FullTextSqlQuery
  • удаляю созданный мной alert2
  • генерирую свой текст HTML-тела письма, основываясь на полях базы данных SQL и полях Saherepoint 2010, полученных в query.


Не буду полностью приводить весь код (он длинный) — только основные моменты.

public class MySearchAlertHandler : IAlertNotifyHandler
// обработчик оповещений
public bool OnNotification(SPAlertHandlerParams alertHandler)
    {
    ...  
          return CustomAlertNotification(alertHandler);
    ...    
    }


public bool CustomAlertNotification(SPAlertHandlerParams alertHandlerParams)
    {
      string myBody = "";
      SPSite site = null;
      int searchAlertNotificationQuota = 200;
        TimeSpan span;
        SPAlert a = alertHandlerParams.a;
        
                          
       site = new SPSite(alertHandlerParams.siteUrl+ alertHandlerParams.webUrl;);
                
           
// alertTime и span нам понадобятся далее

        DateTime alertTime = a.AlertTime;
        if (a.AlertFrequency == SPAlertFrequency.Weekly)
        {
          span = TimeSpan.FromDays(7.0);
        }
        else
        {
          span = TimeSpan.FromDays(1.0);
        }

        if ((alertTime + span) <= DateTime.Now)
        {
          return true;
        }

        using (SPWeb web = site.OpenWeb())
        {
          
          
// из полученного обработчиком оповещения (он перебирает все оповещения которые должны сработать) получаем строку поиска queryText

          string queryText = Utils.GetValueFromXML(a.Properties["p_query"], "QueryText");

          SPSite s1 = new SPSite (alertHandlerParams.siteUrl+alertHandlerParams.webUrl );
          Query q1 = new KeywordQuery(s1);
          q1.QueryText = queryText;

// программно создаем новое оповещение alert2, используя все свойства из обрабатываемого оповещения

          SearchAlert alert2 = new SearchAlert(s1,q1);
          alert2.ChangeType = AlertChangeType.DiscoveredOrModified;
          alert2.InnerAlert.AlertFrequency = alertHandlerParams.a.AlertFrequency ;
          alert2.InnerAlert.Title = "Temp#1";
          alert2.InnerAlert.EventType = alertHandlerParams.a.EventType;
          alert2.InnerAlert.User = s1.OpenWeb().CurrentUser ;
          alert2.InnerAlert.AlertType = alertHandlerParams.a.AlertType;
          alert2.Update();
          

// создаем новый запрос query к серверу основываясь на alert2 и queryText

          using (Microsoft.Office.Server.Search.Query.Query query = alert2.CreateSearchQuery ())
          {
            ResultTableCollection tables;

            query.QueryText =  queryText;
            query.RowLimit = searchAlertNotificationQuota;
            query.TrimDuplicates = false;

// а вот тут главный фокус, с помощью которого мы получим только ту информацию, которая была изменена с момента последнего срабатывания оповещения, а не всю которая удовлетворяет запросу. 
Для этого, с помощью Reflection задаем internal свойство AlertInfo нашего запроса query, установив LastUpdateTime в нужное нам значение

            AlertInfo ai = new AlertInfo();
            ai.ChangeType = alert2.ChangeType;
            ai.LastUpdateTime = alertTime - span;

            Type t1 = query.GetType();
            if (t1.GetProperty("AlertInfo", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic |
              System.Reflection.BindingFlags.Instance) == null)
              throw new ArgumentOutOfRangeException("propName", string.Format("Property {0} was not found in Type {1}",
                "AlertInfo", query.GetType().FullName));
            t1.InvokeMember("AlertInfo", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.SetProperty |
              System.Reflection.BindingFlags.Instance, null, query, new object[] { ai });


// Выполняем запрос query

            ResultTable result = null;
            try
            {
              tables = query.Execute();
            }
            catch
            {
              alert2.Delete();
              return false;
            }

            result = tables[ResultType.RelevantResults];

// Получаем result - результат запроса и удаляем наш alert2 - он уже нам не нужен
            

            alert2.Delete();

// Загружаем результат в DataTable и можем делать с ним что угодно. В таблицу попадают следующие столбцы - WorkId, Rank, Title, Author, Size, Path, Description, Write, SiteName, CollapsingStatus, HitHighlightedSummary, HitHighlightedProperties, ContentClass, IsDocument, PictureThumbnailURL

// по DiscoveredTime мы можем отобрать добавленные или измененные записи

System.Data.DataTable myTable = new System.Data.DataTable();
            myTable.Load(result2, System.Data.LoadOption.OverwriteChanges);

foreach (System.Data.DataRow myrow in myTable.Rows)
            {
              if (Convert.ToDateTime(myrow["DiscoveredTime"]) > alertTime - span)
              {
                // это добавленные записи
              }
              else
              {
// это измененные записи
              }
}

// Кроме полученных столбцов, для кастомизации нам могут понадобится и столбцы из таблицы SQL, воспользуемся FullTextSqlQuery для их получения

FullTextSqlQuery fts = new FullTextSqlQuery(site2);
                    fts.QueryText = "SELECT WorkId, Rank, Title, Author, Size, Path, Description, Write, SiteName, CollapsingStatus, HitHighlightedSummary, HitHighlightedProperties, ContentClass, IsDocument, PictureThumbnailURL, PopularSocialTags, PictureWidth, PictureHeight, DatePictureTaken, ServerRedirectedURL, ErgebnisKenntnis, LetzterKontakt, Kandidatenmail FROM SCOPE() WHERE Path='" + myrow["Path"].ToString() + "'";
                    fts.ResultTypes = ResultType.RelevantResults;
                    fts.RowLimit = 300;
                    ResultTableCollection rtc = fts.Execute();


* This source code was highlighted with Source Code Highlighter.


Где ErgebnisKenntnis, LetzterKontakt, Kandidatenmail — поля базы SQL (приведены только для примера). Для возможности поиска по ним, важно что-бы эти поля были прописаны в Metadata property сервера Sharepoint 2010.

Теперь мы можем сформировать HTML-тело письма myBody, используя любые поля базы SQL, Sharepoint и в конце не забываем его отправить пользователю.

SPUtility.SendEmail(web, false, false, string.Format("{0}", alertHandlerParams.headers["To"]),
            string.Format("{0}", alertHandlerParams.headers["Subject"]), myBody);


* This source code was highlighted with Source Code Highlighter.


Теперь, после выполнения пунктов 2, 5-11 стандартного решения, пользователи будут получать поисковые оповещения, например, в таком виде.



Все, задача решена. Других способов в интернете я не нашел, поэтому буду рад если кому-то мое решение поможет.
Теги:
Хабы:
+6
Комментарии 2
Комментарии Комментарии 2

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн