2

I am searching for a solution where i can ask a model if a property has changed. But i want to prevent to write own setter methods for all models and all their properties.

I want to use this to automatically generate a update queries based models and there changed properties. But if my model has a boolean property Test which is by default false, then i can't differentiate if the value is from the request payload or if it is the default value.

I already saw the INotifyPropertyChanged Implementation but there i have to write a setter for all properties too.

public class Main
{
    public static void main()
    {
        var person = new Person();

        Console.WriteLine(person.HasChanged("Firstname")); // false
        Console.WriteLine(person.HasChanged("Lastname")); // false
        Console.WriteLine(person.HasChanged("LikesChocolate")); // false

        person.Firstname = "HisFirstname";
        person.LikesChocolate = true;

        Console.WriteLine(person.HasChanged("Firstname")); // true
        Console.WriteLine(person.HasChanged("Lastname")); // false
        Console.WriteLine(person.HasChanged("LikesChocolate")); // true
    }
}

public class Person : BaseModel
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public bool LikesChocolate { get; set; }
}

public class BaseModel
{
    public bool HasChanged(string propertyName)
    {
        // ...
    }
}
KMathmann
  • 444
  • 3
  • 15
  • 1
    Have you considered using Entity Framework which solves this problem already? – Camilo Terevinto Aug 30 '18 at 14:50
  • There's nothing in .net that can do it out the box. – Adrian Aug 30 '18 at 15:00
  • Without modifying the model itself to tell you this, the only ways I can think of is to either read it back from the storage to compare the values one-by-one to see if they changed, or to serialize it and compare it to the stored value. Some aspect-oriented frameworks like Fody or Postsharp may be able to help here. – Ron Beyer Aug 30 '18 at 15:01
  • You can take a look at the simple implementations of the INotifyPropertyChanged in WPF, does some simple base class and handle change just internally, save if the object was changed or not. No need to use that framework, but use the idea. – kyrylomyr Aug 30 '18 at 15:08
  • Brave Idea: 1.: you need a method to indicate that the current state is the one you want to compare against. in that mehtod, use reflection to get and read all properties values, and put them in a dictionary. 2.: when the "HasChanged" method is called, check the dictionary against the current value. have no time to implement this now, but it 'should' work and give the result you need. – Chris Aug 30 '18 at 15:09
  • @Chris Such extensive usage of reflection would slow down application significantly. – kyrylomyr Aug 30 '18 at 15:10
  • @KyryloM: it always depends. there is no context on what the performance is supposed to be or what the frequency of the method calls is. also reflection became alot faster in the recent years :) – Chris Aug 30 '18 at 15:12
  • Have you considered using "snippets" in visual studio? They can template common code for you, and I've written a few of them for things like customised INotifyPropertyChanged properties. It's as simple as typing "propc" and hitting tab! (propc here is my snippet for a customised property changed with getter and setter). – Dan Rayson Aug 31 '18 at 07:38
  • take a look at [Fody](https://github.com/Fody/PropertyChanged) or any other [AOP framework](https://en.wikipedia.org/wiki/Aspect-oriented_programming) that does it for you (modifies IL and adds `OnPropertyChanged` – Konrad Aug 31 '18 at 07:38
  • What should be result if you call `HasChanged` multiple times in a row? You don't have any method which should reset state. – Maxim Kosov Aug 31 '18 at 08:40

3 Answers3

1

I'd probably reuse the idea from WPF with their INotifyPropertyChanged pattern and simplify it a bit for the current needs. However, it resolves the question only partially, as you still need to write setters. But at least, you don't need to manage each property on its own.

So, the solution will be something like this:

void Main()
{
    var person = new Person();

    Console.WriteLine(person.HasChanged(nameof(Person.FirstName))); // false
    Console.WriteLine(person.HasChanged(nameof(Person.LastName))); // false
    Console.WriteLine(person.HasChanged(nameof(Person.LikesChocolate))); // false

    person.FirstName = "HisFirstname";
    person.LikesChocolate = true;

    Console.WriteLine(person.HasChanged(nameof(Person.FirstName))); // true
    Console.WriteLine(person.HasChanged(nameof(Person.LastName))); // false
    Console.WriteLine(person.HasChanged(nameof(Person.LikesChocolate))); // true
}

public class Person : ChangeTrackable
{
    private string _firstName;
    private string _lastName;
    private bool _likesChocolate;

    public string FirstName
    {
        get { return _firstName; }
        set { SetProperty(ref _firstName, value); }
    }

    public string LastName
    {
        get { return _lastName; }
        set { SetProperty(ref _lastName, value); }
    }

    public bool LikesChocolate
    {
        get { return _likesChocolate; }
        set { SetProperty(ref _likesChocolate, value); }
    }
}

public class ChangeTrackable
{
    private ConcurrentDictionary<string, bool> _changes =
       new ConcurrentDictionary<string, bool>();

    public bool HasChanged(string propertyName)
    {
        return _changes.TryGetValue(propertyName, out var isChanged)
                   ? isChanged : false;
    }

    public void ResetChanges()
    {
       _changes.Clear();
    }

    protected void SetProperty<T>(
        ref T storage, T value, [CallerMemberName] string propertyName = "")
    {
        if (!Equals(storage, value))
        {
            _changes[propertyName] = true;
        }
    }
}

The ChangeTrackable tracks if property was changed and does it without any reflection that guarantees high performance. Note, that with this implementation you need to call ResetChanges if you initialize property with some actual values after constructing the object. Drawback is that you need to write each property with its backing field and call SetProperty. On the other side, you decide what to track, that could be handy in the future in your application. Also we don't need to write property as strings (thanks to compile-time CallerMemberName and nameof) that simplifies refactorings.

kyrylomyr
  • 12,192
  • 8
  • 52
  • 79
0

INotifyPropertyChanged is the established practice for this type of requirement. Part of keeping your code maintainable is by keeping it predictable and by adopting best practices and patterns.

An alternative, which I wouldn't recommend, would be to use reflection to iterate over all of your properties and dynamically add a property changed event handler. This handler could then set a boolean flag which can be returned by your HasChanges method. Please refer to this for a staring point: AddEventHandler using reflection

I would recommend avoiding unnecessary complexity though and stick with PropertyChanged notifications in your setters.

Mehdi Ibrahim
  • 2,434
  • 1
  • 13
  • 13
0

As followup for my comment a proof of concept (online):

using System.Reflection;

public class HasChangedBase
{
    private class PropertyState
    {
        public PropertyInfo Property {get;set;}
        public Object Value {get;set;}
    }

    private Dictionary<string, PropertyState> propertyStore;

    public void SaveState()
    {
        propertyStore = this
            .GetType()
            .GetProperties()
            .ToDictionary(p=>p.Name, p=>new PropertyState{Property = p, Value = p.GetValue(this)});
    }

    public bool HasChanged(string propertyName)
    {
        return propertyStore != null
            && propertyStore.ContainsKey(propertyName)
            && propertyStore[propertyName].Value != propertyStore[propertyName].Property.GetValue(this);
    }
}

public class POCO : HasChangedBase
{
    public string Prop1 {get;set;}
    public string Prop2 {get;set;}
}

var poco = new POCO();
poco.Prop1 = "a";
poco.Prop2 = "B";

poco.SaveState();
poco.Prop2 = "b";

poco.HasChanged("Prop1");
poco.HasChanged("Prop2");

Be aware, that reflection may reduce the performance of your application when used extensively.

Chris
  • 527
  • 3
  • 15