DotNetCurry Logo

Using WPF 4.5 to implement a Responsive UI with Asynchronous Operations

Posted by: Mahesh Sabnis , on 9/24/2015, in Category WPF
Views: 18825
Abstract: This articles makes use of the Task class in WPF for creating responsive and interactive WPF apps

With the .NET 4.0 release, we got the Task Parallel Library (TPL) which provided APIs for performing Parallel and Task Based Asynchronous Operations. The TPL provides classes like Task and Parallel which were used for performing asynchronous programming and parallel operations respectively. In .NET 4.5 with C# 5.0 we got the async and await keywords for representing asynchronous methods. In WPF, we can make use of the Task class for creating responsive and interactive applications.

 

 

Task Class

This class represents an asynchronous operation and is used for managing unit of work. This class exposes several methods to manage tasks and synchronization task result with UI elements.

Parallel Class

This class provides support for parallel loops e.g. Parallel.For(), Parallel.ForEach().

In this article, we will implement a responsive asynchronous operation using WPF. We will use a XML file as our data store. The application will read data from XML file and will load the data in a DataGrid. We will control the data loading using a Task so that if required, we can cancel the operation without affecting the User experience.

Step 1: Open Visual Studio 2013 Community Edition and create a WPF application, name this application as WPF45_TaskbasedProgress. In this project in bin\debug folder add a new XML file name Company.xml. Add the following sample data in it:

<EmployeeInfo>
  <Employee>
    <EmpNo>101</EmpNo>
    <EmpName>A</EmpName>
    <Salary>12000</Salary>
  </Employee>
  <Employee>
    <EmpNo>102</EmpNo>
    <EmpName>B</EmpName>
    <Salary>13000</Salary>
  </Employee>
</EmployeeInfo>

This is just some sample data. Free feel to add several records in this file. I have added about 50 records in Company.xml.

Step 2: Open the MainWindow.xaml and add the following XAML to it:

<Window x:Class="WPF45_TaskbasedProgress.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="741.323" Width="852.381">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="45*"/>
            <RowDefinition Height="210*"/>
            <RowDefinition Height="47*"/>
            <RowDefinition Height="54*"/>
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Text="Employee Information Manager"
                   TextAlignment="Center"
                   FontFamily="Times New Roman" FontWeight="ExtraBold"
                   FontSize="50" Margin="0,0,-0.4,0.6" Grid.ColumnSpan="2"/>
        <DataGrid Grid.ColumnSpan="2" Margin="0,4.4,-0.4,58.6"
             AutoGenerateColumns="False"  ColumnWidth="*"     Name="dgEmp" Grid.Row="1">
            <DataGrid.Columns>
                <DataGridTextColumn Header="EmpNo" Binding="{Binding EmpNo}"></DataGridTextColumn>
                <DataGridTextColumn Header="EmpName" Binding="{Binding EmpName}"></DataGridTextColumn>
                <DataGridTextColumn Header="Salary" Binding="{Binding Salary}"></DataGridTextColumn>
                <DataGridTextColumn Header="Tax" Binding="{Binding Tax}"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <Button Content="Load Employees" Grid.Row="2" FontSize="20"
                 Name="btnLoadEmployee" Click="btnLoadEmployee_Click" Margin="0,0.4,212.4,0"/>
        <Button Content="Calculate  Tax" Grid.Row="2"
                    FontSize="20"  Name="btnCalculateTax" Click="btnCalculateTax_Click" Margin="288,0.4,358.6,0" Grid.ColumnSpan="2"/>
        <ProgressBar Grid.Row="3" Grid.ColumnSpan="2" Name="progress" Margin="0,0,-0.4,0" />
        <Button Content="Cancel" Grid.Row="2"
            FontSize="20"  x:Name="btnCancel" 
                Click="btnCancel_Click" Margin="157.6,0.4,66.6,0" Grid.Column="1"/>
        <TextBlock HorizontalAlignment="Left" Margin="0,380.4,0,0" 
                   Name="txtStatus"
                   Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Top" Height="30" Width="836" Grid.ColumnSpan="2"/>

    </Grid>
</Window>


The XAML has a DataGrid for displaying Employee Data. This contains buttons to Load Employee data, Calculate Tax and Cancelation of the operation.

Step 4: In the code behind, declare the following objects:

ObservableCollection<Employee> employees;
CancellationTokenSource cancelToken;
Progress<double> progressOperation;

The CancellationTokenSource class is used to assign cancellation token to the task so that it can be cancelled. The Progress<T> class is used to report the progress value so that the ProgressBar on the UI can show the Progress of Data Change.

Step 5: Add the Employee class in the Code-Behind:

public class Employee
{
    public int EmpNo { get; set; }
    public string EmpName { get; set; }
    public int Salary { get; set; }
    public decimal Tax { get; set; }
}

Step 6: Add the following code in the code-behind:

public MainWindow()
{
    InitializeComponent();

    Employees = new ObservableCollection<Employee>();
    btnCancel.IsEnabled = false;
}

// Displaying Employees in DataGrid 
private async void btnLoadEmployee_Click(object sender, RoutedEventArgs e)
{
    cancelToken = new CancellationTokenSource();
    btnLoadEmployee.IsEnabled = false;
    btnCancel.IsEnabled = true;
    txtstatus.Text = "Loading.....";
    progressOperation = new Progress<double>(value => progress.Value = value);

    try
    {

        var Emps = await LoadEmployeesAsync(cancelToken.Token, progressOperation);

        foreach (var item in Emps)
        {
            dgEmp.Items.Add(item);
        }

        txtStatus.Text ="Operation Completed";
    }
    catch (OperationCanceledException ex)
    {
        txtStatus.Text ="Operation cancelled" + ex.Message;
    }
    catch (Exception ex)
    {
        txtStatus.Text = "Operation cancelled" + ex.Message;
    }
    finally
    {
        cancelToken.Dispose();
        btnLoadEmployee.IsEnabled = true;
        btnCancel.IsEnabled = false;
    }

    
}


// Async Method to Load Employees

async Task<ObservableCollection<Employee>> LoadEmployeesAsync(CancellationToken ct, IProgress<double> progress)
{
    Employees.Clear();
    var task = Task.Run(() => {
        var xDoc = XDocument.Load("Company.xml");

        var Res = (from emp in xDoc.Descendants("Employee")
                  select new Employee()
                  {
                       EmpNo = Convert.ToInt32(emp.Descendants("EmpNo").First().Value),
                       EmpName = emp.Descendants("EmpName").First().Value.ToString(),
                       Salary = Convert.ToInt32(emp.Descendants("Salary").First().Value)
                  }).ToList();
       

       int recCount = 0;

       foreach (var item in Res)
        {
            Thread.Sleep(100);
            ct.ThrowIfCancellationRequested();
            Employees.Add(item);
            ++recCount;
            progress.Report(recCount * 100.0 / 50);
        }
        
        return Employees;
    
    });

    return await task;
}

private async void btnCalculateTax_Click(object sender, RoutedEventArgs e)
{
    cancelToken = new CancellationTokenSource();
    btnLoadEmployee.IsEnabled = false;
    btnCalculateTax.IsEnabled = false;
    btnCancel.IsEnabled = true;
    txtStatus.Text = "Calculating.....";
    progressOperation = new Progress<double>(value => progress.Value = value);
    try
    {
        dgEmp.Items.Clear();
        int recCount = 0;

        foreach (Employee Emp in Employees)
        {
            dgEmp.Items.Add(await CalculateTaxPerRecord(Emp, cancelToken.Token, progressOperation));
            ++recCount;
            ((IProgress<double>)progressOperation).Report(recCount * 100.0 / 50);
        }

        txtStatus.Text = "Operation Completed";
    }
    catch (OperationCanceledException ex)
    {
        txtStatus.Text = "Operation cancelled" + ex.Message;
    }
    catch (Exception ex)
    {
        txtStatus.Text = "Operation cancelled" + ex.Message;
    }
    finally
    {
        cancelToken.Dispose();
        btnCalculateTax.IsEnabled = true;
        btnCancel.IsEnabled = false;
    }
}



// Calcluate the Tax for every Employee Record.
async Task<Employee> CalculateTaxPerRecord(Employee Emp, CancellationToken ct, IProgress<double> progress)
{
     var tsk = Task<Employee>.Run(() =>
     {
            Thread.Sleep(100);
            ct.ThrowIfCancellationRequested();
            Emp.Tax = Convert.ToInt32(Emp.Salary * 0.2);
            return Emp;
        });

    
    return await tsk;
}



private void btnCancel_Click(object sender, RoutedEventArgs e)
{
    cancelToken.Cancel();
}

The above code following features:

 

- The method LoadEmployeeAsync, accepts CancellationToken and IProgress<T> parameter. This method reads the Company.xml file on the Task object and the stores it’s Data into the List of Employees. After reading this data, it is added in the Employees ObservableCollection<T> using a loop. This loop monitors the Task Cancellation using ThrowIfCancellationRequested() method. This method throws an exception and cancels the currently running Task.

- The btnLoadEmployee_Click is the method that gets executed on Load Employee button click event. This method declares the CancellationTokenSource object and the Progress<T> object. These objects are passed to the LoadEmployeeAsync method. The Progress<T> object monitor the Task progress for the Load Employee and accordingly sets the value for the ProgressBar of name progress.

- The CalculateTaxPerRecord() method is used to calculate the Tax for each Employee. This method like the LoadEmployeeAsync() method also monitors the Task with Cancellation and Progress. This method is called on the Click event of the Calculate Tax button with method as btnCalculateTax_Click.

- The following code is used to set the ProgressBar’ s progress value

++recCount;
((IProgress<double>)progressOperation).Report(recCount * 100.0 / 50);

This code iterates through all the records and using the Report method of the IProgress<T>, sets the progress value for the ProgressBar.

- The btnCancel_Click method executes on the click event of the Cancel button, requesting for the Task Cancellation.

Run the application, the following WPF Window will be displayed:

wpf-employee

The window has the Cancel button disabled. Click on the Load Employees button. Now Load Employees button gets disabled and the Cancel button gets enabled. The progress bar will start running showing the progress of the data loaded. Once the Progress Bar reaches to its maximum, Employees will be loaded in the DataGrid as shown in the following image

wpf-employee-info

The Task can be cancelled in between by clicking on the Cancel button as shown in the following image

cancel-task

Once all Employees are loaded, click on the Calculate Tax button, this will calculate Tax on each Employee as shown in the following Image

calculate-tax

The above image depicts the progress as the tax gets calculated for each Employee.

Conclusion

Applications have to perform long running tasks which if done synchronously, often leads to unresponsive UI. The Task parallel library offers a number of methods that lets you perform task asynchronously. Task allows you to schedule activities, create parallel workflows by chaining parent and child tasks together, as well as handle exceptions. WPF is a technology which supports high responsive application development using the Task Parallel Library.

Download the entire source code of this library (Github)

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Mahesh Sabnis is a DotNetCurry author and Microsoft MVP having over 17 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!