Подтвердить что ты не робот

Каким будет лучший способ реализации отслеживания изменений объекта

У меня есть класс, который содержит 5 свойств.

Если какое-либо значение присваивается одному из этих полей, другое значение (например, IsDIrty) изменится на true.

public class Class1
{
    bool IsDIrty {get;set;}

    string Prop1 {get;set;}
    string Prop2 {get;set;}
    string Prop3 {get;set;}
    string Prop4 {get;set;}
    string Prop5 {get;set;}
}
4b9b3361

Ответ 1

Для этого вы не можете использовать автоматические приемники и сеттеры, и вам нужно установить IsDirty в каждый сеттер.

Обычно у меня есть общий метод setProperty, который принимает параметр ref, имя свойства и новое значение. Я вызываю это в установщике, разрешает одну точку, где я могу установить isDirty и повышать события уведомления о событиях, например.

protected bool SetProperty<T>(string name, ref T oldValue, T newValue) where T : System.IComparable<T>
    {
        if (oldValue == null || oldValue.CompareTo(newValue) != 0)
        {
            oldValue = newValue;
            PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
            isDirty = true;
            return true;
        }
        return false;
    }
// For nullable types
protected void SetProperty<T>(string name, ref Nullable<T> oldValue, Nullable<T> newValue) where T : struct, System.IComparable<T>
{
    if (oldValue.HasValue != newValue.HasValue || (newValue.HasValue && oldValue.Value.CompareTo(newValue.Value) != 0))
    {
        oldValue = newValue;
        PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
    }
}

Ответ 2

Вы можете реализовать интерфейсы IChangeTracking или IRevertibleChangeTracking, которые теперь включены в .NET Standard 2.0.

Реализация выглядит следующим образом:

IChangeTracking:

class Entity : IChangeTracking
{
  string _FirstName;
  public string FirstName
  {
    get => _FirstName;
    set
    {
      if (_FirstName != value)
      {
        _FirstName = value;
        IsChanged = true;
      }
    }
  }

  string _LastName;
  public string LastName
  {
    get => _LastName;
    set
    {
      if (_LastName != value)
      {
        _LastName = value;
        IsChanged = true;
      }
    }
  }

  public bool IsChanged { get; private set; }    
  public void AcceptChanges() => IsChanged = false;
}

IRevertibleChangeTracking:

class Entity : IRevertibleChangeTracking
{
  Dictionary<string, object> _Values = new Dictionary<string, object>();

  string _FirstName;
  public string FirstName
  {
    get => _FirstName;
    set
    {
      if (_FirstName != value)
      {
        if (!_Values.ContainsKey(nameof(FirstName)))
          _Values[nameof(FirstName)] = _FirstName;
        _FirstName = value;
        IsChanged = true;
      }
    }
  }

  string _LastName;
  public string LastName
  {
    get => _LastName;
    set
    {
      if (_LastName != value)
      {
        if (!_Values.ContainsKey(nameof(LastName)))
          _Values[nameof(LastName)] = _LastName;
        _LastName = value;
        IsChanged = true;
      }
    }
  }

  public bool IsChanged { get; private set; }

  public void RejectChanges()
  {
    foreach (var property in _Values)
      GetType().GetRuntimeProperty(property.Key).SetValue(this, property.Value);
    AcceptChanges();
  }

  public void AcceptChanges()
  {
    _Values.Clear();
    IsChanged = false;
  }
}

Другой вариант, , который мне нравится больше всего, - это использовать библиотеку отслеживания изменений, такую как TrackerDog, которая генерирует весь шаблонный код для вас, в то время как нужно просто предоставить POCO лица.

Есть и другие способы добиться этого, если вы не хотите реализовывать все свойства вручную. Одним из вариантов является использование библиотеки ткачества, такой как Fody.PropertyChanged и Fody.PropertyChanging, и обработка методов изменения для кэширования старых значений и отслеживания состояния объекта. Другой вариант - сохранить граф объектов в виде MD5 или какого-либо другого хэша и сбросить его при любом изменении, вы можете быть удивлены, но если вы не ожидаете изменений в миллиард и если вы запрашиваете его только по требованию, он может действительно работать быстро.

Вот пример реализации (Примечание: требуется Json.NET и Fody/PropertyChanged:

[AddINotifyPropertyChangedInterface]
class Entity : IChangeTracking
{
  public string UserName { get; set; }
  public string LastName { get; set; }

  public bool IsChanged { get; private set; }

    string hash;
  string GetHash()
  {
    if (hash == null)
      using (var md5 = MD5.Create())
      using (var stream = new MemoryStream())
      using (var writer = new StreamWriter(stream))
      {
        _JsonSerializer.Serialize(writer, this);
        var hash = md5.ComputeHash(stream);
        this.hash = Convert.ToBase64String(hash);
      }
    return hash;
  }

  string acceptedHash;
  public void AcceptChanges() => acceptedHash = GetHash();

  static readonly JsonSerializer _JsonSerializer = CreateSerializer();
  static JsonSerializer CreateSerializer()
  {
    var serializer = new JsonSerializer();
    serializer.Converters.Add(new EmptyStringConverter());
    return serializer;
  }

  class EmptyStringConverter : JsonConverter
  {
    public override bool CanConvert(Type objectType) 
      => objectType == typeof(string);

    public override object ReadJson(JsonReader reader,
      Type objectType,
      object existingValue,
      JsonSerializer serializer)
      => throw new NotSupportedException();

    public override void WriteJson(JsonWriter writer, 
      object value,
      JsonSerializer serializer)
    {
      if (value is string str && str.All(char.IsWhiteSpace))
        value = null;

      writer.WriteValue(value);
    }

    public override bool CanRead => false;  
  }   
}

Ответ 3

Решение Dan идеально.

Еще один вариант, чтобы рассмотреть, нужно ли вам делать это на нескольких классах (или, возможно, вы хотите, чтобы внешний класс "прослушивал" изменения свойств):

  • Внедрить интерфейс INotifyPropertyChanged в абстрактном классе
  • Переместите свойство IsDirty в ваш абстрактный класс
  • Имейте Class1 и все другие классы, которым требуется эта функциональность, чтобы расширить ваш абстрактный класс
  • Пусть все ваши сеттеры запускают событие PropertyChanged, реализованное вашим абстрактным классом, передавая свое имя событию
  • В вашем базовом классе слушайте событие PropertyChanged и установите IsDirty в true, когда он запускает

Сначала он немного работал над созданием абстрактного класса, но это лучшая модель для наблюдения за изменениями данных, так как любой другой класс может видеть, когда изменяется IsDirty (или любое другое свойство).

Мой базовый класс для этого выглядит следующим образом:

public abstract class BaseModel : INotifyPropertyChanged
{
    /// <summary>
    /// Initializes a new instance of the BaseModel class.
    /// </summary>
    protected BaseModel()
    {
    }

    /// <summary>
    /// Fired when a property in this class changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Triggers the property changed event for a specific property.
    /// </summary>
    /// <param name="propertyName">The name of the property that has changed.</param>
    public void NotifyPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Любая другая модель затем просто расширяет BaseModel и вызывает NotifyPropertyChanged в каждом сеттере.

Ответ 4

Установите IsDirty значение true во всех ваших сеттерах.

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

Ответ 5

Если существует очень большое количество таких классов, все из которых имеют тот же шаблон, и вам часто приходится обновлять их определения, подумайте об использовании генерации кода, чтобы автоматически выплевывать исходные файлы С# для всех классов, чтобы вы не не нужно вручную их поддерживать. Ввод в генератор кода был бы простым форматом текстового файла, который можно легко проанализировать, указав имена и типы свойств, необходимых в каждом классе.

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

Обновление:

Вероятно, это можно сделать сверху, для простого примера, но было интересно выяснить!

В Visual Studio 2008, если вы добавите файл под названием CodeGen.tt в свой проект, а затем вставьте в него этот материал, вы получите настройки системы генерации кода:

<#@ template debug="false" hostspecific="true" language="C#v3.5" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>

<# 

// You "declare" your classes here, as in these examples:

var src = @"

Foo:     string Prop1, 
         int Prop2;

Bar:     string FirstName,
         string LastName,
         int Age;
";

// Parse the source text into a model of anonymous types

Func<string, bool> notBlank = str => str.Trim() != string.Empty;

var classes = src.Split(';').Where(notBlank).Select(c => c.Split(':'))
    .Select(c => new 
    {
        Name = c.First().Trim(),
        Properties = c.Skip(1).First().Split(',').Select(p => p.Split(' ').Where(notBlank))
                      .Select(p => new { Type = p.First(), Name = p.Skip(1).First() })
    });
#>

// Do not edit this file by hand! It is auto-generated.

namespace Generated 
{
<# foreach (var cls in classes) {#>    class <#= cls.Name #> 
    {
        public bool IsDirty { get; private set; }
        <# foreach (var prop in cls.Properties) { #>

        private <#= prop.Type #> _storage<#= prop.Name #>; 

        public <#= prop.Type #> <#= prop.Name #> 
        {
            get { return _storage<#= prop.Name #>; }
            set 
            {
                IsDirty = true;
                _storage<#= prop.Name #> = value;
            }
        } <# } #>

    }

<# } #>
}

Там есть простой строковый литерал с именем src, в котором вы объявляете нужные классы в простом формате:

Foo:     string Prop1,
         int Prop2;

Bar:     string FirstName,
         string LastName,
         int Age;

Таким образом, вы можете легко добавить сотни похожих объявлений. Всякий раз, когда вы сохраняете свои изменения, Visual Studio будет выполнять шаблон и вывести CodeGen.cs как вывод, который содержит источник С# для классов, в комплекте с логикой IsDirty.

Вы можете изменить шаблон того, что создается, изменив последний раздел, где он проходит через модель и создает код. Если вы использовали ASP.NET, он похож на него, кроме источника С# вместо HTML.

Ответ 6

Внимательно рассмотрите основную цель отслеживания объектов? Предположим, что если это что-то вроде других объектов, нужно что-то сделать на основе другого состояния объекта, тогда рассмотрите возможность использования шаблона проектирования наблюдателя.

Если это что-то маленькое, рассмотрим возможность использования интерфейса INotifyPropertyChanged.

Ответ 7

И ответы Дэна и Энди Шеллама - мои любимые.

В любом случае, если вы хотите сохранить TRACK из ваших изменений, например, в журнале или около того, вы можете рассмотреть использование Словаря, который добавит все ваши изменения свойств, когда они будут уведомлены об изменении. Таким образом, вы можете добавить изменения в свой словарь с помощью уникального ключа и отслеживать свои изменения. Затем, если вы хотите, чтобы Roolback в памяти отображал состояние вашего объекта, вы могли бы таким образом.

ИЗМЕНИТЬ Здесь, что Bart de Smet использует для отслеживания изменений свойств по всему LINQ до AD. После того, как изменения были перенесены на AD, он очищает словарь. Таким образом, когда свойство изменяется, поскольку он реализовал интерфейс INotifyPropertyChanged, когда свойство действительно изменилось, он использует словарь > следующим образом:

    /// <summary>
    /// Update catalog; keeps track of update entity instances.
    /// </summary>
    private Dictionary<object, HashSet<string>> updates 
        = new Dictionary<object, HashSet<string>>();

    public void UpdateNotification(object sender, PropertyChangedEventArgs e)
    {
        T source = (T)sender;

        if (!updates.ContainsKey(source))
            updates.Add(source, new HashSet<string>());

        updates[source].Add(e.PropertyName);
    }

Итак, я думаю, что если Барт де Смет сделал это, это как-то практика, чтобы рассмотреть.

Ответ 8

Это то, что встроено в класс BusinessBase в Rocky Lhokta CLSA, поэтому вы всегда можете пойти и посмотреть, как это делается...

Ответ 9

Я знаю, что это старый поток, но я думаю, что Enumerations не будут работать с решением Binary Worrier. Вы получите сообщение об ошибке времени разработки, которое свойство "enum property Type" не может использоваться как параметр типа "T" в родовом типе или методе "..." SetProperty (строка, ref T, T) ". Нет бокса преобразования...".

Я ссылался на эту запись stackoverflow, чтобы решить проблему с перечислениями: Ошибка перечисления в С# с генериками

Ответ 10

Чтобы поддерживать перечисления, используйте идеальное решение Binary Worrier и добавьте код ниже.

Я добавил поддержку Enum для моего собственного (и это было больно), я думаю, это тоже приятно добавить.

protected void SetEnumProperty<TEnum>(string name, ref TEnum oldEnumValue, TEnum newEnumValue) where TEnum : struct, IComparable, IFormattable, IConvertible
{
    if (!(typeof(TEnum).IsEnum)) {
        throw new ArgumentException("TEnum must be an enumerated type");
    }

    if (oldEnumValue.CompareTo(newEnumValue) != 0) {
        oldEnumValue = newEnumValue;
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
        _isChanged = true;
    }
}

И реализовано через:

    Public Property CustomerTyper As CustomerTypeEnum
        Get
            Return _customerType
        End Get
        Set(value As ActivityActionByEnum)
            SetEnumProperty("CustomerType", _customerType, value)
        End Set
    End Property

Ответ 11

Я знаю, что прошло некоторое время с тех пор, как вы спросили об этом. Если вы по-прежнему заинтересованы в том, чтобы ваши классы были чистыми и простыми без необходимости получения базовых классов, я бы предложил использовать PropertyChanged.Fody, который имеет флаг IsChanged, реализованный

Ответ 12

Есть несколько способов отслеживания изменений со своими плюсами и минусами. Вот несколько идей:

Шаблон наблюдателя

В .NET наиболее распространенным подходом является реализация INotifyPropertyChanged, INotifyPropertyChangeing и/или IObservable (см. также Введение в Rx). Типп: Самый простой способ сделать это - использовать ReactiveObject из библиотеки ReactiveUI в качестве базового объекта.

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

Вы также можете реализовать отслеживание изменений, чтобы отслеживать все для более сложных сценариев. Трекер изменений может иметь список изменений - список имен свойств, значений и временных меток по мере необходимости - внутренне. Вы можете запросить этот список для получения необходимой информации. Подумайте о чем-то вроде шаблона EventSourcing.

Сериализация и различие

Если вы хотите увидеть, изменился ли объект и что изменилось, вы можете сериализовать исходную версию и текущую версию.

Одна из версий этого - сериализация в JSON и вычисление JSON Patch. Для этого вы можете использовать класс JsonPatchDocument<T> (см. также JsonPatchDocument Class). Разница расскажет вам, что изменилось. (см. также этот вопрос)

вручную

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

Это очень легко реализовать, но, возможно, не лучший подход при работе с более сложными сценариями.