Как отследить изменение свойства или свойства коллекции в любой сущности

20 5

Введение

На данный пример меня натолкнула недавняя статья Панкрашова Дмитрия "DirectumRX. Подробная история в карточке сущности", а точнее комментарии к этой статье.

В материале Дмитрия описывается пример записи в историю сущности, при изменении какого-либо свойства этой сущности и в комментариях были заданы несколько вопросов:

  1. Как отследить изменение в коллекциях ?
  2. Как обратиться к GetLocalizedValue() в случае с перечислением ?

Для того чтобы разобраться в этом вопросе, я сделал небольшой пример, который должен ответить на поставленные вопросы.
Весь код можно разместить в серверном или разделяемом слое какого-нибудь модуля (лично у меня серверный слой тестового модуля).

Пример

1) Для того, чтобы работать с метаданными необходимо подключить библиотеку Sungero.Domain.Shared.dll

using Sungero.Domain.Shared;

2) Далее размещаем в коде 2 функции ChangeRequisites() и CheckRequisite()

/// <summary>
    /// Получить список измененных свойств объекта
    /// </summary>
    /// <param name="entity">Сущность</param>
    /// <returns>Список измененных свойств</returns>
    [Public, Remote(IsPure=true)]
    public static List<string> ChangeRequisites(Sungero.Domain.Shared.IEntity entity)
    {
      var changeList = new List<string>();
      
      //Получаем "Тип" объекта
      var objType = entity.GetType().GetFinalType();
      
      //Получаем "Метаданные" объекта
      var objMetadata = objType.GetEntityMetadata();
      
      //TODO: Ниже, в objMetadata.Properties можно добавить фильтрацию по наименованиям реквизитов (либо выбрать, либо исключить из выбора определенные реквизиты)
      //Пример 1: objMetadata.Properties.Where(p => requsitesList.Contains(p.Name));
      //Пример 2: objMetadata.Properties.Where(p => !excludeRequsitesList.Contains(p.Name));
      
      //Получаем свойства объекта
      var properties = objMetadata.Properties;      
      foreach (var propertyMetadata in properties)
      {
        //Если текущее свойство это коллекция, то обработает ее отдельно
        if (propertyMetadata.PropertyType == Sungero.Metadata.PropertyType.Collection)
        {
          //Получим значение коллекции
          Sungero.Domain.Shared.IChildEntityCollection<Sungero.Domain.Shared.IChildEntity> collectionValue = propertyMetadata.GetValue(entity);
          
          //Переберем строки коллекции
          foreach (Sungero.Domain.Shared.IChildEntity line in collectionValue)
          {
            System.Type lineType = line.GetType();
            var lineMetadata = lineType.GetEntityMetadata();
            
            //TODO: Ниже, в lineMetadata.Properties можно добавить фильтрацию по наименованиям реквизитов (либо выбрать, либо исключить из выбора определенные реквизиты)
            //Пример 1: lineMetadata.Properties.Where(p => collectionRequsitesList.Contains(p.Name));
            //Пример 2: lineMetadata.Properties.Where(p => !collectionExcludeRequsitesList.Contains(p.Name));
            
            //Получаем свойства строки коллекции
            var lineProperties = lineMetadata.Properties;
            foreach (var linePropertyMetadata in lineProperties)
            {
              //Если значение свойства это RootEntity, то пропустим обработку этого свойства
              if (Equals(entity, linePropertyMetadata.GetValue(line)))
                continue;
              
              var checkResult = CheckRequisite(line, linePropertyMetadata);
              if (!string.IsNullOrEmpty(checkResult))
                changeList.Add(string.Format("Коллекция {0}. {1}", propertyMetadata.GetLocalizedName(), checkResult));
              }
          }
        }
        else
        {
          var checkResult = CheckRequisite(entity, propertyMetadata);
          if (!string.IsNullOrEmpty(checkResult))
            changeList.Add(checkResult);
        }
      }
      
      return changeList;
    }
    


    /// <summary>
    /// Проверка изменения свойства
    /// </summary>
    /// <param name="entity">Сущность или строка коллекции</param>
    /// <param name="propertyMetadata">Метаданные свойства</param>
    /// <returns>Сообщение об изменении свойства, иначе string.Empty</returns>
    private static string CheckRequisite(Sungero.Domain.Shared.IEntity entity, Sungero.Metadata.PropertyMetadata propertyMetadata)
    {
      var stateProperties = entity.State.Properties;
      var propertyState = stateProperties.GetType().GetProperty(propertyMetadata.Name).GetValue(stateProperties);
      
      //Получим текущее значение свойства
      var newValue = propertyMetadata.GetValue(entity);
      //Получим предыдущее значение свойства, до сохранения
      var originalValue = propertyState.GetType().GetProperty("OriginalValue").GetValue(propertyState);
      
      if (Equals(newValue, originalValue) || (string.IsNullOrEmpty(newValue.ToString()) && string.IsNullOrEmpty(originalValue.ToString())))
        return string.Empty;
      
      //Если тип свойства Enumeration, то получим локализированные значения
      if (propertyMetadata.PropertyType == Sungero.Metadata.PropertyType.Enumeration)
      {
        var infoProperties = entity.Info.Properties;
        var propertyInfo = infoProperties.GetType().GetProperty(propertyMetadata.Name).GetValue(infoProperties) as Sungero.Domain.Shared.EnumPropertyInfo;
        
        newValue = propertyInfo.GetLocalizedValue(newValue as Sungero.Core.Enumeration?);
        originalValue = propertyInfo.GetLocalizedValue(originalValue as Sungero.Core.Enumeration?);
      }
      
      var formatedNewValue = string.Format(" Новое значение: {0}.", string.IsNullOrEmpty(newValue.ToString()) ? "Пусто" : newValue);
      var formatedOldValue = string.Format(" Прежнее значение: {0}.", string.IsNullOrEmpty(originalValue.ToString()) ? "Пусто" : originalValue);
      return string.Format("Изменение свойства \"{0}\".{1}{2}", propertyMetadata.GetLocalizedName(), formatedNewValue, formatedOldValue);
    }

 

Результат

Для теста я перекрыл справочник DocumentRegister, добавил на ленту действие, а в действии написал вызов функции

var changeList = finex.TestModule.PublicFunctions.Module.Remote.ChangeRequisites(_obj);
Dialogs.ShowMessage(string.Join("\n", changeList));

 

Запускаем и проверяем.
Для примера я взял запись "Служебные записки" .


 

Меняем в карточке свойства: Индекс, Состояние, элемент номера в коллекции "Формат номера".


 

Нажимаем на нашу тестовую кнопку "123" и получаем результат:


 

Вместо выводов

Код в примере постарался сделать максимально универсальным с относительно подробными комментариями. Если я чего-то не предусмотрел или у Вас появятся вопросы по реализации, то пишите в комментариях, постараюсь ответить.

P.S. Полные namespace в коде оставлены специально, для понимания, что и откуда берется.

20
Авторизуйтесь, чтобы оценить материал.
3
Konstantin Bastylev

Спасибо. Познавательная статья.

Возникает только вопрос, каким образом производился первоначальный анализ структуры метаданных?

Konstantin Bastylev: обновлено 01.04.2022 в 10:19
Антон Посаженков

Спасибо действительно полезная статья.

Подскажите, пожалуйста, при сборке ругается на строку:

foreach (Sungero.Domain.Shared.IChildEntity line in collectionValue)

Конкретно на collectionValue.

Отсутствует обязательный для компилятора член "Microsoft.CSharp.RuntimeBinder.Binder.Convert"  (CS0656)

Антон Посаженков

Собралось только с помощью приведения к конкретному типу:

//dynamic collectionValue = propertyMetadata.GetValue(entity);
var collectionValue = (IChildEntityCollection<Sungero.Domain.Shared.IChildEntity>)propertyMetadata.GetValue(entity);

 

Сергей Беляков

Антон, Да, начиная с 4.1 dynamic перестал работать.

//Можно так
IChildEntityCollection collectionValue = propertyMetadata.GetValue(entity);

//Или так
foreach (IChildEntity line in (IChildEntityCollection)collectionValue)
{}

 

Сергей Беляков: обновлено 29.04.2022 в 11:50
Сергей Беляков

Konstantin, Описание метаданных объекта (его свойств и действий) можно посмотреть в MTD файле этого объекта (mtd который находится в Shared).
Еще можно смотреть свойства, методы или namespace получая конкретные объекты где-то в коде, т.е. взять условно "Сотрудника", получить какую-нибудь запись и посмотреть в коде что есть и что доступно, после чего код можно удалить.

Авторизуйтесь, чтобы написать комментарий