Search code examples
c#wpfmvvmcommunity-toolkit-mvvm

How to bind to Task<T> in ObservableObject from CommunityToolkit.Mvvm?


ObservableObject from CommunityToolkit.Mvvm has API which allows to bind asynchronously to Task<T> (https://github.com/MicrosoftDocs/CommunityToolkit/blob/main/docs/mvvm/ObservableObject.md#handling-taskt-properties) Problem is the sample do not include xaml part and I don't know how the binding should looks like. Can anybody show me on the example bellow:

public partial class MainWindowViewModel : ObservableObject
    {
        [RelayCommand]
        private void RequestValue()
        {
            RequestTask = LoadAsync();
        }

        private TaskNotifier<int>? requestTask;

        public Task<int>? RequestTask
        {
            get => requestTask;
            private set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
        }

        private static async Task<int> LoadAsync()
        {
            await Task.Delay(3000);
            return 5;
        }
<Window>
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <StackPanel>
        <Button Command="{Binding RequestValueCommand}" Content="Get my value"/>
        <StackPanel Orientation="Horizontal" >
            <TextBlock Text="My value is:"/>
            <TextBlock Text="{Binding ?????????}"/>
        </StackPanel>
    </StackPanel>
</Window>

I expect after button is clicked it waits 3 seconds and then my value is changed to 5.

I've already checked their sample app, but there is binding to Task only, not to Task<T> (https://github.com/CommunityToolkit/MVVM-Samples/blob/master/samples/MvvmSampleUwp/Views/ObservableObjectPage.xaml)


Solution

  • You must always await a Task object. In your case the TaskNotifier<T> is awaiting the Task for you. It will raise the INotifyPropertyChanged.PropertyChanged event as soon as the Task has run to completion. You can then retrieve the value from the Task.Result property. This means you must always bind to the Task.Result property.

    Because asynchronous code implies to be potentially long-running, you should also set Binding.IsAsync to true on the particular Binding to prevent the UI from freezing:

    <Window>
        <Window.DataContext>
            <local:MainWindowViewModel/>
        </Window.DataContext>
        <StackPanel>
            <Button Command="{Binding RequestValueCommand}" 
                    Content="Get my value"/>
            <StackPanel Orientation="Horizontal" >
                <TextBlock Text="My value is:"/>
                <TextBlock Text="{Binding RequestTask.Result, IsAsync=True}"/>
            </StackPanel>
        </StackPanel>
    </Window>
    

    However, an asynchronous property (long-running property) is an oxymoron. Properties and fields (or variables in general) don't "run". Methods do.

    A property is expected to store a value. Referencing a value from a property is not synonymous to executing a method.
    Never would anybody expect that getting the value from a property takes significant time.
    We can consider a long-running property a code smell.

    On the other hand, we naturally expect a method to do something and then once completed return a value or change an object's state. We expect a method to be potentially long-running.

    You should always avoid asynchronous properties and only use them when you really ran out of options.

    You usually avoid this situation by refactoring the application flow properly. Usually a long-running operation is explicitly triggered. And as the word "operation" suggests we use methods for this.

    In your scenario you can perfectly use the ICommand to trigger the long-running operation. Because a long-running operation usually affects the UI, you should allow the user to explicitly start this operation. For example, you can always provide the user a "Download" button. He can select am item from a drop down list and click the button to start the download. This feels natural as the user expects that the time consuming download start when he clicks the button.

    In contrast, your implemented pattern allows the user to select an item from the drop down list. The moment he selects the item the download (the long-running operation) immediately starts (because the SelectedItem was set to the async property behind the scene).

    Allowing the user to explicitly start the long-running operation has several advantages in terms of usability and user experience.In this example the user can revert his decision after selecting an item and pick another one. Because the download has not yet started, everything is smooth. When the user is ready, he explicitly starts the download via the button (a command handler that triggers the long-running operation).

    Most of the time, an asynchronous property should be replaced with a ICommand that is triggered by the user and executes the long-running operation.

    The MVVM Toolkit supports asynchronous commands. Simply define the execute handler of type Task (note, the framework itself does not support async commands i.e. there is no awaitable ICommand.Execute member. This means, a normal synchronous ICommand with an execute handler async void is fine).

    A more graceful solution (in contrast to async properties) could look as follows:

    // Define the async command
    [RelayCommand]
    private async Task RequestValueAsync()
    {
      // Explicitly execute the long-running operation.
      RequestTask = await LoadAsync();
    }
    
    private int requestTask;
    public int RequestTask
    {
      get => requestTask;
      private set => SetProperty(ref requestTask, value);
    }
    
    private async Task<int> LoadAsync()
    {
      await Task.Delay(3000);
      return 5;
    }
    
    <Window>
        <Window.DataContext>
            <local:MainWindowViewModel/>
        </Window.DataContext>
        <StackPanel>
            <Button Command="{Binding RequestValueCommand}" 
                    Content="Get my value"/>
            <StackPanel Orientation="Horizontal" >
                <TextBlock Text="My value is:"/>
                <TextBlock Text="{Binding RequestTask}"/>
            </StackPanel>
        </StackPanel>
    </Window>