6
\$\begingroup\$

I have a class, which stores an enum to track if my object is modified, newly created, or no change. If the object is modified, I have additional booleans for each field that is modified.

For instance,

public enum status
{
    NoChange,
    Created,
    Modified
}

private bool? name;
private bool? address;
...
private bool? numberOfDonkeysPurchased;

So the idea is that if the status is modified, then the nullable bools will be either true or false, depending on if these fields are changed.

If the status is not modified, then the nullable bools will be null.

The problem with this is that I have a couple of these fields that I want to check (say 10). Is it valid to create a nullable bool for each field that I am checking, knowing that I maybe be storing a lot of nulls? Or is that not a concern?

Is there a better way to store this?

Thanks!

\$\endgroup\$
5
  • \$\begingroup\$ Are you manually adding a new bool? each time you add a new property? Or are you using something to auto-generate your code? In the former case, this looks like very painful code to modify... \$\endgroup\$
    – Bobson
    Commented Mar 11, 2013 at 17:42
  • \$\begingroup\$ @Bobson Yes, a new bool? will need to be added for each new property. \$\endgroup\$
    – Rhs
    Commented Mar 11, 2013 at 17:52
  • 2
    \$\begingroup\$ You might want to look into an observable dictionary. You can also generate your bools automatically using a T4 template. \$\endgroup\$
    – Leonid
    Commented Mar 11, 2013 at 18:03
  • 1
    \$\begingroup\$ @Leonid - Yeah, that's where I was going. Auto-generated code would be much easier to do this for. \$\endgroup\$
    – Bobson
    Commented Mar 11, 2013 at 18:09
  • \$\begingroup\$ Actually, what does NoChange mean? Has not changed since the last time I looked at it? This is subjective to a consumer. What if you have multiple consumers? Why not just sore the created time and modified time that is associated with every value, and keep track of it that way. You can use a dict for that. Alternatively, borrow some ideas from git or Clojure - make your objects immutable and track every change by storing a new value in a LinkedList along with who made a change and when. That way you can have all of the diffs. There is more than one way to implement this; it depends on usage. \$\endgroup\$
    – Leonid
    Commented Mar 11, 2013 at 18:23

3 Answers 3

4
\$\begingroup\$

Alternatively, you could use a bitmask via a Flags enum:

[Flags]
public enum Changes
{
   None                     = 0x000,
   Name                     = 0x001,
   Address                  = 0x002,
   ...
   NumberOfDonkeysPurchased = 0x100,
   ...
}

Then, you merely update the appropriate flags as you call setters:

public string Name
{
   get { return name; }
   set
   {
      name = value;
      changes |= Changes.Name;
   }
}

Checking for changes is just a matter of comparing changes with Change.None.

Note: this will be preferable to a collections object if you are transmitting your data between a client and a server, since the bitmask will be stored as a single int or long.

\$\endgroup\$
4
  • \$\begingroup\$ The downside is maintaining the enum when new properties are added/renamed but a neat idea. \$\endgroup\$ Commented Mar 12, 2013 at 22:01
  • \$\begingroup\$ @TrevorPilley - That's no worse than adding a new bool when new properties are added. \$\endgroup\$
    – Bobson
    Commented Mar 13, 2013 at 18:16
  • \$\begingroup\$ @Bobson that's true, it is still an additional maintenance task (especially if it's across multiple objects) unless you have some clever T4 skills and it's practical to use templates \$\endgroup\$ Commented Mar 13, 2013 at 22:13
  • \$\begingroup\$ Care should be taken when passing this from one platform to another as the bit order may differ. \$\endgroup\$
    – bloudraak
    Commented Mar 15, 2013 at 18:30
4
\$\begingroup\$

I would suggest that you use a Dictionary, with the property as the key and the bool as the value. e.g.

Dictionary<string, bool?> myProperties = new Dictionary<string, bool?>();

Use reflection to get all the properties from your object. Then you dont have any work when properties gets added/removed.

You can initialise your dictionary with reflection like this:

foreach (PropertyInfo propertyInfo in myObject.GetType().GetProperties()) {
    myProperties.Add(propertyInfo.Name, false);
}

Or you can even only add those that are not null.

\$\endgroup\$
3
  • \$\begingroup\$ +1 - This is what I had in mind, but I've been too busy today to write up. Only thing I'd add would be an PropertyChanged(string propname) function to called from each property's setter to automatically update the dictionary. \$\endgroup\$
    – Bobson
    Commented Mar 11, 2013 at 19:12
  • \$\begingroup\$ The only downside to this is that if you create 100 instances of that object, you are running that reflection code 100 times. \$\endgroup\$ Commented Mar 12, 2013 at 12:07
  • \$\begingroup\$ @TrevorPilley: if running the reflection code with each new instance is a concern, it can be optimized by adding a static Set which gets initialized once with the property names. Then each instance only needs to load the map with the field names in the set (and no need to do further reflection on the properties). \$\endgroup\$ Commented Mar 13, 2013 at 13:20
4
\$\begingroup\$

You could use INotifyPropertyChanged which would also mean that your object can be databound easily. Something like this may be suitable:

public abstract class ChangeTrackingObject : INotifyPropertyChanged
{
    private readonly HashSet<string> changedProperties = new HashSet<string>();

    protected ChangeTrackingObject()
    {
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public IEnumerable<string> ChangedProperties
    {
        get
        {
            return this.changedProperties;
        }
    }

    public bool PropertyWasChanged(string member)
    {
        return this.changedProperties.Contains(member);
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string member = "")
    {
        this.changedProperties.Add(member);

        var propertyChanged = this.PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(member));
        }
    }
}

The [CallerMemberName] is new in C#5 which does some compile time magic to put the correct string in place. Using a HashSet<string> instead of List<string> ensures that we get unique values only and no duplicates without having to call List<T>.Contains()

public class Thing : ChangeTrackingObject
{
    private string address;
    private string name;
    private int numberOfDonkeysPurchased;

    public string Address
    {
        get
        {
            return address;
        }
        set
        {
            address = value;
            this.OnPropertyChanged();
        }
    }

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            this.OnPropertyChanged();
        }
    }

    public int NumberOfDonkeysPurchased
    {
        get
        {
            return numberOfDonkeysPurchased;
        }
        set
        {
            numberOfDonkeysPurchased = value;
            this.OnPropertyChanged();
        }
    }
}

Example usage:

var thing = new Thing();

System.Console.WriteLine("NumberOfDonkeysPurchased Changed? " + thing.PropertyWasChanged("NumberOfDonkeysPurchased"));
thing.NumberOfDonkeysPurchased = 5;
System.Console.WriteLine("NumberOfDonkeysPurchased Changed? " + thing.PropertyWasChanged("NumberOfDonkeysPurchased"));

System.Console.WriteLine("Changed Properties");
thing.ChangedProperties.ToList().ForEach(System.Console.WriteLine);
\$\endgroup\$
2
  • \$\begingroup\$ I'd move the responsibility for change tracking to some other object that subscribes to NotifyProperyChanged. \$\endgroup\$ Commented Mar 13, 2013 at 8:43
  • \$\begingroup\$ That is something that would be easy to do if you wanted. \$\endgroup\$ Commented Mar 13, 2013 at 8:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.