在对象上实现更改跟踪的最佳方法是什么

时间:2010-03-02 14:33:32

标签: c# .net

我有一个包含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;}
}

13 个答案:

答案 0 :(得分:43)

要做到这一点,你不能真正使用自动吸气剂和放大器。 setter,你需要在每个setter中设置IsDirty。

我通常有一个“setProperty”泛型方法,它接受ref参数,属性名称和新值。 我在setter中调用它,允许我可以设置isDirty的单个点并提高Change通知事件,例如。

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));
    }
}

答案 1 :(得分:14)

您可以实现现在包含在.NET Standard 2.0中的IChangeTrackingIRevertibleChangeTracking接口。

实施如下:

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.PropertyChangedFody.PropertyChanging,并处理更改方法以缓存旧值并跟踪对象状态。 另一种选择是将对象的图形存储为MD5或其他一些哈希值,并在任何更改时重置它,您可能会感到惊讶,但如果您不期望有太多变化,并且如果您只是按需请求它,它可以真正起作用快。

以下是一个示例实现(注意:需要Json.NETFody/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;  
  }   
}

答案 2 :(得分:7)

Dan的解决方案非常完美。

另一个选择是考虑你是否必须在多个类上执行此操作(或者您希望外部类“监听”属性的更改):

  • 在抽象类中实现INotifyPropertyChanged接口
  • IsDirty属性移动到抽象类
  • Class1和所有其他需要此功能的类来扩展您的抽象类
  • 让您的所有setter触发抽象类实现的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

答案 3 :(得分:4)

在所有设置器中将IsDirty设置为true。

您也可以考虑将IsDirty的setter设为private(如果您的子类具有其他属性,则可以使用protected)。否则你可能会在类之外使用代码来否定其确定肮脏的内部机制。

答案 4 :(得分:3)

如果有大量此类类,都具有相同的模式,并且您经常需要更新其定义,请考虑使用代码生成自动吐出所有类的C#源文件,以便您不要不必手动维护它们。代码生成器的输入只是一个简单的文本文件格式,您可以轻松解析,说明每个类中所需属性的名称和类型。

如果只有少数几个,或者定义在开发过程中很少发生变化,那么它就不值得付出努力,在这种情况下你也可以手工维护它们。

更新:

对于一个简单的例子来说,这可能是最重要的,但是弄清楚它很有趣!

在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作为输出,其中包含类的C#源,并带有IsDirty逻辑。

您可以通过更改最后一个部分来更改生成内容的模板,它在模型中循环并生成代码。如果您使用过ASP.NET,那么除了生成C#源而不是HTML之外,它与此类似。

答案 5 :(得分:1)

仔细考虑需要对象跟踪的根本目的?假设它是否像其他对象必须基于另一个对象的状态做某事,那么考虑实现observer design pattern

如果它很小,可以考虑实现INotifyPropertyChanged接口。

答案 6 :(得分:1)

Dan's和Andy Shellam的答案都是我的最爱。

无论如何,如果你想改变你的TRACK,就像在日志中那样,你可能会考虑使用一个Dictionary,它会在收到通知更改时添加你所有的属性更改。因此,您可以使用唯一键将更改添加到词典中,并跟踪您的更改。然后,如果你希望Roolback在内存中你的对象的状态,你可以这样。

修改 以下是Bart de Smet用于跟踪整个LINQ到AD的房地产变化的信息。一旦将更改提交给AD,他就会清除词典。因此,当一个属性发生变化时,因为他实现了INotifyPropertyChanged接口,当一个属性实际发生变化时,他使用了一个Dictionary&gt;如下:

    /// <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);
    }

所以,我想如果Bart de Smet这样做,这在某种程度上是一种考虑的做法。

答案 7 :(得分:1)

我知道这是一个旧线程,但我认为Enumerations不适用于Binary Worrier的解决方案。你将得到一个设计时错误信息,即enum属性Type“不能在泛型类型或方法中用作类型参数'T'”...“SetProperty(string,ref T,T)'。没有装箱转换......“。

我引用了这个stackoverflow帖子来解决枚举问题:C# boxing enum error with generics

答案 8 :(得分:0)

这是Rocky Lhokta BusinessBase框架中的CLSA课程中的内容,所以你总是可以去看看它是如何完成的......

答案 9 :(得分:0)

要支持枚举,请使用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

答案 10 :(得分:0)

我知道你问这个问题已经有一段时间了。如果您仍然有兴趣让您的课程干净简单而不需要从基类派生,我建议您使用PropertyChanged.Fody实施的IsChanged Flag

答案 11 :(得分:0)

可以通过多种方式跟踪变更的优缺点。这里有一些想法:

观察者模式

在.NET中,最常见的方法是实现INotifyPropertyChangedINotifyPropertyChangeing和/或IObservable(另请参见Introduction to Rx)。提示:最简单的方法是将ReactiveUI库中的ReactiveObject用作基础对象。

使用此接口,您可以跟踪属性何时更改或更改。因此,这是对“实时”变化做出反应的最佳方法。

您还可以实现变更跟踪器,以跟踪更复杂情况下的所有情况。更改跟踪器可能在内部具有更改列表-列出所需的属性名称,值和时间戳。然后,您可以查询此列表以获取所需的信息。想一想EventSourcing模式。

序列化和差异

如果要查看对象是否已更改以及已更改的内容,可以序列化原始版本和当前版本。

此版本的一个版本是序列化为JSON并计算JSON Patch。您可以为此使用JsonPatchDocument<T>类(另请参见JsonPatchDocument Class)。差异会告诉您发生了什么变化。 (另请参见this question

手动

然后,还有一种具有多个属性以保持原始状态和当前状态的方法,并且可能是一个布尔值,它告诉您该字段是否已更改,但之后又更改回原始值。

这很容易实现,但是在处理更复杂的场景时可能不是最佳方法。

答案 12 :(得分:-1)

您可以使用 Roslyn 的 Source Generators,我找到了一个 roslyn-sdk github 示例。

我还创建了此示例的 cleaned version。 在我的版本中,生成器使用 .NET Standard 2.0,与 .NET Framework 4.7.2 兼容

您可以根据需要随意克隆和修改。