Setting TaskCompletionSource result in RelayCommand not triggering awaited code

110 Views Asked by At

Edit2: I tracked down the problem to an unrelated issue (the view and the logic were looking at different ViewModel instances. oops.)

I have the following code in a WinUI 3 application, using CommunityToolkit.MVVM, trying to use TaskCompletionSource with RelayCommand to control application flow around user input. It's working in one spot, and not in another. I cannot for the life of me tell what I'm doing differently in the second that would cause it to fail like this.

Edit: Following up on Andrew's answer, I've discovered it works fine if I call the Next method from code, rather than from the UI (using the generated NextCommand.) I still don't know why it works for the first one and not the second. I've tried using both ConfigureAwait(false) and TaskCreationOptions.RunContinuationsAsynchronously, to no avail.

//App.xaml.cs
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    m_window = new MainWindow();
    m_window.Activate();

    _ = RunGame();
}

private async Task RunGame()
{
    // ... create start view ...

    var settings = await startView.ViewModel.WaitForStartAsync(); //This works

    // ... create first game step ...
        
    await currentStep.WaitForNextAsync(); //This doesn't

    Exit(); //never called
}

//StartViewModel.cs
public partial class StartViewModel : ObservableObject
{
    //settings logic

    private readonly TaskCompletionSource<GameSettings> m_taskCompletionSource = new();

    [RelayCommand]
    private void Start() => m_taskCompletionSource.TrySetResult(new(Difficulty));

    public Task<GameSettings> WaitForStartAsync() => m_taskCompletionSource.Task;
}

//GameStepViewModel.cs
public abstract partial class GameStepViewModel : ObservableObject
{
    [ObservableProperty, NotifyCanExecuteChangedFor("NextCommand")]
    private bool m_nextEnabled = true;

    private readonly TaskCompletionSource _tcs = new();

    [RelayCommand(CanExecute = "NextEnabled")]
    private void Next() => _tcs.TrySetResult();

    public Task WaitForNextAsync() => _tcs.Task;
}

edit: I know from debugging that the Next() method is being called successfully and the TaskCompletionSource.Task is transitioning to RanToCompletion.

1

There are 1 best solutions below

2
Andrew KeepCoding On

This is the code that worked (Exit() called after Next() is called) for me.

App.xaml.cs

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    _window = new MainWindow();
    _window.Activate();

    _ = RunGame();
}

private async Task RunGame()
{
    var startView = new StartView();
    _window.Content = startView;
    var settings = await startView.ViewModel.WaitForStartAsync();
    var firstGameStep = new FirstGameStep();
    firstGameStep.CallNextLater(TimeSpan.FromSeconds(5));
    await firstGameStep.WaitForNextAsync();
    Exit();
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;

namespace TaskCompletionSourceExample;

public enum Difficulty
{
    Easy,
    Medium,
    Hard
}

public class GameSettings
{
    public GameSettings(Difficulty difficulty)
    {
    }
}

public partial class StartViewModel : ObservableObject
{
    private readonly TaskCompletionSource<GameSettings> m_taskCompletionSource = new();

    private Difficulty Difficulty { get; set; }

    public Task<GameSettings> WaitForStartAsync() => m_taskCompletionSource.Task;

    [RelayCommand]
    private void Start() => m_taskCompletionSource.TrySetResult(new(Difficulty));
}

public sealed partial class StartView : Page
{
    public StartView()
    {
        this.InitializeComponent();
    }

    public StartViewModel ViewModel { get; } = new();
}

public class FirstGameStep : GameStepViewModel
{
    public FirstGameStep()
    {
        NextEnabled = false;
    }
}

public abstract partial class GameStepViewModel : ObservableObject
{
    public void CallNextLater(TimeSpan delay)
    {
        Task.Run(() =>
        {
            Task.Delay(delay);
            NextEnabled = true;
            Next();
        });
    }

    [ObservableProperty, NotifyCanExecuteChangedFor("NextCommand")]
    private bool m_nextEnabled = true;

    private readonly TaskCompletionSource _tcs = new();

    [RelayCommand(CanExecute = "NextEnabled")]
    private void Next() => _tcs.TrySetResult();

    public Task WaitForNextAsync() => _tcs.Task;
}