2

I have this ReactiveCommand;

LoadFileCommand = ReactiveCommand.CreateAsyncTask((_, cancellationToken) => LoadFile(cancellationToken));

I also subscribe to the command

subscription = LoadFileCommand.Subscribe(file => OnFileLoaded(file);

Now I want to make another command that is used from the UI (in a button) to cancel the Task.

But HOW?

I have no way to "inject" my cancellationToken to the LoadFileCommand. I'm really lost!

EDIT:

Currently, under my MainViewModel.cs (in the constructor) I have this:

OpenFileCommand = ReactiveCommand.CreateAsyncTask(async (o, ct) => await LoadFile(ct));

var whenButtonClick =
    Observable
        .Timer(TimeSpan.FromSeconds(10));
whenButtonClick.Subscribe(_ => Console.WriteLine());

OpenFileCommand
    .ExecuteAsync()
    .TakeUntil(whenButtonClick)
    .Subscribe(OnDocumentLoaded);

I have a "Load" Button in my View that is bound to LoadFileCommand, but with the code executes the task as soon as the viewmodel is created, not when the user clicks the button.

By the way, I want to have another "Cancel" button, that allows the user to cancel the loading.

SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • Can you show me where you are binding your button to `OpenFileCommand`? – Jason Boyd Feb 25 '16 at 22:32
  • I'm binding to the command using a Binding in XAML like – SuperJMN Feb 26 '16 at 07:06
  • I don't think you are going to get anywhere with the XAML binding. That is going to rely on the `ICommand` interface to invoke `Execute` which returns void. You need to invoke `ExecuteAsync` and hang onto the observable it returns so that you can cancel it. I will try to throw something together that might work for you situation. – Jason Boyd Feb 27 '16 at 04:12
  • Bummer! I hope you can provide me with some workaround to achieve what I need. There is almost no online documentation about cancellation scenarios. Thanks for your efforts! – SuperJMN Feb 27 '16 at 12:33
  • I think I have a possible solution for you. Take a look at my updated answer. – Jason Boyd Feb 28 '16 at 01:48

1 Answers1

3

Subscribing to the LoadFileCommand does not invoke the command. The command is not invoked until you call one of the execute methods on the command. In your case you want to call LoadFileCommand.ExecuteAsync. This will return an IObservable<File> in your case, I believe. Disposing of the subscription to that observable or otherwise terminating the observable will cause the observable to request that the cancellation token that was passed to LoadFile in your delegate be cancelled.

I tried to create a .NET Fiddle here to demonstrate but it keeps saying an assembly is not referenced even though it clearly is. Anyway, here is the same code you can copy into LinqPad or a console application if you want to play around with it:

var testCommand = ReactiveCommand.CreateAsyncTask(async (name, ct) =>
{
    // Do some long running work and periodically check if the
    // token has been cancelled.
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine(
            "{0} cancellation requested: {1}", 
            name, 
            ct.IsCancellationRequested);

        if (ct.IsCancellationRequested)
        {
            ct.ThrowIfCancellationRequested();
        }
        await Task.Delay(1000);
    }
});

var whenButtonClick =
    Observable
    .Timer(TimeSpan.FromSeconds(2));

// Execute a command that is cancelled when a button click happens.
// Note the TakeUntil(whenButtonClick)
testCommand
.ExecuteAsync("first")
.TakeUntil(whenButtonClick)
.Subscribe(
    onNext: _ => Console.WriteLine("first next"),
    onCompleted: () => Console.WriteLine("first completed"));

// Execute a command that runs to completion.
testCommand
.ExecuteAsync("second")
.Subscribe(
    onNext: _ => Console.WriteLine("second next"),
    onCompleted: () => Console.WriteLine("second completed"));

This is the output from the above code. You can see that the cancellation token does indeed request cancellation:

first cancellation requested: False
second cancellation requested: False
second cancellation requested: False
first cancellation requested: False
first completed
first cancellation requested: True
second cancellation requested: False
second cancellation requested: False
second cancellation requested: False
second next
second completed

Edit - Possible Solution

So I think I have something that will work in your scenario while still allowing you to use the Xaml binding. I am pushing the cancellation logic into the command factory method rather trying to grab individual invocations and cancel those.

CancelOpenFileCommand = ReactiveCommand.Create();

LoadFileCommand = 
    ReactiveCommand
    .CreateAsyncObservable(_ =>
        Observable
        .FromAsync(cancellationToken => LoadFile(cancellationToken))
        .TakeUntil(CancelOpenFileCommand));

Now if you bind the button you want to use to open the file to the LoadFileCommand and the button you want to use to cancel the command to the CancelOpenFileCommand everything should just work.

Here is an example using the same pattern I describe above. I replaced LoadFile with a dummy task that just contains a loop that loops five times, inside the loop I am writing the state of the cancellation token to the console and then delaying for one second. So the task should take five seconds to complete. But instead of allowing it to complete I am invoking CancelOpenFileCommand after one second. This demonstrates that the cancellation token is being cancelled when the CancelOpenFileCommand is invoked and that the command is terminating early.

var CancelOpenFileCommand = ReactiveCommand.Create();

CancelOpenFileCommand
.Subscribe(x => 
    Console
    .WriteLine(
        "{0} CancelOpenFileCommand Invoked", 
        DateTime.Now.TimeOfDay));

var LoadFile = new Func<CancellationToken, Task>(async cancellationToken =>
    {
        for (int i = 0; i < 5; i++)
        {
            Console
            .WriteLine(
                "{0} Cancellation requested: {1}", 
                DateTime.Now.TimeOfDay, 
                cancellationToken.IsCancellationRequested);             

            if (cancellationToken.IsCancellationRequested)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }
            await Task.Delay(1000);
        }
    });

var LoadFileCommand = 
    ReactiveCommand
    .CreateAsyncObservable(
        name =>
            Observable
            .FromAsync(ct => LoadFile(ct))
            .TakeUntil(CancelOpenFileCommand));

LoadFileCommand.Execute(null);

Observable
.Timer(TimeSpan.FromSeconds(1))
.Subscribe(_ => CancelOpenFileCommand.Execute(null));

And here is the console output:

19:04:57.6087252 Cancellation requested: False
19:04:58.6157828 Cancellation requested: False
19:04:58.6197830 CancelOpenFileCommand Invoked
19:04:59.6268406 Cancellation requested: True

Jason Boyd
  • 6,839
  • 4
  • 29
  • 47
  • So doing subscription = LoadFileCommand.Subscribe(file => OnFileLoaded(file) is wrong? I thought that subscribing to the command was the way of receiving the results of the Task after it completes. Am I missing something? – SuperJMN Feb 25 '16 at 21:34
  • 1
    @Super No, that is not wrong. You get the results of the command invocation on the `LoadFileCommand` observable. If you want to cancel a command the only way I am aware of is to capture the observable returned when you invoke the command and terminate that observable. I am not aware of any way to do it with the command observable. Which kind of makes sense, if it is a long running task the command could have been invoked any number of additional times, the only way you could cancel an individual command invocation would be to have some object that represents that command invocation. – Jason Boyd Feb 25 '16 at 22:01
  • I'm a bit confused :( What I'm doing is to bind a Button to the LoadCommand. In MainViewModel (in the ctor). If I put the line `testCommand .ExecuteAsync("first")` it raises the Task even when the button isn't clicked. I would like the user to raise it by clicking a button and to allow to cancel using another button. Please, take a look at the edit I have posted in the original message. – SuperJMN Feb 25 '16 at 22:19
  • @ SuperJMN Sorry, what I was trying to show with my code was an example demonstrating how one would go about cancelling a command once it had been invoked. I was not suggesting that the code could be incorporated into your project as is to solve your problem. – Jason Boyd Feb 25 '16 at 22:32