Reputation: 1016
I have a ViewModel with:
I'd like to disable the command when none of the "Active" property from the MyObject list or from the single MyObject is true or in any case when IsBusy is true.
Before adding the IsBusy to the picture the best solution I came up with was:
var canSave = this.WhenAnyObservable(
x => x.MyObjectsList.ItemChanged, x => x.MyObject.Changed)
.Where(x => x.PropertyName == "Active")
.Select(_ => MyObjectsList.Any(x => x.Active) || MyObject.Active);
SaveCommand = ReactiveCommand.Create(Save, canSave);
The idea is that it re-evaluates every time an Active property changes. Not sure whether this is the best solution at all (therefore any suggestion to improve it is welcome), but definitely I'm unable to add the IsBusy to the picture in order to re-evaluate the Select clause (including IsBusy state) when IsBusy changes.
Upvotes: 0
Views: 637
Reputation: 662
Here's another alternative that avoids the need for MyObjectList.Any() which has O(n) complexity. This solution is a bit more involved but has potential for little better efficiency. It's the same as Glenn's CombineLatest approach except for the way his listItemChangedObs observable is calculated. This version keeps a running total of the number of active objects in the list. So, it only has to do a +1 or -1 every time ItemChanged is triggered. Then it just checks if it's greater than 0.
public MyViewModel()
{
var itemChangedObs = this.WhenAnyValue(x => x.MyObject.Active);
var isBusyObs = this.WhenAnyValue(x => x.IsBusy);
// Recalculate the # of active objects each time ObjectList is reassigned.
var activeListItemCountInitializedObs = this
.WhenAnyValue(x => x.ObjectList)
.Select(
list =>
{
// Return 0 if ObjectList is null.
return list == null ? Observable.Return(0) : list
.ToObservable()
// Otherwise, increment by 1 for each active object.
.Select(x => x.Active ? 1 : 0)
// We use Aggregate, which is a single value sequence, because
// we're only interested in the final result.
.Aggregate((acc, current) => acc + current);
})
// We no longer need the inner observable from the last time active item count
// was initialized. So unsubscribe from that one and subscribe to this most recent one.
.Switch();
var activeListItemCountChangedObs = this
.WhenAnyObservable(x => x.ObjectList.ItemChanged)
.Where(x => x.PropertyName == "Active")
// Increment or decrement the number of active objects in the list.
.Select(x => x.Sender.Active ? 1 : -1);
// An IObservable<bool> that signals if *any* of objects in the list are active.
var anyListItemsActiveObs = activeListItemCountInitializedObs
.Select(
// Use the initialized count as the starting value for the Scan accumulator.
initialActiveCount =>
{
return activeListItemCountChangedObs
.Scan(initialActiveCount, (acc, current) => acc + current)
// Return true if one or more items are active.
.Select(x => x > 0)
.StartWith(initialActiveCount > 0);
})
// ObjectList was completely reassigned, so the previous Scan accumulator is
// no longer valid. So we "reset" it by "switching" to the new one.
.Switch();
var canRunCommand = itemChangedObs
.CombineLatest(
anyListItemsActiveObs,
isBusyObs,
(itemActive, listItemActive, isBusy) => (itemActive || listItemActive) && !isBusy);
Save = ReactiveCommand.CreateFromObservable(() => Observable.Return(Unit.Default), canRunCommand);
}
And here's a Unit test that passed when I ran the code. It basically checks the number of times the ReactiveCommand's CanExecute changes state and if it's true or false, each time one of the variables changes.
[Fact]
public void TestMethod1()
{
var objectList = new ReactiveList<IMyObject>(
initialContents: new[] { new MyObject(), new MyObject() },
resetChangeThreshold: 0.3,
scheduler: ImmediateScheduler.Instance);
objectList.ChangeTrackingEnabled = true;
IMyViewModel myViewModel = new MyViewModel
{
ObjectList = objectList,
MyObject = new MyObject()
};
var canExecute = myViewModel.Save
.CanExecute
.CreateCollection(scheduler: ImmediateScheduler.Instance);
Assert.Equal(1, canExecute.Count);
Assert.False(canExecute[0]);
myViewModel.ObjectList[0].Active = true;
Assert.Equal(2, canExecute.Count);
Assert.True(canExecute[1]);
myViewModel.MyObject.Active = true;
Assert.Equal(2, canExecute.Count);
myViewModel.IsBusy = true;
Assert.Equal(3, canExecute.Count);
Assert.False(canExecute[2]);
myViewModel.IsBusy = false;
Assert.Equal(4, canExecute.Count);
Assert.True(canExecute[3]);
myViewModel.MyObject.Active = false;
Assert.Equal(4, canExecute.Count);
var object1 = new MyObject { Active = true };
var object2 = new MyObject { Active = true };
myViewModel.ObjectList = new ReactiveList<IMyObject>(
initialContents: new[] { object1, object2 },
resetChangeThreshold: 0.3,
scheduler: ImmediateScheduler.Instance);
Assert.Equal(4, canExecute.Count);
object1 = new MyObject { Active = false };
object2 = new MyObject { Active = false };
myViewModel.ObjectList = new ReactiveList<IMyObject>(
initialContents: new[] { object1, object2 },
resetChangeThreshold: 0.3,
scheduler: ImmediateScheduler.Instance);
Assert.Equal(5, canExecute.Count);
Assert.False(canExecute[4]);
}
Upvotes: 1
Reputation: 2888
I think your answer can be simplified.
var itemChangedObs = this.WhenAnyValue(x => x.MyObject.Active);
var isBusyObs = this.WhenAnyValue(x => x.IsBusy);
var listItemChangedObs = this.WhenAnyObservable(x => x.MyObectsList.ItemChanged).Where(x => x.PropertyName == "Active").Select(_ => MyObjectsList.Any(x => x.Active)).StartsWith(false)
var canRunCommand = itemChangedObs.CombineLatest(listItemChangedObs, isBusyObs, (itemActive, listItemActive, isBusy) => (itemActive || listItemActive) && !isBusy);
This version essentially uses a CombineLatest which takes a Lambda of what you'd like the value to be after combining the two observables together.
CombineLatest() does not produce values until both Observables have emitted a value, hence why listItemChanged has a StartsWith(false) at the front.
WhenAnyValue() will always emit by default the default(T) of the value as it's initial value so you don't need the StartsWith with those statements.
Upvotes: 3