0

I have a wpf application with a TextBox bound to ActualPageNumber property in the VM. I also have a DataGrid bound to an ObservableCollection which displays the given page. The data are stored in DB. When I change the ActualPageNumber, the setter accesses the db which can be slow. That is why I wanted an async setter, to keep the gui responsive.

I understand there is no async setter: https://blog.stephencleary.com/2013/01/async-oop-3-properties.html

I also found useful stuff like https://stackoverflow.com/a/9343733/5852947, https://stackoverflow.com/a/13735418/5852947, https://nmilcoff.com/2017/07/10/stop-toggling-isbusy-with-notifytask/

Still I struggle how to go on this case. AsyncEx library can be the solution, an example would be nice.

I just would like to notify the user that the page is actually loading. If I could call async from the setter I could do it, but then I still can not use await in the setter because it is not async.

Istvan Heckl
  • 864
  • 10
  • 22

2 Answers2

0

I also have a DataGrid bound to an ObservableCollection which displays the given page.

This is going to be the difficult part. DataGrid (and DataTable and friends) are designed with a synchronous API, and have never been updated to support asynchrony.

I'm not terribly familiar with DataGrid, but I'd say your options are:

  1. Replace the DataGrid with your own custom control - say, a ListView that displays custom controls. Then you can display a loading spinner since you control the custom control. There are some common patterns for this like NotifyTask.
  2. There might be a way to virtualize the data in the DataGrid in a way that it would asynchronously load. I'm not familiar enough with DataGrid to say whether this is actually possible, but it's worth looking into.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks @Stephen Clearly I think option 1 will be the answer. I will try it. I just wonder what is the rational that in WPF a bound property setter can not be async. In my program there are several property setter which should initiate DB operation (changing the page size, changing the filter). I would be nice if I could simply use async operation in these setters and I also could await them. – Istvan Heckl Oct 09 '20 at 14:08
  • @IstvanHeckl: Property setters return `void` according to the C# language specification. – Stephen Cleary Oct 12 '20 at 12:58
0

1) For the responsiveness of the DataGrid, this binding property might help: IsAsync=True

<DataGrid ItemsSource="{Binding MyCollection, IsAsync=True}"

also look into these DataGrid properties:

VirtualizingPanel.IsVirtualizing
VirtualizingPanel.VirtualizationMode (you'll probably need Recycling)
VirtualizingPanel.IsVirtualizingWhenGrouping
EnableRowVirtualization
EnableColumnVirtualization

But be careful, virtualization can play tricks on you. For example, I had a RowHeader (with the row number) and the values got scrambled when virtualization was on.

2) About the async setter for data binding: I was using a custom version of IAsyncCommand (see Stephen Cleary's example).

I used the command in 2 ways: a) binding to it from the view (avoiding the async setter altogether) or b) launching it from the setter (not nice).

Example: I created an UpdateCommand as an AsyncCommand and placed everything I needed done asynchronously (like getting the values from the DB). Everything in this command is wrapped within a display+hide of a "in progress"-like control - in my case, a transparent cover with a spinner + "please wait...", to prevent other user actions (the "screen" is visible while the task is performed). Stripped down sample:

    ....
    public MainWindowViewModel()
    {
       UpdateCommand = AsyncCommand.Create(Update); // our own custom implementation of AsyncCommand
    }
    ....

    public AsyncCommand UpdateCommand { get; }
    internal async Task Update(object arg)
    {
        await SafeWrapWithWaitingScreenAsync(async () =>
        {
            var value = (int)arg; // or the ActualPageNumber, if used from a1)

            var data = await GetDataFromDb(value).ConfigureAwait(false);

            ...// fill in MyCollection (which is the DataGrid's ItemsSource) using the data

            OnPropertyChanged(nameof(MyCollection));// if still needed
        }).ConfigureAwait(false);
    }

    ....
    public async Task SafeWrapWithWaitingScreenAsync(Func<Task> action)
    {
        DisplayWaitingScreen = true; //Visibility of the "Waiting screen" binds to this
        try
        {
            await action().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            HandleException(ex); // display/log ex
        }
        finally
        {
            DisplayWaitingScreen = false;
        }
    }

a) Binding to the command from the view and

a1) in the command's body use ActualPageNumber property instead of the arg value

or a2) passing a CommandParameter which binds to the same property as TextBox.Text does. Example (could be missing something, couse is not the real code):

<TextBox Text="{Binding ActualPageNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
         <KeyBinding Key="Return" Command="{Binding UpdateCommand}" CommandParameter="{Binding ActualPageNumber}" />
         <KeyBinding Key="Enter" Command="{Binding UpdateCommand}" CommandParameter="{Binding ActualPageNumber}" />
    </TextBox.InputBindings>
</TextBox>

b) Not sure this is right, but before seeing Stephen's approach with NotifyTaskCompletion<TResult> (which I will probably use in the future), for the setter, I launched the command something like:

private int actualPageNumber;
public int ActualPageNumber
    {
        get => actualPageNumber;
        set
        {
            actualPageNumber = value;
            OnPropertyChanged(); //the sync way

            UpdateCommand.Execute(value);
        }
    }