Home > database >  Datagrid remains empty after asynchronous initialization in view model constructor
Datagrid remains empty after asynchronous initialization in view model constructor

Time:02-02

I have a WPF application with a view containing a data grid and a view model with an observable collection that is initialized by calling an asynchronous method in the constructor. But the data grid remains empty upon running the code.

The view model class looks like this.

internal class MainWindowViewModel : INotifyPropertyChanged
    {
        private readonly IBookingRecordService service;

        public event PropertyChangedEventHandler? PropertyChanged;
        private ObservableCollection<BookingRecord> bookingRecords = new();

        public ObservableCollection<BookingRecord> BookingRecords
        {
            get => bookingRecords;
            set
            {
                bookingRecords = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
            }
        }

        public MainWindowViewModel() 
        {
            service = new BookingRecordService();
            Task.Run(() => LoadBookingRecords());
        }

        private async Task LoadBookingRecords()
        {
            BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
        }
    }

I all LoadBookingRecords() in the constructor, so that the data starts loading on initialization of the view model already but I do it asynchronously, so it does not block the UI thread and makes the application unresponsive.

I have tried waiting for the completion of the task in the constructor via

Task.Run(() => LoadBookingRecords()).Wait();

to check that this has something to do with the asynchronous function call. And indeed, if I wait for the method to finish in the constructor, the data grid displays correctly. But I don't want to wait for the task to finish on the UI thread because it blocks the UI.

I have read that you must raise the PropertyChanged event on the UI thread to trigger a UI update and I suppose that is the problem here. I have also read that one can use

Application.Current.Dispatcher.BeginInvoke() 

to schedule a delegate to run on the UI thread as soon as possible, so I tried the following.

private async Task LoadBookingRecords()
{
    await Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
    {
        BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
    }));
}

But this leaves the DataGrid empty as well.

CodePudding user response:

"'asynchronous ... in constructor" is something you must avoid.

Async calls must be awaited, which can not be done in a constructor. Declare an awaitable Initialize method instead

public Task Initialize()
{
    return LoadBookingRecords();
}

and call it in an async Loaded event handler in your MainWindow:

private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    await viewModel.Initialize();
}

Alternatively, create a factory method like

public static async Task<MainWindowViewModel> Create()
{
    var viewModel = new MainWindowViewModel();
    await viewModel.LoadBookingRecords();
    return viewModel;
}

and call that in the Loaded handler:

private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    DataContext = await MainWindowViewModel.Create();
}

CodePudding user response:

Building on Clemens' answer, I tried something a little different in order to avoid touching the MainWindow code-behind.

I removed the call on LoadBookingRecords in the constructor and instead created a delegate command as a property that holds this method.

internal class MainWindowViewModel : INotifyPropertyChanged
{
    private readonly IBookingRecordService service;
    private ObservableCollection<BookingRecord> bookingRecords = new();

    public ICommand LoadBookingRecordsCommand { get; set; }

    public ObservableCollection<BookingRecord> BookingRecords
    {
        get => bookingRecords;
        set
        {
            bookingRecords = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
        }
    }

    public MainWindowViewModel() 
    {
        service = new BookingRecordService();
        LoadBookingRecordsCommand = new DelegateCommand(async _ => await LoadBookingRecords());
    }

    private async Task LoadBookingRecords()
    {
        BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
    }
}

I then added the NuGet package Microsoft.Xaml.Behaviors.Wpf to the project and added the following namespace to the MainWindow XAML.

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

Finally, I bound the delegate command to the MainWindow's Loaded event.

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding LoadBookingRecordsCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Now the data grid displays correctly after being loaded.

  • Related