-3

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.

StepUp
  • 36,391
  • 15
  • 88
  • 148
  • If you call `Dispatcher.(Begin)Invoke()` you are executing the lambda expression on the UI thread. Remove the call and invoke only when you need to update UI controls/elements. – Visual Vincent Sep 21 '16 at 18:03
  • @VisualVincent If I delete Dispatcher, it still freezes UI. – StepUp Sep 21 '16 at 18:07
  • And if you change `TaskCreationOptions.AttachedToParent` to `TaskCreationOptions.None`? – Visual Vincent Sep 21 '16 at 18:10
  • @VisualVincent yeah, it still freezes UI. – StepUp Sep 21 '16 at 18:19
  • @downvoter what's a reason to downvote? – StepUp Sep 21 '16 at 18:19
  • `job of new button is not performed(new window is not opened)` - What window? Perhaps you should rather use `Task.Delay()` than `Thread.Sleep()`? – Visual Vincent Sep 21 '16 at 18:22
  • 1
    @VisualVincent I would like to use Thread.Sleep(5000); cause it freezes Task thread. And it is imitating behavior of my program. – StepUp Sep 21 '16 at 18:30
  • So use `await Task.Delay(5000)`, it's what you're supposed to use in tasks. It will halt the code execution but not block the task thread. – Visual Vincent Sep 21 '16 at 19:08
  • @VisualVincent If I delete `Thread.Sleep(5000)` and set `Task.Delay(5000)`, then my UI still freezes. Could you see my test project? I know that it is not good solution to provide a link to error, but all rules of multithreading are applied, but UI is frozen. Thank in advance. – StepUp Sep 21 '16 at 19:14
  • 1
    I am currently on my phone, but I can test it in about 40-60 minutes. – Visual Vincent Sep 21 '16 at 19:16
  • I took a look at your code. You are not awaiting your Task, but that might be a choice. But you should use the native async methods of the entity framework: https://msdn.microsoft.com/en-us/data/jj819165.aspx. Using Task.Factory.StartNew to wrap a sync call is an anti pattern: http://www.ben-morris.com/why-you-shouldnt-create-asynchronous-wrappers-with-task-run/ – Peter Bons Sep 21 '16 at 19:34
  • @PeterBons no, I do not use `wait()`. I've put my edited code to my question. Please, see. – StepUp Sep 21 '16 at 19:40
  • @PeterBons I am using `.Net 4.0`. – StepUp Sep 21 '16 at 19:43
  • But you are loading data in the constructor. That's a bad idea. UI is not responsive there. – Peter Bons Sep 21 '16 at 19:43
  • @PeterBons so what should I do to improve my program and responsiveness of UI? And why UI is frozen as I am loading data in non UI-thread(I am using `Task`). – StepUp Sep 21 '16 at 19:45
  • Depending on the version of Visual Studio you can use async/await https://blogs.msdn.microsoft.com/bclteam/2012/10/22/using-asyncawait-without-net-framework-4-5/. Combine that with the async/await support of EF. I cannot run the code since I do not have the needed db. – Peter Bons Sep 21 '16 at 19:49
  • @PeterBons `async await` is not available in `.Net 4.0` – StepUp Sep 21 '16 at 19:52
  • 1
    Yes it is, by using a NuGet package. https://blogs.msdn.microsoft.com/bclteam/2012/10/22/using-asyncawait-without-net-framework-4-5/ But only in VS 2012 and higher – Peter Bons Sep 21 '16 at 19:54
  • @PeterBons I cannot use async/await cause some other computers does not have 4.5 Framework, and some PCs have .NET 3.5 – StepUp Sep 21 '16 at 20:00
  • @StepUp you don't need framework 4.5 installed to use what Peter linked to, also you you said this was targeting 4.0 in your tags, if you are actually targeting 3.5 you need to update your question tags. – Scott Chamberlain Sep 21 '16 at 21:12
  • I have tested your code and I've experienced the same problem. Unfortunately I don't think I can be much of help... I'm not very familiar with WPF and not at all with databases. – Visual Vincent Sep 21 '16 at 21:24
  • @StepUp: Try to reduce the problem down to the minimum amount of code that still duplicates the issue. Most people find the error themselves while going through this process. – Stephen Cleary Sep 22 '16 at 13:03
  • .NET 4.0 is no longer supported. The earliest supported version is 4.5.2, a binary replacement for all earlier versions. Chances are that even though you think you target 4.0, the customer's computer already has the 4.5.2 runtime installed. – Panagiotis Kanavos Sep 22 '16 at 13:21
  • @StephenCleary please be very kind to see my updated question. Feel free to say to me to add new info. Thank in advance. – StepUp Sep 22 '16 at 16:14

2 Answers2

3

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.

Community
  • 1
  • 1
Peter Bons
  • 26,826
  • 4
  • 50
  • 74
  • Thanks for a try, but it still freezes UI. It is really interesting what the reason is. I've done what you've suggested. Maybe you have other suggestions? I am opened to any suggestion:). – StepUp Sep 21 '16 at 20:27
  • @StepUp it's a lot of work to guide you in helping to create a responsive application. I will try to give you some advice, see updated answer. You have to think about your synchronous methods and the best place to call them. – Peter Bons Sep 22 '16 at 06:55
  • thanks for a help, but why `That code has to complete before ShowDialog() is called.` as we use multithreading. Why thread of `LoadData()` should wait for UI thread? – StepUp Sep 22 '16 at 10:48
  • The LoadData method in AddPersonVM is not async. Only the ReadDataAsunchronous() method in MainWindowVM is. When I add a Thread.Sleep(60*5*1000); in ReadDataAsunchronous() and I click the 'Add' button it still takes around 5 seconds before it shows up due to the LoadData method in AddPersonVM . So we can conclude that ReadDataAsunchronous() is not the only problem (because I do not have to wait 5 minutes before the Add Person dialog comes up. – Peter Bons Sep 22 '16 at 12:12
  • I've tried to load data at `Loaded` event of MainWindow. And there is no improvements. Data is loading and I cannot see new opened window. – StepUp Sep 22 '16 at 12:15
  • because you need to load the data in the Loaded event of the AddPersonWindow. See updated answer – Peter Bons Sep 22 '16 at 12:17
  • I do not load data in `AddPersonWindow`, but I am loading data in constructor of `MainWindowVM` - viewModell of `MainWindow`. While data is loading in `MainWindowVM`, `AddPersonWindow` could not be opened. – StepUp Sep 22 '16 at 16:21
  • Ok, I am going to try one more time. Try adding `Thread.Sleep(60*5*1000);` (That is 5 minutes!!) just before the line `using (PersonDBEntities db = new PersonDBEntities())` (around line 40 in `MainWindowVM.cs` Now start the program and press 'Add' button. How long does that take. When I run your code it does NOT take 5 minutes. – Peter Bons Sep 22 '16 at 16:55
  • yes, loading of data takes 5 minutes, but opening new window takes 5 seconds. So it means all is okay? If I change calling loading data to `Window.Loaded` event and call method in MainWindowVM, then nothing changes - loading of data takes 5 minutes, but opening new window takes 5 seconds. – StepUp Sep 22 '16 at 17:16
  • Well. We now know that it is not the loading of the main form that is the problem but the loading of the data in the new window. So you need to do the same trick that you did in the main window in the new window. So for example The `LoadData();` method In the constructor of `AddPersonVM`. should be modified using `Task.Factory.StartNew` – Peter Bons Sep 22 '16 at 17:38
  • What an awful mistake. Thanks a lot for your patience. You are superprogrammer! – StepUp Sep 22 '16 at 17:46
0

You are doing your work on the Dispatcher.....

Might not be your issue as I just saw your thread.sleep outside that invoke

Banners
  • 247
  • 1
  • 7