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

我有一个包含5个属性的类。

如果任何值被分配给这些字段中的任何一个,则另一个值(例如IsDIrty)将改变为真。

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

要做到这一点,你不能真正使用自动getter和setter,你需要在每个setter中设置IsDirty。

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

 protected bool SetProperty(string name, ref T oldValue, T newValue) where T : System.IComparable { 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(string name, ref Nullable oldValue, Nullable newValue) where T : struct, System.IComparable { if (oldValue.HasValue != newValue.HasValue || (newValue.HasValue && oldValue.Value.CompareTo(newValue.Value) != 0)) { oldValue = newValue; PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name)); } } 

Dan的解决方案非常完美。

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

  • 在抽象类中实现INotifyPropertyChanged接口
  • IsDirty属性移动到抽象类
  • Class1和所有其他需要此function的类来扩展您的抽象类
  • 让所有setter触发抽象类实现的PropertyChanged事件,并将其名称传递给事件
  • 在您的基类中,侦听PropertyChanged事件并在触发时将IsDirty设置为true

最初创建抽象类有点工作,但它是一个更好的模型,用于监视数据更改,因为任何其他类在IsDirty (或任何其他属性)更改时都可以看到。

我的基类如下所示:

 public abstract class BaseModel : INotifyPropertyChanged { ///  /// Initializes a new instance of the BaseModel class. ///  protected BaseModel() { } ///  /// Fired when a property in this class changes. ///  public event PropertyChangedEventHandler PropertyChanged; ///  /// Triggers the property changed event for a specific property. ///  /// The name of the property that has changed. public void NotifyPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } } 

任何其他模型然后只是扩展BaseModel ,并在每个setter中调用NotifyPropertyChanged

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

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

您可以实现现在包含在.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 _Values = new Dictionary(); 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; } } 

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

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

更新:

对于一个简单的例子来说,这可能超过了顶部,但想出来很有趣!

在Visual Studio 2008中,如果您将一个名为CodeGen.tt的文件添加到项目中然后将其粘贴到其中,您将拥有代码生成系统的function:

 <#@ 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 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之外,它与此类似。

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

如果它的东西很小,请考虑实现INotifyPropertyChanged接口。

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

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

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

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

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

  ///  /// Update catalog; keeps track of update entity instances. ///  private Dictionary> updates = new Dictionary>(); public void UpdateNotification(object sender, PropertyChangedEventArgs e) { T source = (T)sender; if (!updates.ContainsKey(source)) updates.Add(source, new HashSet()); updates[source].Add(e.PropertyName); } 

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

这是在Rocky Lhokta的CLSA框架中构建到BusinessBase类中的内容,因此您可以随时查看它是如何完成的…

要支持枚举,请使用Binary Worrier的完美解决方案并添加以下代码。

我为自己添加了Enum支持(这很痛苦),我想这也很好。

 protected void SetEnumProperty(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 

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