StepUp StepUp - 2 months ago 11
C# Question

Task.Factory.StartNew with TaskScheduler.Default freezes UI. Net 4.0

I've read a Stephen Cleary's article that it is necessary to explicitly set from

TaskScheduler.Current
to
TaskScheduler.Default
.

When my program is started, then loading of data begins. It takes approximately 5 seconds. If I click at the button Add while loading data, then new window will not be opened till
Task
operation of method
ReadData()
is not completed.

ViewModel:

public class MainWindowVM:ViewModelBase
{
public MainWindowVM()
{
AddPersonCommand = new RelayCommand<object>(AddPerson);
ReadData();
}

private void ReadData()
{
PersonData = new ObservableCollection<PersonDepBureau>();
IList<PersonDepBureau> persons;
Task.Factory.StartNew(() =>
{
using (PersonDBEntities db = new PersonDBEntities())
{
persons = (from pers in db.Person
join bu in db.Bureau on pers.Fk_IdBureau equals bu.IdBureau
join dep in db.Departament on pers.Fk_IdDep equals dep.IdDep
select new PersonDepBureau
{
IdPerson = pers.IdPerson,
PersonName = pers.Name,
Surname = pers.Surname,
).ToList();
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
foreach (var person in persons)
{
PersonData.Add(person);
}
}));
}
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
}

//Opening new Window AddPersonWindow.xaml
private void AddPerson(object obj)
{
AddPersonWindow addPersonWindow = new AddPersonWindow();
addPersonWindow.DataContext new AddPersonVM();
addPersonWindow.ShowDialog();
}
}


View:

<extToolkit:BusyIndicator IsBusy="{Binding IsBusy}" Foreground="Black">
<DataGrid ItemsSource="{Binding PersonData}" SelectedItem="{Binding SelectedPerson}"
IsSynchronizedWithCurrentItem="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding PersonName}"/>
<DataGridTextColumn Header="Surname" Binding="{Binding Surname}" />
</DataGrid.Columns>
</DataGrid>
</extToolkit:BusyIndicator>
<Grid Grid.Row="1">
<Button Command="{Binding AddPersonCommand}" Margin="5" Grid.Column="1">Add</Button>
</Grid>


Cannot use
async/await
syntactic sugars as my application can be used in Windows XP(I cannot ask users to install
.NET 4.5
). Thank in advance.

It is really weird behaviour. All info that I've read about is using
Task
like I did. But my example is not working properly(new window is not opened while loading data), so to show an error behavior I've made a test application and it can be downloaded here.. Cause I've heard a lot of comments to use
TaslScheduler.Default
.

Please, do not close my thread as it is really important to me to understand the reason why my UI is unresponsive.

Stephen Cleary's article is perfect. Thanks for patience to Peter Bons.
The construction
Task.Factory.StartNew(() =>}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
is perfectly works, the reason of freezing UI is another operation executed on UI thread.

Answer

One of your problem lies in this code:

   Task.Factory.StartNew(() =>
        {
            using (PersonDBEntities db = new PersonDBEntities())
            {
                try
                {
                    persons = (from pers in db.Person
                               join bu in db.Bureau on pers.Fk_IdBureau equals bu.IdBureau
                               join dep in db.Departament on pers.Fk_IdDep equals dep.IdDep
                               select new PersonDepBureau
                               {
                                   IdPerson = pers.IdPerson,
                                   PersonName = pers.Name,
                                   Surname = pers.Surname,
                                   CurrentBureau = bu,
                                   CurrentDepartament = dep
                               }).ToList();
                    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
                    {
                        foreach (var person in persons)
                        {
                            PersonData.Add(person);
                        }
                    }));  

                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }     
            }
            IsBusy = false;
        }, CancellationToken.None, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);

You are trying to update the UI from another thread. That's why you are using the dispatcher. You should split the data loading from the actually (UI) processing logic. Since you cannot use async/await you need to use ContinueWith

something like (pseudo code):

var loadDataTask = Task.Factory.StartNew(() =>
        {
            var persons = new List<PersonDepBureau>();
            using (PersonDBEntities db = new PersonDBEntities())
            {
                    persons = (from pers in db.Person
                               join bu in db.Bureau on pers.Fk_IdBureau equals bu.IdBureau
                               join dep in db.Departament on pers.Fk_IdDep equals dep.IdDep
                               select new PersonDepBureau
                               {
                                   IdPerson = pers.IdPerson,
                                   PersonName = pers.Name,
                                   Surname = pers.Surname,
                                   CurrentBureau = bu,
                                   CurrentDepartament = dep
                               }).ToList();
            }

            return persons;
        }, CancellationToken.None, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
loadDataTask.ContinueWith((t) =>
{
    // you can check t.Exception for errors

    // Do UI logic here. (On UI thread)  
    foreach (var person in t.Result)
    {
          PersonData.Add(person);
    }
}, TaskScheduler.FromCurrentSynchronizationContext());

See also Task continuation on UI thread

Then another problem is the fact that when you press the Add button an instance of the AddPersonVM is created. With this code in the constructor:

    public AddPersonVM()
    {
        LoadData();

        AddPersonCommand = new RelayCommand<object>(AddPerson);
        CancelPersonCommand = new RelayCommand<object>(CancelPerson);
    }

When I time the duration of that LoadData() call it takes anywhere between 1.5 and 4s complete. That code has to complete before ShowDialog() is called.

You should not load data in constructors. And in your case, not before the dialog is displayed. You could either load that data async as well or you should open the dialog and use the Window.Loaded event to start getting the data (sync or async). Because at that point the UI is already displayed so you could then use a busy indicator while fetching the data from the database.

In the MainWindow you have this code:

private void AddPerson(object obj)
    {
        AddPersonWindow addPersonWindow = new AddPersonWindow();
        AddPersonVM addPersonVM = new AddPersonVM(); // This synchronous code takes about 2 to 5 seconds to complete
        addPersonWindow.DataContext = addPersonVM;            
        addPersonVM.OnRequestClose += (sender, args) => { addPersonWindow.Close(); };
        addPersonVM.OnPersonSave += addPersonVM_OnPersonSave;
        addPersonWindow.ShowDialog();
    }

Since the constructor of AddPersonVM loads data from the database taking around 2 to 5 seconds to complete the addPersonWindow is not shown directly.