3

In my MVVM application, when a ViewModel gets activated, a Task gets started that establishes a network connection and could take some time to complete. This Task is cancalable:

private async Task ConnectAsync(CancellationToken cancellationToken = default)
{
    ...
}

I'm using IActivatableViewModel to start it on ViewModel-activation like that:

// Constructor:
public SomeViewModel(...)
{
    this.WhenActivated(disposable => {
        Observable.StartAsync(ConnectAsync);
    });
}

Now what is the recommended method to cancel this long-running Task when the ViewModel gets deactivated before the Task completes?

I came up with this:

this.WhenActivated(disposable => {
    Observable.StartAsync(ConnectAsync).Subscribe().DisposeWith(disposable);
});

Is this the right solution or is there a better one?

Thank you in advance!

Marcus Wichelmann
  • 762
  • 1
  • 6
  • 18

1 Answers1

1

Yeah, the code like you show in your code snippet looks good. However, probably worth moving the ConnectAsync method call to a ReactiveCommand<TInput, TOutput> (docs). If you do this, you get such perks as the ability to subscribe to ThrownExceptions and IsExecuting observables, and then display some loading indicators or error messages to keep your users informed about what the app is doing. Also, following the pattern described here, you can cancel that ReactiveCommand<TInput, TOutput> via another command or event. Cancelation via an event would look like this:

// ViewModel.cs
Cancel = ReactiveCommand.Create(() => { });
Connect = ReactiveCommand
    .CreateFromObservable(
        () => Observable
            .StartAsync(ConnectAsync)
            .TakeUntil(Cancel));

// View.xaml.cs
this.WhenActivated(disposable => {
    this.Events() // Launch the long-running operation
        .Loaded
        .Select(args => Unit.Default)
        .InvokeCommand(ViewModel, x => x.Connect)
        .DisposeWith(disposable);
    this.Events() // Stop that long-running operation
        .Unloaded
        .Select(args => Unit.Default)
        .InvokeCommand(ViewModel, x => x.Cancel)
        .DisposeWith(disposable);
});

Here, I assume ConnectAsync is a method accepting a cancelation token and returning a Task. In order to enable the this.Events() magic, you need to either use Pharmacist, or to install one of the ReactiveUI.Events packages. But anyway, your option looks good as well if you want to rely on WhenActivated, don't need ThrownExceptions, IsExecuting etc. If you'd like to use commands and rely on WhenActivated, then modify the View.xaml.cs code:

// View.xaml.cs
this.WhenActivated(disposable => {
    Connect.Execute().Subscribe();
    Disposable
        .Create(() => Cancel.Execute().Subscribe())
        .DisposeWith(disposable);
});

We aren't disposing of the subscriptions returned by Execute() because they'll get disposed anyway when the commands complete their execution. Hope this helps! ✨

Artyom
  • 446
  • 4
  • 7
  • Thank you very much, I like this solution, it's way more powerful and cleaner. But because I'm using Avalonia UI, I'll probably have to wait for https://github.com/AvaloniaUI/Avalonia/pull/4535 being released in the next preview. Which event can I use in case of Avalonia? Loaded/Unloaded doesn't seem to exist there? AttachedToVisualTree? – Marcus Wichelmann Sep 30 '20 at 12:45
  • `WhenActivated` uses `AttachedToVisualTree` under the hood, so in your case `this.Events()` isn't required I suppose. Also, `this.Events()` will become available if you install `Pharmacist` like this https://github.com/worldbeater/ReactiveMvvm/blob/main/src/ReactiveMvvm.Terminal/ReactiveMvvm.Terminal.csproj#L7 – Artyom Sep 30 '20 at 15:12
  • Also `Avalonia.ReactiveUI.Events` should be available in a Avalonia nightly build feed https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed – Artyom Sep 30 '20 at 15:15