Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

WinForms and TPL - Achieving quick Multitasking and Responsive User Interface

Rate me:
Please Sign up or sign in to vote.
4.82/5 (17 votes)
26 Mar 2014CPOL9 min read 48.8K   2.5K   65   21
Using the Task Parallel Library (TPL) for multitasking and UI responsiveness in WinForms

If you have problems with codeproject download links above, try the skydrive links below


Source - http://1drv.ms/OQNUYU
Exe only - http://1drv.ms/1l0wBj3

Introduction

Ok. First things first. This article would in no way describe any super advanced technique or describe anything novel. TPL is still relatively new to many of the developers, and when it comes to multitasking and UI responsiveness in desktop applications, most of the examples on codeproject tend to demonstrate TPL with WPF. Although it is relatively the same code that would do the work in WinForms as well, I try to write down a WinForms sample here. So here's my attempt demonstrate how easy it is in TPL to do multitasking and update a Windows forms user interface. Constraining to .Net 4.0 it is expected that you are familiar with Action delegate.



Image 1


This article would help you in:

1. New task creation
2. UI responsiveness and UI updating
3. Passing data to a task
4. Task chaining
5. A sample application to jumpstart your first WinForms multitasking endeavour

What this article does not explain:

1. Task Cancellation Operations
2. Waiting till task(s) completion
3. Synchronization mechanisms for data sharing
4. Progress reporting via the IProgress interface
5. What is a Task? Why Tasks?
6. What is a ThreadPool? What not QueueUserWorkItem?

Prerequisites

Mandatory:
Threads Versus Tasks or at least the introduction and first few paragraphs of Task Parallel
Library: 1 of n


Optional:
Choosing Between the Task Parallel Library and the ThreadPool


[Simply put, a Task is method that would be scheduled to run on a .Net thread pool's thread. The .Net thread pool creates and manages threads as and when required.]

Background

Before TPL, I have been using a combination of ThreadPool's QueueUserWorkItem, InvokeRequired, and Invoke. I couldn't really get a grasp of WindowsFormsSynchronizationContext.Post method. So the mix of BackgroundWorker or QueueUserWorkItem, InvokeRequired, and Invoke really appealed to be a good pattern until TPL came along to simplify things to a greater extent.

Under normal circumstances, you may be in situations where:

  1. you would like to update the UI, once a particular task is complete;
  2. or you may choose to update the UI when a task has reached a checkpoint (certain place in the code, or after having reached a business logic) or a well defined updating point according to your standards;
  3. or if you are continuously polling against a resource, you may want to update the UI as frequently as the resource polling routine polls for the resource.
    and so much more situations alike.
A couple of points to remember:
  1. <span>Task</span><span>.Factory.StartNew()</span> takes a method as a parameter and schedules it as a task to be run on one of the threads available from the .Net's thread pool
  2. <span>TaskScheduler </span><span>uiScheduler =</span><span> </span><span>TaskScheduler</span><span>.FromCurrentSynchronizationContext();</span> returns a TaskScheduler instance of the thread currently executing the statement. So when called with the UI thread (or the main thread), we get a TaskScheduler instance of the UI thread (or the main thread)

Note:

1. You may see the word multitasking and multithreading used interchangeably; although multithreading refers to managing multiple operating system threads while multitasking in .Net parlance refers to managing multiple Task instances. A Task is later scheduled on a .Net thread to run.
2. If you haven't done multithreading in WinForms, then you must know that the Main thread that created the UI (aka UI thread) is the only thread that is allowed to update the UI. This is a strict rule, and if you violate it, i.e., if you try to update the UI from another thread, then a System.InvalidOperationException is thrown. You would definitely get this exception in Debug mode (when the debugger is attached), you may or may not get this Exception when the debugger is not attached.

The sample application

The WinForms application - ResponsiveWinFormsWithTPL - presented in this article is just a user interface with button click event handlers and a couple of other methods do some work and update the UI. There are four ProgressBar objects and four Button objects close to each one of them. When you click the button the application is going to do some work - which is actually Thread.Sleep(int) - and going to update the progress bars, and the labels close to them, while keeping the UI responsive.

On click of a button, the actual work is delegated to a method in another class (from which we would like to update the UI) that actually does the hard work of performing any designated operations. Also, most of the times the work done by your threads reside in some other method in some other class or dll.

Image 2

In our example class BusinessLayer under BusinessLayer.cs is going to demonstrate the actual work done by a task. Which means the actual worker tasks are spawned from the BusinessLayer's static methods. When you want to update progress from the BusinessLayer back to your UI, you would definitely need some kind of call back mechanism. What comes in handy here is an Action<T> delegate that we would fire from the BusinessLayer when we want to update the progress of the current task back to the UI. So pay attention that every method in the BusinessLayer class takes an action delegate as the second parameter, through which they will update progress back to the caller. This is in fact a common a common pattern you could follow, any method that wants to update it's caller once in a while could take an Action delegate as a parameter.


The four fictionally named static methods in a class BusinessLayer are as follows:
  1. The method named <span>ProcessData</span> would update the UI once the task is complete.
  2. The method named <span>PerformInternalValidationsOnData</span> would update the UI once a certain amount of task is complete.
  3. The method named <span>PrepareTransformationsForProcessing</span> would update the UI very frequently at an interval of 15 milliseconds.
  4. The method named <span>PrepareLaunchSequenceForData</span> would update the UI with by spawning a new task to call the call back itself. The last method would also demonstrate the right way passing data to a task i.e. the variable<span> </span>i sent in the name of the variable state.

Using the code

So, as mentioned before, if you'd like a method in your library to report status back to the caller, then add another parameter to the method which looks something like Action updateCaller, or something like Action<string> updateCallerWithAMessage. When you want to update status back to the caller you just call the updateCaller() delegate. And then it is upto the updateCaller to update the UI or log or to do whatever.

To put things in perspective let's take a look at the method private void button1_Click(object sender, EventArgs e) in MainForm.cs which calls in to the BusinessLayer method called <span>public static void ProcessData(int data, Action<string> updateUICallBack) </span>. All the ProcessData method expects is an integer parameter, and a delegate that accepts a string (refer to the ProcessData method's signature from the solution explorer image at the top, or the entire method definition at the bottom).

C#
private void button1_Click(object sender, EventArgs e)
{
    button1.Enabled = false;
    pictureBox1.Show();

    BusinessLayer.ProcessData(count, (message) => UpdateProgressBar1(message));
    ++count;
}

There is another method in MainForm.cs called private void UpdateProgressBar1(string statusMessage) which is a method that accepts a string parameter. So while calling BusinessLayer.ProcessData we shall pass the UpdateProgressBar1 method as an argument to our Action<string> parameter updateUICallBack.

C#
private void UpdateProgressBar1(string statusMessage)
{
    Task.Factory.StartNew(() =>
        {
            progressBar1.PerformStep();
            label1.Text = statusMessage;
            pictureBox1.Hide();
            button1.Enabled = true;
        }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
}


Clearly, the method UpdateProgressBar1 updates the UI components like the progressBar1, label1, and so on, with a typical syntax for creating and running a Task with the Task.Factory.StartNew method. uiScheduler is the fourth parameter to the StartNew method which is very vital. uiScheduler is of type TaskScheduler that gets assigned in the MainForm_Load method as below:

C#
private void MainForm_Load(object sender, EventArgs e)
{
    uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}


Every time there is an UI update, it is safe is spawn a Task with a TaskScheduler instance that is assigned with the UI thread's context. Assigning the TaskSchedler with TaskScheduler.FromCurrentSynchronizationContext during the MainForm_Load or in the constructor MainForm() is guaranteed to return a TaskScheduler instance from the UI thread's synchronization context.

In the ProcessData method we spawn a new Task with Task.Factory.StartNew that sleeps for 2000 milliseconds, and once that task is complete, it continues with (via ContinueWith) a delegate method call to updateUICallBack whose argument will be UpdateProgressBar1 method.

C#
/// <summary>
/// Method will update the UI once the task is entirely complete
/// </summary>
/// <param name="data"></param>
/// <param name="updateUICallBack"></param>
public static void ProcessData(int data, Action<string> updateUICallBack)
{
    Task.Factory.StartNew(
        () =>
        {
            Thread.Sleep(2000); //simulateWork, do something with the data received
        })
        .ContinueWith(
        (cw) =>
        {
            updateUICallBack(string.Format("Finished step {0}", data));
        }
    );
}


So, here's what we have so far: button click event calling in to a library method ProcessData. ProcessData method does some work, and on completion of the work, it calls the call back method UpdateProgressBar1 to update the UI via a new Task with uiScheduler.

Image 3

The ContinueWith task will only run after the task in StartNew has completed. I did not plan to explain the syntax for creating a task, however feel free to go through the entire solution, and I am sure you would comprehend then.

That is one example of updating the UI while a task is complete.

If you take a look at the method PerformInternalValidationsOnData, it updates the caller once a certain percentage of the task is complete.

C#
/// <summary>
/// Method will update the UI while the task is running
/// </summary>
/// <param name="data"></param>
/// <param name="updateUICallBack"></param>
public static void PerformInternalValidationsOnData(object data, Action<string, bool> updateUICallBack)
{
    Task.Factory.StartNew(
        () =>
        {
            Thread.Sleep(10); //simulateWork, do something with the data received
            updateUICallBack("Running validation 20%", false);
            Thread.Sleep(1000); //simulateWork, do something with the data received
            updateUICallBack("Running validation 40%", false);
            Thread.Sleep(800); //simulateWork, do something with the data received
            updateUICallBack("Running validation 60%", false);
            Thread.Sleep(700); //simulateWork, do something with the data received
            updateUICallBack("Running validation 80%", false);
            Thread.Sleep(1000); //simulateWork, do something with the data received
            updateUICallBack("Validations complete - 100%", true);
        });
}


There are other two methods namely PrepareTransformationsForProcessing and PrepareLaunchSequenceForData in the BusinessClass, and other methods in MainForm.cs such as UpdateProgressBar2, UpdateProgressBar3, and UpdateProgressBar4, take a look them and modify or design your first WinForms with TPL. That's it for now.

Passing Data to Tasks

The method PrepareLaunchSequenceForData creates a new task within a task, to update the UI. If you pay close attention, when we are calling the updateUICallBack via StartNew, we are passing i as an argument, and our Task accepts a new parameter called state. And state is used inside our task. Whenever you want to pass data to a task, you would have to pass it as a parameter to the StartNew and access it inside the new Task. If you uncomment the commented task code and commented the Task with parameter state then you would actually see the difference in the UI. The last status bar would sometimes say 101% done, instead of 100% done.

Image 4

C#
/// <summary>
/// Method will update UI very frequently with new task while the original task is running
/// </summary>
/// <param name="data"></param>
/// <param name="updateUICallBack"></param>
public static void PrepareLaunchSequenceForData(object data, Action<string, bool> updateUICallBack)
{
    Random rand = new Random();
    Task.Factory.StartNew(
        () =>
        {
            for (int i = 0; i <= 100; i++)
            {
                Thread.Sleep(rand.Next(10, 11)); //simulateWork, do something with the data received

                Task.Factory.StartNew(
                    (state) =>
                    {
                        updateUICallBack(string.Format("Preparing launch requence for data {0}% done", state), false);
                    }, i);

                //Incorrect way of passing data to a task.
                //
                //The value of variable i may be different from the time the task was scheduled
                //and from the time the task actually runs
                //
                //If you comment the above task, and run the below task, sometimes you may see 101% done in the UI, instead of 100% done.
                //
                //Task.Factory.StartNew(
                //   () =>
                //   {
                //       updateUICallBack(string.Format("Preparing launch requence for data {0}% done", i), false);
                //   });
            }

                Task.Factory.StartNew(
                        () =>
                        {
                            updateUICallBack("Preparing launch requence for data 100% complete", true);
                        });

        });
}

Points of Interest

The motto of this article was to demonstrate and provide a jumpstart sample for people that need a responsive multithreaded application in WinForms. It is also a very elementary example. If you liked the idea of this article, you would also prefer reading

  1. Synchronous and Asynchronous Delegate Types
  2. ThreadPool vs Tasks, ThreadPool vs. Tasks
  3. .NET asynchrony in the UI context
  4. "Task.Factory.StartNew" vs "new Task(...).Start"
  5. Using the Task Parallel Library (TPL) in WPF
  6. Task Parallel Library: 6 of n

History

Version 1 : Initial Post
Version 2 : Added Prerequisites and necessary links on Points of Interest section

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
India India
I like programming for Windows using .Net Framework.

http://renouncedthoughts.wordpress.com/about

gmaran23

Comments and Discussions

 
QuestionCancellation Issue Pin
helhadad18-Jan-22 13:12
helhadad18-Jan-22 13:12 
QuestionVery cool but how would one apply this Pin
Member 1394912217-Sep-18 6:44
Member 1394912217-Sep-18 6:44 
How would one apply this to a data set. During the phases of updating the database to run this on its own task. I am not sure how to end edit the binding source/ update the table adapters set, etc.
PraiseExcellent resource, thank you Pin
Member 1140848921-Sep-16 5:31
Member 1140848921-Sep-16 5:31 
PraiseGreat Article Pin
Joydeep177-May-16 5:40
Joydeep177-May-16 5:40 
GeneralMy vote of 5 Pin
Member 1154049419-Mar-15 21:14
Member 1154049419-Mar-15 21:14 
Questionabout PrepareLaunchSequenceForData Pin
benny85669427-Mar-14 21:06
benny85669427-Mar-14 21:06 
AnswerRe: about PrepareLaunchSequenceForData Pin
gmaran2327-Mar-14 22:42
gmaran2327-Mar-14 22:42 
GeneralRe: about PrepareLaunchSequenceForData Pin
benny85669428-Mar-14 0:21
benny85669428-Mar-14 0:21 
Questionuseful example Pin
Ivan B Dcosta26-Mar-14 20:45
Ivan B Dcosta26-Mar-14 20:45 
AnswerRe: useful example Pin
gmaran2327-Mar-14 22:42
gmaran2327-Mar-14 22:42 
QuestionGood Work Pin
Sirstrafe Prime26-Mar-14 3:55
professionalSirstrafe Prime26-Mar-14 3:55 
AnswerRe: Good Work Pin
gmaran2327-Mar-14 22:43
gmaran2327-Mar-14 22:43 
Generalexcellent! Pin
Southmountain25-Mar-14 8:47
Southmountain25-Mar-14 8:47 
GeneralRe: excellent! Pin
gmaran2325-Mar-14 20:51
gmaran2325-Mar-14 20:51 
QuestionDownload does not work Pin
descartes25-Mar-14 2:52
descartes25-Mar-14 2:52 
AnswerRe: Download does not work Pin
gmaran2325-Mar-14 2:56
gmaran2325-Mar-14 2:56 
QuestionBoth attached zip-files are corrupded Pin
Johann Krenn25-Mar-14 2:26
Johann Krenn25-Mar-14 2:26 
AnswerRe: Both attached zip-files are corrupded Pin
gmaran2325-Mar-14 2:37
gmaran2325-Mar-14 2:37 
GeneralRe: Both attached zip-files are corrupded Pin
Johann Krenn25-Mar-14 3:13
Johann Krenn25-Mar-14 3:13 
GeneralRe: Both attached zip-files are corrupded Pin
gmaran2325-Mar-14 3:17
gmaran2325-Mar-14 3:17 
AnswerRe: Both attached zip-files are corrupded Pin
gmaran2325-Mar-14 2:56
gmaran2325-Mar-14 2:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.