49

I have a class which contains 5 properties.

If any value is assingned to any of these fields, an another value (for example IsDIrty) would change to 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;}
}
Ashkan S
  • 10,464
  • 6
  • 51
  • 80
user137348
  • 10,166
  • 18
  • 69
  • 89

13 Answers13

50

To do this you can't really use automatic getter & setters, and you need to set IsDirty in each setter.

I generally have a "setProperty" generic method that takes a ref parameter, the property name and the new value. I call this in the setter, allows a single point where I can set isDirty and raise Change notification events e.g.

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));
    }
}
Stanislav
  • 1,074
  • 1
  • 9
  • 11
Binary Worrier
  • 50,774
  • 20
  • 136
  • 184
  • 29
    Good answer (+1). My only suggestion would be to implement IChangeTracking (http://msdn.microsoft.com/en-us/library/system.componentmodel.ichangetracking.aspx) rather than creating your own IsDirty property. – John Myczek Mar 02 '10 at 15:33
  • 3
    John Myczek: I was completely unaware of that interface, I shall switch my pet projects to use this immediately (or as soon as that pesky `life` thing allows). Thanks :) – Binary Worrier Mar 02 '10 at 15:51
  • 1
    Didn't know about that either John – Chris Marisic Mar 02 '10 at 16:14
  • 5
    You should use `IEquatable` instead of `IComparable`, and you don't need to test if `oldValue` is null. Just use `if (oldValue.Equals(newValue)) return;`. – Sam Harwell Mar 02 '10 at 16:27
  • 4
    Event invocation here is not thread-safe. You need to write `var handler = PropertyChanged; if (handler != null) handler(this, e);` – Aaronaught Mar 02 '10 at 17:06
  • +1 Thanks a lot for the answer. Maybe it is by design, but it looks like you are missing the IsDirty = true in the void SetProperty() method. – TyCobb Mar 04 '12 at 18:30
  • Thats how i implemented it too without help. But i extended it a bit more to handle properties whichs values might change when another property has been changed ( Like the property `Width` when `Size` has changed ). I'm not happy with my solution so i would be interested how you would solve this? – Felix K. Jul 07 '13 at 06:35
  • Felix K. I'd have Width & Size properties, that both set a third "Dimension" property, that's the property that fires a "changed" notification ? – Binary Worrier Jul 29 '13 at 08:18
  • @280Z28 Could you answer here with your `IEquatable` best practice? Can't seem to get it done, but I need it, since enums are not IComparable – Nick N. Feb 21 '14 at 10:08
  • HI @BinaryWorrier, do you have any sample on how to use this SetProperty method in an object? Appreciate it – VAAA May 06 '14 at 00:01
33

You can implement the IChangeTracking or IRevertibleChangeTracking interfaces, now included in .NET Standard 2.0.

Implementation is as follows:

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

Another option, which I like the most, is to use a change tracking library, such as TrackerDog, that generates all the boilerplate code for you, while just need to provide POCO entities.

There are more ways to achieve this if you don't want to implement all the properties by hand. One option is to use a weaving library, such as Fody.PropertyChanged and Fody.PropertyChanging, and handle the change methods to cache old values and track object state. Another option is to have the object's graph stored as MD5 or some other hash, and reset it upon any change, you might be surprised, but if you don't expect zillion changes and if you request it only on demand, it can work really fast.

Here is an example implementation (Note: requires Json.NET and 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;  
  }   
}
Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
  • Quick note: GetHash method doesn't work - it always produce the same hash. Adding writer.Flush() and stream.Position = 0 fix that. – darthkurak Mar 20 '19 at 13:59
  • wath if im settings values from database like a.Propr = datareader.getString(0) of from a linq "from a in list select new B { Prop = a.Prop }" for example – juan carlos peña cabrera May 25 '20 at 19:15
  • 1
    Question: I'm actually looking at implementing the IRevertibleChangeTracking pattern you mentioned... but doesn't the code you're using set "IsChanged" to true on first load, like when deserializing from a service? – Robert McLaws May 11 '21 at 22:01
  • @Shimmy: You can also track changes of any POCO object in an external change tracker, just like Entity Framework Core's Change tracker. Store the original values somewhere else and then compare them to actual values – Liero Aug 13 '23 at 08:15
9

Dan's solution is perfect.

Another option to consider if you're going to have to do this on multiple classes (or maybe you want an external class to "listen" for changes to the properties):

  • Implement the INotifyPropertyChanged interface in an abstract class
  • Move the IsDirty property to your abstract class
  • Have Class1 and all other classes that require this functionality to extend your abstract class
  • Have all your setters fire the PropertyChanged event implemented by your abstract class, passing in their name to the event
  • In your base class, listen for the PropertyChanged event and set IsDirty to true when it fires

It's a bit of work initially to create the abstract class, but it's a better model for watching for data changes as any other class can see when IsDirty (or any other property) changes.

My base class for this looks like the following:

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

Any other model then just extends BaseModel, and calls NotifyPropertyChanged in each setter.

Andy Shellam
  • 15,403
  • 1
  • 27
  • 41
4

Set IsDirty to true in all of your setters.

You might also consider making the setter for IsDirty private (or protected, if you may have child classes with additional properties). Otherwise you could have code outside of the class negating its internal mechanism for determining dirtiness.

Dan Tao
  • 125,917
  • 54
  • 300
  • 447
3

If there are a very large number of such classes, all having that same pattern, and you frequently have to update their definitions, consider using code generation to automatically spit out the C# source files for all the classes, so that you don't have to manually maintain them. The input to the code generator would just be a simple text file format that you can easily parse, stating the names and types of the properties needed in each class.

If there are just a small number of them, or the definitions change very infrequently during your development process, then it's unlikely to be worth the effort, in which case you may as well maintain them by hand.

Update:

This is probably way over the top for a simple example, but it was fun to figure out!

In Visual Studio 2008, if you add a file called CodeGen.tt to your project and then paste this stuff into it, you'll have the makings of a code generation system:

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

    }

<# } #>
}

There's a simple string literal called src in which you declare the classes you need, in a simple format:

Foo:     string Prop1,
         int Prop2;

Bar:     string FirstName,
         string LastName,
         int Age;

So you can easily add hundreds of similar declarations. Whenever you save your changes, Visual Studio will execute the template and produce CodeGen.cs as output, which contains the C# source for the classes, complete with the IsDirty logic.

You can change the template of what is produced by altering the last section, where it loops through the model and produces the code. If you've used ASP.NET, it's similar to that, except generating C# source instead of HTML.

Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
2

Both Dan's and Andy Shellam's answers are my favorites.

In anyway, if you wanted to keep TRACK of you changes, like in a log or so, you might consider the use of a Dictionary that would add all of your property changes when they are notified to have changed. So, you could add the change into your Dictionary with a unique key, and keep track of your changes. Then, if you wish to Roolback in-memory the state of your object, you could this way.

EDIT Here's what Bart de Smet uses to keep track on property changes throughout LINQ to AD. Once the changes have been committed to AD, he clears the Dictionary. So, when a property changes, because he implemented the INotifyPropertyChanged interface, when a property actually changed, he uses a Dictionary> as follows:

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

So, I guess that if Bart de Smet did that, this is somehow a practice to consider.

Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
Will Marcouiller
  • 23,773
  • 22
  • 96
  • 162
  • 2
    I got berated by a colleague recently for doing something similar - apparently this will box value types such as int and doubles into .NET types, resulting in a performance penalty. It wasn't so much of a worry on the small(ish) app I'm working on, but could be something to watch out for on larger apps. – Andy Shellam Mar 02 '10 at 15:33
  • Interesting! Thanks for this comment, I perhaps would not have gotten aware of it before I make it myself. I shall be warned then. =) – Will Marcouiller Mar 02 '10 at 15:37
  • 1
    Thus, when I come to think of it, Bart de Smet, from Microsoft, the one who designed LINQ to AD on CodePlex, uses a Dictionary> to keep track of property changes. I'll edit my answer to indlude the sample code. – Will Marcouiller Mar 02 '10 at 16:53
  • @AndyShellam There is no way to avoid boxing of primitive values when unifying on object (or an interface). If such boxing is a problem then such is not the right tool for the job anyway - and yes, I realize the comment is 5 years old. – user2864740 Feb 25 '15 at 17:44
  • This would also nicely work with Blazor and EditContext.OnFieldChanged – Liero Aug 13 '23 at 08:19
2

There are multiple ways for change tracking with their pros and cons. Here are some ideas:

Observer Pattern

In .NET, the most common approach is to implement INotifyPropertyChanged, INotifyPropertyChangeing, and/or IObservable (see also Introduction to Rx). Tipp: The easiest way to do this is to use ReactiveObject from the ReactiveUI library as base object.

Using this interfaces, you can track when a property is changing or has changed. So this is the best method to react to changes in „real-time“.

You can also implement a change tracker to keep track of everything for more complex scenarios. The change tracker might have a list of the changes—listing property names, values, and timestamps as needed—internally. You can than query this list for the information you need. Think of something like the EventSourcing pattern.

Serialization and Diff

If you want to see if an object has changed and what has changed, you could serialize the original version and the current version.

One version of this is to serialize to JSON and calculate an JSON Patch. You can use the JsonPatchDocument<T> class (see also JsonPatchDocument Class) for this. The diff will tell you what has changed. (see also this question)

Manually

Then there is also the method of having multiple properties to keep the original state and the current state, and maybe an boolean that tells you if the field has changed but was changed back to the original value later.

This is very easy to implement, but might not be the best approach when handling more complex scenarios.

MovGP0
  • 7,267
  • 3
  • 49
  • 42
1

I know this is an old thread, but I think Enumerations will not work with Binary Worrier's solution. You will get a design-time error message that the enum property Type "cannot be used as type parameter 'T' in the generic type or method"..."SetProperty(string, ref T, T)'. There is no boxing conversion...".

I referenced this stackoverflow post to solve the issue with enumerations: C# boxing enum error with generics

Community
  • 1
  • 1
iCode
  • 1,254
  • 1
  • 13
  • 16
1

Carefully consider the underlying purpose the object tracking is required? Suppose if it is something like other objects have to do something based on another object's state, then consider implementing the observer design pattern.

If its something small consider implementing the INotifyPropertyChanged interface.

deostroll
  • 11,661
  • 21
  • 90
  • 161
1

This is something that is built into the BusinessBase class in Rocky Lhokta's CLSA framework, so you could always go and look at how it's done...

Alexander
  • 2,320
  • 2
  • 25
  • 33
rohancragg
  • 5,030
  • 5
  • 36
  • 47
0

To support enums, please use the perfect solution of Binary Worrier and add the code below.

I have added Enum support for my own (and it was a pain), I guess this is nice to add as well.

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

And implemented via:

    Public Property CustomerTyper As CustomerTypeEnum
        Get
            Return _customerType
        End Get
        Set(value As ActivityActionByEnum)
            SetEnumProperty("CustomerType", _customerType, value)
        End Set
    End Property
Nick N.
  • 12,902
  • 7
  • 57
  • 75
0

I know that it's been a while since you asked this. If you're still interested to keep your classes clean and simple without the need of deriving from base classes, I would suggest to use PropertyChanged.Fody that has an IsChanged Flag implemented

mamuesstack
  • 1,111
  • 2
  • 16
  • 34
0

There actually is a way by checking the object fields. I know this isn't the way that you wanted, you need an extra call to check it but you can use this Script:

using System.Reflection;
using System.Collections.Generic;

namespace Tracking
{
   public static class Tracker
   {
       public static List<(string Key, (FieldInfo Info, object Value)[] Fields)> Items = new List<(string Key, (FieldInfo Info, object Value)[] Fields)>();

       public static void Dispose(string Key)
       {
           for (int i = 0; i < Items.Count; i++)
           {
               if (Items[i].Key == Key)
               {
                   Items.RemoveAt(i);
                   break;
               }
           }
       }

      
       public static bool? IsChanged(this object Value, string Key)
       {
           
           for (int i = 0; i < Items.Count; i++)
           {
               var Item = Items[i];

               if (Item.Key == Key)
               {
                   for (int j = 0; j < Item.Fields.Length; j++)
                   {
                       if (Item.Fields[j].Info.GetValue(Value) != Item.Fields[j].Value)
                       {
                           Item.Fields[j].Value = Item.Fields[j].Info.GetValue(Value);

                           return true;
                       }
                   }

                   return false;
               }
           }

           var list = new List<(FieldInfo, object)>();
           var Fields = Value.GetType().GetFields();

           for (int i = 0; i < Fields.Length; i++)
           {
               list.Add((Fields[i], Fields[i].GetValue(Value)));
           }
           Items.Add((Key, list.ToArray()));

           return null;
       }

       public static FieldInfo[] Track(this object Value, string Key)
       {
           for (int i = 0; i < Items.Count; i++)
           {
               var Item = Items[i];

               if (Item.Key == Key)
               {
                   var result = new List<FieldInfo>();

                   for (int j = 0; j < Item.Fields.Length; j++)
                   {
                       if (Item.Fields[j].Info.GetValue(Value) != Item.Fields[j].Value)
                       {
                           Item.Fields[j].Value = Item.Fields[j].Info.GetValue(Value);

                           result.Add(Item.Fields[j].Info);
                       }
                   }

                   if (result.Count > 0) { return result.ToArray(); }
               }
           }

           var list = new List<(FieldInfo, object)>();
           var Fields = Value.GetType().GetFields();

           for (int i = 0; i < Fields.Length; i++)
           {
               list.Add((Fields[i], Fields[i].GetValue(Value)));
           }
           Items.Add((Key, list.ToArray()));

           return null;
       }
   }
}