How would you implement dirty tracking in hierarchical view models?

569 views
Skip to first unread message

Kent Boogaart

unread,
Mar 8, 2013, 1:46:04 PM3/8/13
to reacti...@googlegroups.com
This is a design question more than anything else, and is possibly more of an Rx one than RxUI. However, it is very pertinent to UI use cases.

Suppose you have the following simple VM:

    public class ChildViewModel : ReactiveObject
    {
        private string name;
        private bool isSelected;
        
        public string Name
        {
            get { return this.name; }
            set { this.RaiseAndSetIfChanged(x => x.Name, ref this.name, value); }
        }
        
        public bool IsSelected
        {
            get { return this.isSelected; }
            set { this.RaiseAndSetIfChanged(x => x.IsSelected, ref this.isSelected, value); }
        }
    }

Importantly, changes to the Name property should result in the object becoming "dirty" whilst changes to IsSelected do not (it's just a property to support the view).

Now suppose you have this view model:

    public class ParentViewModel : ReactiveObject
    {
        private readonly ReactiveCollection<ChildViewModel> children;
        
        public ParentViewModel()
        {
            this.children = new ReactiveCollection<ChildViewModel>();
        }
        
        public ICollection<ChildViewModel> Children
        {
            get { return this.children; }
        }
        
        public bool IsDirty
        {
            get { // implementation discussed below }
        }
        
        public void Save()
        {
            // implementation discussed below
        }
    }
    
We also want the parent to know when it's dirty, which necessitates monitoring every child to see whether it has been modified. It also needs to reset the dirty flag whenever a save occurs.

Here is how I've implemented this. I'd love to hear any suggestions for improvements.

    public class ChildViewModel : ReactiveObject
    {
        private readonly Subject<Unit> dataChanged;
        private string name;
        private bool isSelected;
        
        public ChildViewModel()
        {
            this.dataChanged = new Subject<Unit>();
            this.ObservableForProperty(x => x.Name)
                .Select(_ => Unit.Default)
                .Subscribe(_ => this.dataChanged.OnNext(Unit.Default));
        }
        
        public string Name
        {
            get { return this.name; }
            set { this.RaiseAndSetIfChanged(x => x.Name, ref this.name, value); }
        }
        
        public bool IsSelected
        {
            get { return this.isSelected; }
            set { this.RaiseAndSetIfChanged(x => x.IsSelected, ref this.isSelected, value); }
        }

        // fires whenever data changes that affects the "dirty" status of this object
        // does not fire for other properties such as IsSelected
        public IObservable<Unit> DataChanged
        {
            get { return this.dataChanged; }
        }
    }
    
    public class ParentViewModel : ReactiveObject
    {
        private readonly Subject<Unit> dataChanged;
        private readonly Subject<Unit> saved;
        private readonly ReactiveCollection<ChildViewModel> children;
        private readonly ObservableAsPropertyHelper<bool> isDirty;
        
        public ParentViewModel()
        {
            this.dataChanged = new Subject<Unit>();
            this.saved = new Subject<Unit>();
            this.children = new ReactiveCollection<ChildViewModel>();
            
            // if any child's data changes or if the collection of children changes, then this parent is considered changed too
            Observable.Merge(this.children.ItemsAdded.Select(x => x.DataChanged))
                .Merge(this.children.Changed.Select(_ => Unit.Default))
                .Subscribe(_ => this.dataChanged.OnNext(Unit.Default));

            // we're dirty if we haven't saved since the last data change
            this.isDirty = this.dataChanged.Select(_ => true)
                .Merge(this.saved.Select(_ => false))
                .ToProperty(this, x => x.IsDirty);
        }
        
        public ICollection<ChildViewModel> Children
        {
            get { return this.children; }
        }
        
        public bool IsDirty
        {
            get { return this.isDirty.Value; }
        }
        
        public void Save()
        {
            // save logic here
            
            this.saved.OnNext(Unit.Default);
        }
    }
    
I have two concerns really:

1. Could it be simpler?
2. What happens when an item is removed from the children collection? Presumably the subscription is left hanging? Any way I can ensure that gets cleaned up?

Thanks,
Kent

Cameron MacFarland

unread,
Mar 8, 2013, 8:09:37 PM3/8/13
to reacti...@googlegroups.com
Here's how I tend to do it. I use PropertyChanged.Fody (https://github.com/Fody/PropertyChanged) to automatically inject property change info for my ViewModels. 

A feature of PropertyChanged.Fody is that if you have a boolean property called IsChanged it will automatically mark it as true whenever another property changes.

So, with that here is the code:
 
    public class ChildViewModel : ReactiveObject
    {
        public string Name { get; set; }
        
        [DoNotSetChanged]
        public bool IsSelected { get; set; }

        public bool IsChanged { get; set; }
    }

The code that PropertyChanged.Fody injects is fairly obvious. You can implement it by hand if necessary. 

    public class ParentViewModel : ReactiveObject
    {
        private readonly ReactiveCollection<ChildViewModel> children;

        public ParentViewModel()
        {
            this.children = new ReactiveCollection<ChildViewModel>();
            this.children.ChangeTrackingEnabled = true;

            this.children.Changed.Subscribe(c => IsDirty = children.Any(cvm => cvm.IsChanged));
        }

        public ICollection<ChildViewModel> Children
        {
            get { return this.children; }
        }

        public bool IsDirty { get; private set; }

        public void Save()
        {
            // save logic here

            foreach (var child in this.children)
                child.IsChanged = false;

Matthew Walton

unread,
Mar 9, 2013, 3:08:22 AM3/9/13
to reacti...@googlegroups.com
I did something very similar to that, but packaged it up a bit more automatically. All my ViewModels derive from a common base class which, in its protected constructor, uses reflection to set up all the bindings to children (and yes, it does also listen for child properties having new children swapped in and does the right thing). It identifies properties to track by decoration with a custom TrackPropertyAttribute class, and there's also a TrackCollectionAttribute which has some options for whether you're interested in changes in membership, changes in the items or both, and whether you want to remember which items in the collection have been added, removed or modified since the last global change reset (which is an impulse that flows the other way to make everything in the structure forget what it's learned about things changing). They also maintain enough state around this to expose a boolean property saying if they've changed or not, thus you can easily inspect any subtree to check for changes without having to have specifically subscribed to it in advance.

It was all rather fun to implement, although making sure it's right under all circumstances is a bit pernickety. Using attributes and reflection really cleans the code up though.

Another option we looked at to start with was to replace RaiseAndSetIfChanged with a custom TrackAndSetIfChanged, but ultimately we found that setting it all up in a protected base class constructor made things far easier to think about and get right, and the attribute syntax is something we were comfortable with having made extensive use of data validation attributes and structural decorations for our Entity Framework POCOs.

Oh and as for items removed from the child collections, my solution was to keep a structure around with the IDisposable for those subscriptions in, and then to listen to ItemsRemoved and ShouldReset to know when I should dispose of the subscriptions. It's not that hard, just a bit tedious and of course you don't want to leave any holes in it or you start leaking things all over the place.

Matthew

--
You received this message because you are subscribed to the Google Groups "ReactiveUI mailing list" group.
To unsubscribe from this group and stop receiving emails from it, send an email to reactivexaml...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Martin H. Andersen

unread,
Mar 18, 2013, 12:44:43 PM3/18/13
to reacti...@googlegroups.com
I like the dirty tracking implementation suggested by Kent but the objects I have is wrapped in a tree-view model. 

[DataMember]
public PropertyField Property { get; set; }
[DataMember]
public PropertyFieldViewModel Parent { get; set; }
[DataMember]
public ReactiveCollection<PropertyFieldViewModel> Children { get; set; }

Children.ChangeTrackingEnabled = true;
Children.Changed.Subscribe(_ => Property.IsChanged = Children.Any(cvm => cvm.Property.IsChanged));

My question is, can I listen to IsChanged one-level down?

Thanks, Martin
Reply all
Reply to author
Forward
0 new messages