Search code examples
c#wpfmvvm

Update UI while backgroundworker is working in WPF Project (MVVM)


In my WPF project, I need to transfer large SQL data to a datagrid. Naturally, it waits for a long time. I used the MVVM pattern.

To make UI more responsive, I use a backgroundworker. But the screen freezes again and the UI is updated when the transfer is completed. I want the UI to be updated while loading data. There are topics about this subject, I understood it is about using ProgressChanged event but I couldn't figure it out. So I prepared a simple project for asking.

This is my XAML markup:

<Window x:Class="BackRoundWorkerTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BackRoundWorkerTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:BackroundWorkerVM x:Key="vm"/>
    </Window.Resources>

    <Grid DataContext="{StaticResource vm}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <DataGrid ItemsSource="{Binding Persons, UpdateSourceTrigger=PropertyChanged}"
                  AutoGenerateColumns="True">
        </DataGrid>
      
        <Button Grid.Row="1"
                Command="{Binding DoWorkCommand}"/>
    </Grid>
</Window>

This is the view model:

public class BackgroundWorkerVM : INotifyPropertyChanged
{
     private BackgroundWorker worker;

     // it calls the PopulateList() method.
     public DoWorkCommand DoWorkCommand { get; set; } 

     public BackgroundWorkerVM()
     {
         // it calls the PopulateList() method.
         DoWorkCommand = new DoWorkCommand(this); 
         worker = new BackgroundWorker();
         worker.DoWork += Worker_DoWork;
         worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
         worker.WorkerReportsProgress = true;
     }

     private ObservableCollection<Person> persons = new ObservableCollection<Person>();

     public ObservableCollection<Person> Persons
     {
         get { return persons; }
         set
         {
             persons = value;
             OnPropertyChanged();
         }
     }

     public void PopulateList()
     {
         worker.RunWorkerAsync();
     }

     private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
     {
     }

     private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
     {
     }

     private void Worker_DoWork(object sender, DoWorkEventArgs e)
     {
         App.Current.Dispatcher.Invoke((Action)delegate 
         {
             for (int i = 0; i < 100; i++)
             {
                 Persons.Add(new Person { PersonName = "john", Surname = "Smith", ResultChanged = true });
                 Thread.Sleep(100);
                 Persons.Add(new Person { PersonName = "jack", Surname = "Smith", ResultChanged = false });
             }
         });
     }

     public event PropertyChangedEventHandler PropertyChanged;
     protected void OnPropertyChanged([CallerMemberName] string name = null)
     {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
     }
 }

This is the model class:

public class Person
{
    public string PersonName { get; set; }
    public string Surname { get; set; }
    public bool ResultChanged { get; set; }
}

Solution

  • You still do all the waiting on the main application thread App.Current.Dispatcher.Invoke(...). Therefore, while the method called in Invoke(...) is running, the main application thread cannot update the GUI.

    One of the possible options is to prepare the collection in the pool thread, and then replace the entire collection:

        public class BackgroundWorkerVM : INotifyPropertyChanged
        {
            public RelayCommand DoWorkCommand { get; }
    
            public BackgroundWorkerVM()
            {
                DoWorkCommand = new(PopulateList);
            }
    
            private ObservableCollection<Person> _persons = new ObservableCollection<Person>();
    
            public ObservableCollection<Person> Persons
            {
                get { return _persons; }
                set
                {
                    _persons = value;
                    OnPropertyChanged();
                }
            }
    
            public async void PopulateList()
            {
                var list = await Task.Run(Worker_DoWork);
    
                Persons = list;
            }
    
    
            private ObservableCollection<Person> Worker_DoWork()
            {
                ObservableCollection<Person> list = new ObservableCollection<Person>();
    
                for (int i = 0; i < 100; i++)
                {
                    list.Add(new Person { PersonName = "john", Surname = "Smith", ResultChanged = true });
                    Thread.Sleep(100);
                    list.Add(new Person { PersonName = "jack", Surname = "Smith", ResultChanged = false });
                }
    
                return list;
            }
    
            public event PropertyChangedEventHandler? PropertyChanged;
            protected void OnPropertyChanged([CallerMemberName] string? name = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
            }
        }
    

    Another option is to update the collection element by element (works in C#8+). In this case, a mutable property for the collection is not needed:

        public class BackgroundWorkerVM : INotifyPropertyChanged
        {
            public RelayCommand DoWorkCommand { get; }
    
            public BackgroundWorkerVM()
            {
                DoWorkCommand = new(PopulateList);
            }
    
            public ObservableCollection<Person> Persons { get; } = new();
    
            public async void PopulateList()
            {
                Persons.Clear();
                await foreach (Person person in Worker_DoWork())
                {
                    Persons.Add(person);
                }
            }
    
    
            private async IAsyncEnumerable<Person> Worker_DoWork()
            {
    
                for (int i = 0; i < 100; i++)
                {
                    yield return new Person { PersonName = "john", Surname = "Smith", ResultChanged = true };
                    await Task.Delay(100);
                    yield return new Person { PersonName = "jack", Surname = "Smith", ResultChanged = false };
                    await Task.Delay(100);
                }
    
            }
    
            public event PropertyChangedEventHandler? PropertyChanged;
            protected void OnPropertyChanged([CallerMemberName] string? name = null)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
            }
        }