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
{
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
{
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