Создание собственного слежения за объектом

В статье приведен пример создания собственного слежения за объектом IMyObject. Приведен пример реализации действия Rename в объекте IMyObject (переименование записи справочника) и получение оповещения об изменении объекта.

Пример отображения данных

Рис. 1. Слежение за объектом справочника

Рис. 2. Оповещение об изменении названия справочника

Методы расширения (интерфейса)

Точка расширения (интерфейс) IWatchProvider имеет следующие методы:

 

/// <summary>
/// Типы, за которыми идет слежение
/// </summary>
IEnumerable<Guid> TypeUid { get; }

/// <summary>
/// Нужно ли выводить предупреждение при удалении себя из наблюдателей
/// </summary>
/// <param name="entityTypeUid">Uid типа объекта</param>
/// <param name="entityId">Идентификатор объекта (не может быть null)</param>
/// <returns><c>true</c> если нужно выводить подтверждение при удалении из наблюдателей</returns>
bool NeedConfirmFromDelete(Guid entityTypeUid, [NotNull]object entityId);

/// <summary>
/// Возвращает текст предупреждения при удалении себя из наблюдателей
/// </summary>
/// <param name="entityTypeUid">Uid типа объекта</param>
/// <param name="entityId">Идентификатор объекта (не может быть null)</param>
/// <returns>Возвращает текст предупреждения при удалении себя из наблюдателей</returns>
string TextConfirmFromDelete(Guid entityTypeUid, [NotNull]object entityId);

/// <summary>
/// Поддерживается ли указанный объект
/// </summary>
bool IsAvailable(Guid entityTypeUid, object entityId);

/// <summary>
/// Действия, которые отслеживаются
/// </summary>
IEnumerable<Guid> ActionUids { get; }

/// <summary>
/// Описание слежения по типу объекта
/// </summary>
string GlobalWatchDescription { get; }

/// <summary>
/// Описание слежения за конкретным объектом
/// </summary>
string WatchDescription { get; }

/// <summary>
/// Идентификатор типа родительского объекта
/// </summary>
IEnumerable<Guid> ParentTypeUid { get; }

/// <summary>
/// Получить родителей по сущности
/// </summary>
/// <param name="entity">Сущность</param>
/// <returns>Список родителей сущности</returns>
IEnumerable<Guid> GetParentTypeUid(IEntity entity);

/// <summary>
/// Получить идентификатор родительской сущности
/// </summary>
/// <returns>Идентификатор родительской сущности</returns>
long? GetParentEntityId(IEntity entity);

/// <summary>
/// Можно ли посылать указанному пользователю
/// </summary>
/// <param name="entity">Сущность</param>
/// <param name="user">Пользователь</param>
/// <returns><c>true</c>, если можно посылать указанному пользователю</returns>
bool CanSendToUser(IEntity entity, IUser user);
 
Методы точки расширения, указанные ниже, актуальны для версии ниже 3.8
/// <summary>
/// Типы, за которыми идет слежение
/// </summary>
IEnumerable<Guid> TypeUid { get; }

/// <summary>
/// Действия которые отслеживаются
/// </summary>
IEnumerable<Guid> ActionUids { get; }

/// <summary>
/// Описание слежения по типу объекта
/// </summary>
string GlobalWatchDescription { get; }

/// <summary>
/// Описание слежения за конкретным объектом
/// </summary>
string WatchDescription { get; }

/// <summary>
/// Идентификатор типа родительского объекта
/// </summary>
IEnumerable<Guid> ParentTypeUid { get; }

/// <summary>
/// Получить родителей по сущности
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
IEnumerable<Guid> GetParentTypeUid(IEntity entity);

/// <summary>
/// Получить идентификатор родительской сущности
/// </summary>
/// <returns>Идентификатор родительской сущности</returns>
long? GetParentEntityId(IEntity entity);

/// <summary>
/// Можно ли посылать указанному пользователю
/// </summary>
/// <param name="entity">Сущность</param>
/// <param name="user">Пользователь</param>
/// <returns><c>true</c>, если можно послать указанному пользователю</returns>
bool CanSendToUser(IEntity entity, IUser user);

Пример класса точки расширения

Точка расширения IWatchProvider имеет базовый класс BaseWatchProvider, в котором уже реализована базовая логика работы точки расширения. Для создания собственного слежения за объектом Вам достаточно переопределить несколько методов. Пример указан ниже:

[Component]
public class WatchProviderExample : BaseWatchProvider
{
    public override IEnumerable<Guid> TypeUid
    {
        get { yield return InterfaceActivator.UID<IMyObject>(); } //Реализация слежения за объектом типа IMyObject (простой объект справочник)
    }

    public override IEnumerable<Guid> ActionUids
    {
        get { yield return MyObjectActions.RenameGuid; } //Uid действия в объекте
    }

    public override string GlobalWatchDescription
    {
        get { return SR.T("Вы будете получать оповещения по переименованным записям справочника с типом объекта"); }
    }

    public override string WatchDescription
    {
        get { return SR.T("Вы будете получать оповещения по переименованным записям справочника"); }
    }
}
 
Примечание
Для того, чтобы применить данную точку расширения, необходимо реализовать свое собственное действие в объекте. В данном примере действием в объекте IMyObject является Rename (Переименовать). 

Рис. 3. Действие Rename в объекте

В менеджере объекта нужно реализовать логику этого действия. Пример реализации логики переименования записи Вашего объекта:

public IEntityActionHandler EntityActionHandler { get; set; }

[Transaction]
[ActionMethod(MyObjectActions.Rename)]
public virtual void Rename(IMyObject model, string newName)
{
    model.Name = newName;
    model.Save();
    var args = new EntityActionEventArgs(null, model, MyObjectActions.Rename);
    EntityActionHandler.ActionExecuted(args);     
}

EntityActionEventArgs – параметры события для действия с сущностью. Участвует в формировании истории действий. Использование данного кода обязательно! Иначе Ваше оповещение по действию не будет работать.

EntityActionHandler.ActionExecuted(args) – обработчик действий с сущностью сработает после выполнения действия.

В данном примере была реализована карточка записи справочника, представленная на рисунке 1, в которой отображены его свойства, а также кнопка подписки (слежения за объектом) и кнопка редактирования названия записи справочника. Чтобы добавить в разметку кнопку подписки, достаточно воспользоваться html-хелпером Html.Header и передать в него тему/название объекта и сам объект. Пример реализации разметки для отображения карточки записи справочника:

@model ObjectIconModule.Models.IMyObject
@using EleWise.ELMA.Model.Services
@using EleWise.ELMA;
@using EleWise.ELMA.BPM.Web.Tasks.Extensions
@using ObjectIconModule.Web

@(Html.Toolbar()
        .Group("tb-group1")
            .Button(b => b
                .Uid("toolbar-action-Back")
                .Text(SR.Back)
                .IconUrl("#x32/Prev.png")
                .Click("javascript:history.back(-1);")
            )
            .Button(b => b
                .Uid("toolbar-action-Edit")
                .Text(SR.T("Изменить название"))
                .IconUrl("#x32/Edit.png")
                .Click(Html.OpenPopup("RenamePopup", useReferrer: true).ToString())
            )
)

@{
    Html.Header(Model.Name, Model);
}

@using (Html.ElmaForm())
{
    @Html.TableFormStart()
    @Html.DisplayFor(m => m, "Object")
    @Html.TableFormEnd()
}

@(Html.PopupWindow(
    "RenamePopup",
SR.T("Изменить имя объекта"),
          Url.Action("RenamePopup", "Home", new { area = RouteProvider.AreaName, Name = Model.Name, id = Model.Id })
, 600)
)
 
Для версии ниже 3.8 нужно использовать html-хелпер Html.TaskWatchButton

Пример реализации разметки для отображения карточки записи справочника для версии ниже 3.8:

@model IMyObject
@using EleWise.ELMA.Model.Services
@using EleWise.ELMA;
@using ObjectIconModule.Models
@using EleWise.ELMA.BPM.Web.Tasks.Extensions
@using ObjectIconModule.Web

@(Html.Toolbar()
        .Group("tb-group1")
            .Button(b => b
                .Uid("toolbar-action-Back")
                .Text(SR.Back)
                .IconUrl("#x32/Prev.png")
                .Click("javascript:history.back(-1);")
            )
            .Button(b => b
                .Uid("toolbar-action-Edit")
                .Text(SR.T("Изменить название"))
                .IconUrl("#x32/Edit.png")
                .Click(Html.OpenPopup("RenamePopup", useReferrer: true).ToString())
            )
)

@{
    var metadata = InterfaceActivator.UID<IMyObject>();
    Html.Header(string.Format("{0} {1}", SR.T("Мой объект"), Html.TaskWatchButton(metadata, Model.Id, false)), false, false);
}

@using (Html.ElmaForm())
{
    @Html.TableFormStart()
    @Html.DisplayFor(m => m, "Object")
    @Html.TableFormEnd()
}

@(Html.PopupWindow(
    "RenamePopup",
SR.T("Изменить имя объекта"),
          Url.Action("RenamePopup", "Home", new { area = RouteProvider.AreaName, Name = Model.Name, id = Model.Id })
, 600)
)

Как можно заметить из приведенного примера кода: при нажатии на кнопку Изменить название  на панели инструментов открывается popup-окно с полем Название. При вызове popup-окна выполняется метод RenamePopup в контроллере Home, который возвращает представление этого popup-окна. Пример кода представления (View) popup-окна для изменения названия записи справочника:

@model IMyObject
@using EleWise.ELMA;
@using ObjectIconModule.Models
@using ObjectIconModule.Web

@{
    var id = Model.Id;
    var name = Model.Name; 
}
@Html.EditableProperty(m => m.Name)
<div class="popup-buttons">
    <input type="button" class="confirm" value="@(SR.T("Сохранить"))"
           onclick="renameItem($(’#@ViewData.TemplateInfo.GetFullHtmlFieldId("Name")’).val())" />
    <input type="button" value="@(SR.T("Отмена"))" onclick="@(Html.ClosePopup())" />
</div>

<script language="javascript" type="text/javascript">
    function renameItem(name) {
        $.ajax({
            url: ’@(Url.Action("Rename", "Home", new { area = RouteProvider.AreaName, id }))’,
            type: "POST",
            dataType: ’json’,
            data: { "name": name },
            cache: false,
            success: function (data) {
                if (data && data.error) {
                    jAlert(data.error, ’@SR.T("Ошибка")’);
                } else {
                    location.reload();
                }
            }
        });
    }
 
Примечание
Как можно заметить из кода представления popup-окна редактирования названия записи справочника: при нажатии на кнопку Сохранить вызывается функция JavaScript renameItem(name), которая вызывает метод Rename контроллера  Home. 

Пример кода контроллера:

public ActionResult RenamePopup(long id)
{
    var model = MyObjectManager.Instance.LoadOrNull(id);
    return PartialView(model);
}

public ActionResult Rename(long id, string Name)
{
    var model = MyObjectManager.Instance.LoadOrNull(id);
    MyObjectManager.Instance.Rename(model, Name);
    var json = new JsonSerializer().Serialize(model);
    return new JsonResult{Data = json};
}

В методе Rename вызывается метод Rename из менеджера Вашего объекта, после выполнения которого будет отправлено оповещение подписчикам на запись справочника. Для того, чтобы оповещение появилось в ленте сообщений и в Вашем канале отправки сообщений, необходимо создать шаблон оповещения. Подробнее про шаблоны оповещений можно прочитать здесь. Пример шаблона оповещения:

<?xml version="1.0" encoding="utf-8"?>
<Notifications description="Оповещения по объекту" version="3.7.3.13420">
  
  <Default>
    <Filter>
      <Event>IEntityActionHandler.ActionExecuted</Event>
      <Object>ObjectIconModule.Models.MyObject</Object>
    </Filter>
    <RecipientSet>
      <User>{$WatchList}</User>
    </RecipientSet>
    <URL>
      /ObjectIconModule/Home/ViewItem/{$New.Id}
    </URL>
    <ObjectId>{$New.Id}</ObjectId>
  </Default>
  
  <Notification description="Объект переименован">
    <Filter>
      <Action>Rename</Action>
    </Filter>
    <Subject>
      ({SR(’Объект переименован’)}) {$New.Name}
    </Subject>
    <ShortMessage>
      {SR(’{$New.ChangeAuthor} переименовал объект: из "{$Old.Name}" в "{$New.Name}"’)}
    </ShortMessage>
    <FullMessage>
      {TableStart()}
      {PropertyRowWithChanges({$Old.Name};{$New.Name})}
      {PropertyRow({$New.Name})}
      {PropertyRow({$New.ChangeAuthor})}
      {TableEnd()}
    </FullMessage>
  </Notification>
</Notifications>

Для  того, чтобы у Вас отображались «старые» значения в оповещениях (например, {$Old.Name}), необходимо реализовать следующий класс:

[Component]
internal class MyObjectRenameActionsEventAggregator : BaseEntityUpdateEventAggregator
{
    protected override IEnumerable<Guid> ProcessedActions
    {
        get { yield return MyObjectActions.RenameGuid; }
    }
}

Ссылки на элементы API