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

28 17

Введение

На данный пример меня натолкнула недавняя статья Панкрашова Дмитрия "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)
        {
          //Получим значение коллекции
          var collectionValue = (Sungero.Domain.Shared.IChildEntityCollection<Sungero.Domain.Shared.IChildEntity>)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 в коде оставлены специально, для понимания, что и откуда берется.

28
Авторизуйтесь, чтобы оценить материал.
5
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 получая конкретные объекты где-то в коде, т.е. взять условно "Сотрудника", получить какую-нибудь запись и посмотреть в коде что есть и что доступно, после чего код можно удалить.

Никита Юрков

Антон, Sungero.Domain.Shared.IChildEntity -  на var замени, он сам нужный тип поставит

Иван Романов

Сергей, а есть возможность отслеживания в истории добавление или удаление строк коллекции?

Иван Романов: обновлено 25.05.2023 в 15:28
Станислав Егоров

True / False в Да / Нет кто-то заморочился переделать?

Станислав Егоров: обновлено 26.08.2023 в 00:21
Денис Зайцев

Станислав, у вас возникли сложности? На первый взгляд несложная задача. Проверить является ли свойство логическим и в зависимости от значения переопределить новое и старое значения.

Станислав Егоров

Денис, это колхоз же типа перед формированием текста смотреть если тру меняем на да если фолс на нет) думал есть более изящное решение ?

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

Денис Зайцев

Станислав, я же написал "Проверить является ли свойство логическим...". В статье и пример реализации есть

 

Станислав Егоров

а как можно локализовать комментарий?
как локализуется сумма при смене суммы. Сумма / Total Amount?
Вместо propertyMetadata.GetLocalizedName() надо чтоб заполнялась ресурсом? как это сделать?

пока что решил сделать всё на русском, но не получается получать имя перечисления на русском. как сделать?

return string.Format("\"{0}\": {1} -> {2}", propertyMetadata.GetLocalizedName(new System.Globalization.CultureInfo("ru-RU")), formatedOldValue, formatedNewValue);
    

​


По поводу Да/Нет придумал только так... если кто то придумает лучше давайте)

if (propertyMetadata.PropertyType == Sungero.Metadata.PropertyType.Boolean)
      {
        originalValue = Convert.ToBoolean(originalValue) ? "Да" : "Нет";
        newValue = Convert.ToBoolean(newValue) ? "Да" : "Нет";
      }

Ну и падало когда менялось значение на пустоту при попытке сделать tostring, убрал эти условия

if (Equals(newValue, originalValue) || (string.IsNullOrEmpty(newValue.ToString()) && string.IsNullOrEmpty(originalValue.ToString())))
        return string.Empty;

Оставил только Equals

Станислав Егоров: обновлено 02.09.2023 в 10:24
Станислав Егоров: обновлено 02.09.2023 в 10:45
Сергей Беляков

Иван, Да, такая возможность есть, ничего сложного.
 

var stateProperties = entity.State.Properties;
var property = stateProperties.GetType().GetProperties().Where(p => p.Name == propertyName).FirstOrDefault();
var propertyState = property.GetValue(stateProperties);
var deletedLines = (IEnumerable<IChildEntity>)propertyState.GetType().GetProperty("Deleted").GetValue(propertyState);
//propertyState.GetType().GetProperty("Added").GetValue(propertyState);
foreach (var deletedLine in deletedLines)
{}

 

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

Станислав, Ваш вопрос связан с метаданными и работой с ними.
У нас в телеграмме есть группа Directum RX Developer, там выложена презентация "Метаданные + Reflection.pptx", она как раз поможет ответить на подобного рода вопросы.

Станислав Егоров

Станислав, по языку решил

var russianCulture = System.Globalization.CultureInfo.GetCultureInfo("ru-RU");
      using (Sungero.Core.CultureInfoExtensions.SwitchTo(russianCulture))
      {
... 
//Если тип свойства 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?);
          
        }
...
}

 

Станислав Егоров: обновлено 27.11.2023 в 14:27
Станислав Егоров: обновлено 27.11.2023 в 14:28
Святослав Романов

Спасибо за статью!

Вопрос: как можно отследить и записать в историю информацию об удаленной записи из коллекции?

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

Святослав, У коллекции есть свойство Deleted, в котором лежат удаленные строки.
Собственно через него и можно отследить.

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