update UI in Task using TaskScheduler.FromCurrentSynchronizationContext

12,560

Solution 1

where is problem?

Well you're explicitly saying that you want to execute the task in the UI thread... and then you're sleeping within the task, so it's blocking the UI thread. How did you expect to be in the UI thread, but for Thread.Sleep not to cause a problem?

If you can use C# 5 and async/await, that would make things much easier:

private static async Task ShowCitiesAsync()
{
    for (int i = 0; i < 10; i++)
    {
        listBox1.Items.Add("Number cities in problem = " + i);
        await Task.Delay(1000);
    }
}

If you can't use C# 5 (as suggested by your tags), it's significantly trickier. You might be best off using a Timer:

// Note: you probably want System.Windows.Forms.Timer, so that it
// will automatically fire on the UI thread.
Timer timer = new Timer { Interval = 1000; }
int i = 0;
timer.Tick += delegate
{
    listBox1.Items.Add("Number cities in problem = " + i);
    i++;
    if (i == 10)
    {
        timer.Stop();
        timer.Dispose();
    }
};
timer.Start();

As you can see, it's pretty ugly... and it assumes you don't want to actually do any work between UI updates.

Another alternative would be to simulate your long-running task (sleeping at the moment) on a different thread using BackgroundWorker, and use ReportProgress to come back to the UI thread to add the list item.

Solution 2

You could do all work in the thread, but then when you need to update the UI use the dispatcher:

Task.Factory.StartNew(() =>
{
  for (int i = 0; i < 10; i++)
  {
     Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, 
        new Action(() => {listBox1.Items.Add("Number cities in problem = " + i.ToString()); }));
     System.Threading.Thread.Sleep(1000);
  }
});

Solution 3

Just giving another flavour of the solution for c# 4.0. This is similar to @Debora and @Jay (well, if you forget about the while(true)... just talking about the BeginInvoke) solutions, but fully based on TPL and the one that gets closer to the code generated with async/await:

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
    for (int i = 0; i < 10; i++)
    {
        Task.Factory.StartNew(() =>
        {
             listBox1.Items.Add("Number cities in problem = " + i.ToString());
        }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

        System.Threading.Thread.Sleep(1000);
    }
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);

Your work task should be scheduled using the default TaskScheduler (that will use the ThreadPool) and use the uiScheduler to callback to the UI thread when required to update the UI. Keep in mind that this is a sample implementation and there could be some problems with this sample, for instance, the inner task is scheduled to execute on the UI thread, but it is not waited by the calling task, so the sleep will actually run while the inner task is running. It is also very important that you do not wait for the tasks, or you could have a deadlock (inner task is trying to run on the UI thread that is blocked waiting for the outer task).

I usually use the uiScheduler on continuation task to provide the data to the UI. In your case it could be something like this:

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() =>
{
    //load data or perform work on background thread
    var itemList = new List<int>();
    for (int i = 0; i < 10; i++)
    {
        itemList.Add(i);
        System.Threading.Thread.Sleep(1000);
    }
    return itemList;
}).ContinueWith((itemList) => 
{
   //get the data from the previous task a continue the execution on the UI thread
   foreach(var item in itemList)
   {
      listBox1.Items.Add("Number cities in problem = " + item.ToString());
   }
}, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

The resulting compiled code will be very similar (if not equal) to the code generated with async/await

Share:
12,560
Arian
Author by

Arian

Please vote-up this thread: RDLC Report Viewer for Visual Studio 2022

Updated on June 04, 2022

Comments

  • Arian
    Arian almost 2 years

    I want to add some text to list box using Task and I simply use a button and place in click event this code:

    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            listBox1.Items.Add("Number cities in problem = " + i.ToString());
            System.Threading.Thread.Sleep(1000);
        }
    }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    

    but it does not work and UI locked until the end of the for loop.

    Where is the problem ?

    thanks :)

  • Arian
    Arian almost 11 years
    thanks but I'm currently using C#-4 what can I do?In my main program I have not use Sleep I have some methods that are long running and I add Item after execute of them in list box such as Step 1 Completed... but this message does not show until running
  • Jon Skeet
    Jon Skeet almost 11 years
    @Kerezo: See my edits for two alternatives. But we don't really know what the bigger picture is - presumably you'll want to do things other than sleeping at some point.
  • Admin
    Admin about 9 years
    Please see why while(true) and Thread.Sleep together are "evil", and you are also lacking a CancellationToken or anything alike to control a break-scenario.
  • Jay
    Jay about 9 years
    The code above does what it needs to. It starts once and runs until the program is closed. A cancellation token is not required, because I never intend to end the task. It's a small bit of code that does not take up any significant resources. It runs in the background and is lightweight. The thread you linked to explains that Thread.Sleep is not very precise and that it takes up 1Mb of memory (= no big deal). The linked-to article explains that it's evil to not use Thread.Sleep in a while loop, because the condition check will max out the cpu. Feel free to provide a better solution.
  • Ignacio Soler Garcia
    Ignacio Soler Garcia almost 7 years
    This should be the accepted answer in my humble opinion
  • nflash
    nflash over 6 years
    I actually didn't notice the while (true) until I read the comments. Why would you want to keep this task running forever? You are actually wasting resources by spinning the cycle every second, scheduling work on the main thread and probably forcing a redraw of the app. Why not just to call the BeginInvoke whenever (and only when) the data (taskDataGenerator.Count and/or taskDataGenerator.RandomData) changes? You would have "real-time" updates (as opposed to wait 1 sec) and would run the update on UI only when the data changes, and therefore avoiding unnecessary redraws of the app.
  • Jay
    Jay about 6 years
    The task needs to run 'forever' because it needs to run all throughout the application's lifetime. I wanted to keep resource usage to a minimum, hence the 1 second delay, which is an eternity in CPU time. I'm sure there are better solutions, but this thing worked when I needed it to. Take it or leave it. I'm no longer involved with the project this was for.
  • Chris Moschini
    Chris Moschini over 5 years
    If you happen to be working on a Xamarin cross-platform mobile project, the equivalent command is Xamarin.Forms.Device.BeginInvokeOnMainThread
  • AlvinfromDiaspar
    AlvinfromDiaspar about 4 years
    @IgnacioSolerGarcia - But i dont believe this solves the problem. The list of items is being updated by the UI thread, BUT, the thread.sleep is still going to make the UI freeze.
  • Ignacio Soler Garcia
    Ignacio Soler Garcia about 4 years
    @AlvinfromDiaspar: Sleep(1000) is executed on a Task so I do not see how it could block the UI. Have you tried the code?