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

问题描述 投票:34回答:11

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

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

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;}
}
c# .net
11个回答
42
投票

要做到这一点,你不能真正使用自动getter和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));
    }
}

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

0
投票

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


12
投票

您可以实现IChangeTrackingIRevertibleChangeTracking接口,现在包含在.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.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;  
  }   
}

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,并在每个setter中调用NotifyPropertyChanged


4
投票

在所有的二传手中将IsDirty设置为true。

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


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


1
投票

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

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


1
投票

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

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


1
投票

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

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

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

    /// <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这样做,这在某种程度上是一种考虑的做法。


0
投票

这是在Rocky Lhokta的BusinessBase框架中的CLSA类中构建的,所以你总是可以去看看它是如何完成的......

© www.soinside.com 2019 - 2024. All rights reserved.