Search code examples
wpfuser-interfaceasynchronousdatagrid

Datagrid remains empty after asynchronous initialization in view model constructor


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.


Solution

  • 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.