Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Responsive Applications with Asynchronous Programming

4.88/5 (6 votes)
7 Nov 2016CPOL6 min read 23.4K  
In this article by Dirk Strauss, author of the book C# Programming Cookbook, he sheds some light on how to handle events, exceptions and tasks in asynchronous programming, making your application responsive.

In this article by Dirk Strauss, author of the book C# Programming Cookbook, he sheds some light on how to handle events, exceptions and tasks in asynchronous programming, making your application responsive.

(For more resources related to this topic, see here.)

Handling tasks in asynchronous programming

Task-Based Asynchronous Pattern (TAP) is now the recommended method to create asynchronous code. It executes asynchronously on a thread from the thread pool and does not execute synchronously on the main thread of your application. It allows us to check the task's state by calling the Status property.

Getting ready

We will create a task to read a very large text file. This will be accomplished using an asynchronous Task.

How to do it…

  1. Create a large text file (we called ours taskFile.txt) and place it in your C:\temp folder:

    Image 1

  2. In the AsyncDemo class, create a method called ReadBigFile() that returns a Task<TResult> type, which will be used to return an integer of bytes read from our big text file:
    C++
    public Task<int> ReadBigFile()
    {   
    
    }
  3. Add the following code to open and read the file bytes. You will see that we are using the ReadAsync() method that asynchronously reads a sequence of bytes from the stream and advances the position in that stream by the number of bytes read from that stream. You will also notice that we are using a buffer to read those bytes.
    C++
    public Task<int> ReadBigFile()
    {
        var bigFile = File.OpenRead(@"C:\temp\taskFile.txt");
        var bigFileBuffer = new byte[bigFile.Length];
        var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, "    (int)bigFile.Length);
    
        return readBytes;
    }

    Exceptions you can expect to handle from the ReadAsync() method are ArgumentNullException, ArgumentOutOfRangeException, ArgumentException, NotSupportedException, ObjectDisposedException and InvalidOperatorException.

  4. Finally, add the final section of code just after the var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, (int)bigFile.Length); line that uses a lambda expression to specify the work that the task needs to perform. In this case, it is to read the bytes in the file:
    C++
    public Task<int> ReadBigFile()
    {
        var bigFile = File.OpenRead(@"C:\temp\taskFile.txt");
        var bigFileBuffer = new byte[bigFile.Length];
        var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, (int)bigFile.Length);
        readBytes.ContinueWith(task =>
        {
            if (task.Status == TaskStatus.Running)
                Console.WriteLine("Running");
            else if (task.Status == TaskStatus.RanToCompletion)
                Console.WriteLine("RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
                Console.WriteLine("Faulted");
    
            bigFile.Dispose();
        });
        return readBytes;
    }
  5. If not done so in the previous section, add a button to your Windows Forms application's Form designer. On the winformAsync form designer, open Toolbox and select the Button control, which is found under the All Windows Forms node:

    Image 2

  6. Drag the button control onto the Form1 designer:

    Image 3

  7. With the button control selected, double-click the control to create the click event in the code behind. Visual Studio will insert the event code for you:
    C++
    namespace winformAsync
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
    
            }
        }
    }
  8. Change the button1_Click event and add the async keyword to the click event. This is an example of a void returning asynchronous method:
    C++
    private async void button1_Click(object sender, EventArgs e)
    {
    
    }
  9. Now, make sure that you add code to call the AsyncDemo class's ReadBigFile() method asynchronously. Remember to read the result from the method (which are the bytes read) into an integer variable:
    C++
    private async void button1_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Start file read");
        Chapter6.AsyncDemo oAsync = new Chapter6.AsyncDemo();
        int readResult = await oAsync.ReadBigFile();
        Console.WriteLine("Bytes read = " + readResult);
    }
  10. Running your application will display the Windows Forms application:

    Image 4

  11. Before clicking on the button1 button, ensure that the Output window is visible:

    Image 5

  12. From the View menu, click on the Output menu item or type Ctrl + Alt + to display the Output window. This will allow us to see the Console.Writeline() outputs as we have added them to the code in the Chapter6 class and in the Windows application.
  13. Clicking on the button1 button will display the outputs to our Output window. Throughout this code execution, the form remains responsive.

    Image 6

    Take note though that the information displayed in your Output window will differ from the screenshot. This is because the file you used is different from mine.

How it works…

The task is executed on a separate thread from the thread pool. This allows the application to remain responsive while the large file is being processed. Tasks can be used in multiple ways to improve your code. This recipe is but one example.

Exception handling in asynchronous programming

Exception handling in asynchronous programming has always been a challenge. This was especially true in the catch blocks. As of C# 6, you are now allowed to write asynchronous code inside the catch and finally block of your exception handlers.

Getting ready

The application will simulate the action of reading a logfile. Assume that a third-party system always makes a backup of the logfile before processing it in another application. While this processing is happening, the logfile is deleted and recreated. Our application, however, needs to read this logfile on a periodic basis. We, therefore, need to be prepared for the case where the file does not exist in the location we expect it in. Therefore, we will purposely omit the main logfile, so that we can force an error.

How to do it…

  1. Create a text file and two folders to contain the logfiles. We will, however, only create a single logfile in the BackupLog folder. The MainLog folder will remain empty:

    Image 7

  2. In our AsyncDemo class, write a method to read the main logfile in the MainLog folder:
    C++
    private async Task<int> ReadMainLog()
    {
        var bigFile = "    File.OpenRead(@"C:\temp\Log\MainLog\taskFile.txt");
        var bigFileBuffer = new byte[bigFile.Length];
        var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, "    (int)bigFile.Length);
        await readBytes.ContinueWith(task =>
        {
            if (task.Status == TaskStatus.RanToCompletion)
                Console.WriteLine("Main Log RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
                Console.WriteLine("Main Log Faulted");
    
            bigFile.Dispose();
        });
        return await readBytes;
    }
  3. Create a second method to read the backup file in the BackupLog folder:
    C++
    private async Task<int> ReadBackupLog()
    {
        var bigFile = "    File.OpenRead(@"C:\temp\Log\BackupLog\taskFile.txt");
        var bigFileBuffer = new byte[bigFile.Length];
        var readBytes = bigFile.ReadAsync(bigFileBuffer, 0, "    (int)bigFile.Length);
        await readBytes.ContinueWith(task =>
        {
            if (task.Status == TaskStatus.RanToCompletion)
                Console.WriteLine("Backup Log "            RanToCompletion");
            else if (task.Status == TaskStatus.Faulted)
                Console.WriteLine("Backup Log Faulted");
    
            bigFile.Dispose();
        });
        return await readBytes;
    }

    In actual fact, we would probably only create a single method to read the logfiles, passing only the path as a parameter. In a production application, creating a class and overriding a method to read the different logfile locations would be a better approach. For the purposes of this recipe, however, we specifically wanted to create two separate methods so that the different calls to the asynchronous methods are clearly visible in the code.

  4. We will then create a main ReadLogFile() method that tries to read the main logfile. As we have not created the logfile in the MainLog folder, the code will throw a FileNotFoundException. It will then run the asynchronous method and await that in the catch block of the ReadLogFile() method (something which was impossible in the previous versions of C#), returning the bytes read to the calling code:
    C++
    public async Task<int> ReadLogFile()
    {
        int returnBytes = -1;
        try
        {
            Task<int> intBytesRead = ReadMainLog();
            returnBytes = await ReadMainLog();
        }
        catch (Exception ex)
        {
            try
            {
                returnBytes = await ReadBackupLog();
            }
            catch (Exception)
            {
                throw;
            }
        }
        return returnBytes;
    }
  5. If not done so in the previous recipe, add a button to your Windows Forms application's Form designer. On the winformAsync form designer, open Toolbox and select the Button control, which is found under the All Windows Forms node:

    Image 8

  6. Drag the button control onto the Form1 designer:

    Image 9

  7. With the button control selected, double-click on the control to create the click event in the code behind. Visual Studio will insert the event code for you:
    C++
    namespace winformAsync
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
    
            }
        }
    }
  8. Change the button1_Click event and add the async keyword to the click event. This is an example of a void returning an asynchronous method:
    C++
    private async void button1_Click(object sender, EventArgs e)
    {
    
    }
  9. Next, we will write the code to create a new instance of the AsyncDemo class and attempt to read the main logfile. In a real-world example, it is at this point that the code does not know that the main logfile does not exist:
    C++
    private async void button1_Click(object sender, EventArgs "e)
    {
        Console.WriteLine("Read backup file");
        Chapter6.AsyncDemo oAsync = new Chapter6.AsyncDemo();
        int readResult = await oAsync.ReadLogFile();
        Console.WriteLine("Bytes read = " + readResult);
    }
  10. Running your application will display the Windows Forms application:

    Image 10

  11. Before clicking on the button1 button, ensure that the Output window is visible:

    Image 11

  12. From the View menu, click on the Output menu item or type Ctrl + Alt + O to display the Output window. This will allow us to see the Console.Writeline() outputs as we have added them to the code in the Chapter6 class and in the Windows application.
  13. To simulate a file not found exception, we deleted the file from the MainLog folder. You will see that the exception is thrown, and the catch block runs the code to read the backup logfile instead:

    Image 12

How it works…

The fact that we can await in catch and finally blocks allows developers much more flexibility because asynchronous results can consistently be awaited throughout the application. As you can see from the code we wrote, as soon as the exception was thrown, we asynchronously read the file read method for the backup file.

Summary

In this article we looked at how TAP is now the recommended method to create asynchronous code. How tasks can be used in multiple ways to improve your code. This allows the application to remain responsive while the large file is being processed also how exception handling in asynchronous programming has always been a challenge and how to use catch and finally block to handle exceptions.

License

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