Reputation: 6242
I'm migrating from reactive 4.5 to 6.5.0 and I encounter some issues. I have a WPF app with button bound to ReactiveCommand. Previously I was using ReactiveCommand constructor like this:
_runProcessCommand = new ReactiveCommand(CanRunProcess(null));
_runProcessCommand.Subscribe(RunImpl);
public IObservable<bool> CanRunProcess(object arg)
{
return this.WhenAny( ... )
}
Now I've changed it to be:
_runProcessCommand = ReactiveCommand.Create(CanRunProcess(null));
_runProcessCommand..Subscribe(RunImpl);
So I expected that the behaviour should be exactly the same but it isn't. My button is disabled until I change something from WhenAny in CanRunProcess
bound which are basically properties from UI. It happens in many places in the project so there is no mistake. Is anything different between these two ways of creating ReactiveCommand? How to achieve the same result? The funny thing is when I subscribe to CanExecuteObservable it works as expected:
_runProcessCommand.CanExecuteObservable.Subscribe(x =>
{
Debug.WriteLine(x);
});
and it's the same when I Invoke CanExecute explicitly:
var c = _runProcessCommand.CanExecute(null);
I suppose it may be related with lazyness somewhere but I don't understand why it would be the case because button should invoke CanExecute to get the current initial value.
When I subscribe to CanRunProcess I get a lot of falses followed by a lot of trues and the last value is true which I suspect should enable the command.
CanRunProcess(null).Subscribe(x =>
{
Debug.WriteLine(x);
});
EDIT:
I've downloaded ReactiveUI sources and I've noticed that there is no subscription to canExecute but instead Do
function is being used:
this.canExecute = canExecute.CombineLatest(isExecuting.StartWith(false), (ce, ie) => ce && !ie)
.Catch<bool, Exception>(ex => {
exceptions.OnNext(ex);
return Observable.Return(false);
})
.Do(x => {
var fireCanExecuteChanged = (canExecuteLatest != x);
canExecuteLatest = x;
if (fireCanExecuteChanged) {
this.raiseCanExecuteChanged(EventArgs.Empty);
}
})
.Publish();
It looks like something needs to instantiate it - something needs to call
either CanExecuteObservable
or CanExecute
to instantiate canExecute object. Why isn't it created when you bind it to the button?
After debugging ReactiveUI sources I know exactly what happens. Do
is lazy function so until connect
function is invoked handler won't be executed. It means that canExecuteLatest
will be false when command is being bound to the button and when calling CanExecute
function, so the button stays disabled.
Reproducable example (note it works when I do the same example with WhenAny):
public class MainViewModel : ReactiveObject
{
private ReactiveCommand<object> _saveCommand;
private string _testProperty;
private ReactiveList<string> _ReactiveList;
public ReactiveCommand<object> SaveCommand
{
get
{
return _saveCommand;
}
set { this.RaiseAndSetIfChanged(ref _saveCommand, value); }
}
public ReactiveList<string> ReactiveList
{
get
{
return _ReactiveList;
}
set { this.RaiseAndSetIfChanged(ref _ReactiveList, value); }
}
public MainViewModel()
{
ReactiveList = new ReactiveList<string>();
ReactiveList.ChangeTrackingEnabled = true;
SaveCommand = ReactiveCommand.Create(CanRunSave(null));
SaveCommand.Subscribe(Hello);
// SaveCommand.CanExecute(null); adding this line will invoke connect so the next line will run CanSave and enable the button.
ReactiveList.Add("sad");
}
public void Hello(object obj)
{
}
private IObservable<bool> CanRunSave(object arg)
{
return ReactiveList.Changed.Select(x => CanSave());
}
private bool CanSave()
{
return ReactiveList.Any();
}
}
<Window x:Class="WpfApplication8.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="test" Command="{Binding SaveCommand}" />
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
Button is still disabled even though I add something to ReactiveList. The problem is that updates between creating the command and binding it to the button are ignored because connect wasn't called so the changes are simply not reflected.
Upvotes: 0
Views: 250
Reputation: 26233
The issue in your example is that the Changed
event on ReactiveList<T>
is essentially a hot observable. It is producing changes that occur even if no observer is subscribed. When an observer does subscribe, any previous changes will have been missed.
The result of this is that a subscriber to CanRunSave
will not get any initial value. The first value received will be the result of the first change to the ReactiveList
(e.g. the next addition/removal) after subscription.
As a result of the laziness in ReactiveCommand
, any change to the list before CanExecute
is called (which is when the observable is subscribed to) will be missed. On subscription, there will be no initial value, so command's 'can execute' state will be the default of false
until the list is changed.
The fix is surprisingly simple - make sure there is an initial value on subscription. You can do this using StartWith
:
private IObservable<bool> CanRunSave(object arg)
{
return ReactiveList.Changed.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Select(_ => CanSave());
}
Upvotes: 1