0

Here is my MainWindow.

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid Loaded="EmployeesGridLoaded">
        <Grid.RowDefinitions>
            <RowDefinition Height="6*" />
            <RowDefinition  />
        </Grid.RowDefinitions>
        <DataGrid x:Name="gEmployees" HorizontalAlignment="Left" Margin="10,10,0,0" 
                  VerticalAlignment="Top" AlternatingRowBackground="LightBlue" AlternationCount="2" AutoGenerateColumns="False" Grid.Row="0">
            <DataGrid.Columns>
                <DataGridCheckBoxColumn Header="Select" Binding="{Binding Select}"  Width="1*" />
                <DataGridTextColumn Header="Last Name" Binding="{Binding LastName}" Width="3*" />
                <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="3*" />
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="1" >
            <Button Content="Process" Margin="5"  Click="Process_Click" />
            <Button Content="Cancel" Margin="5"  Click="Cancel_Click" />
        </StackPanel>

    </Grid>
</Window>

Here is the code.

namespace WpfApplication2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private List<Employee> Employees = null;
        public MainWindow()
        {
            InitializeComponent();
        }

    private void EmployeesGridLoaded(object sender, RoutedEventArgs e)
    {
        Employees = new List<Employee>()
        {
            new Employee() { Select = false, LastName = "Silly", FirstName = "Dude" },
            new Employee() { Select = false, LastName = "Mean", FirstName = "Person" },
            new Employee() { Select = false, LastName = "New", FirstName = "Friend" },
            new Employee() { Select = false, LastName = "My", FirstName = "Buddy" },
        };

        gEmployees.ItemsSource = Employees;
    }

    private void Process_Click(object sender, RoutedEventArgs e)
    {
        Task task = Task.Factory.StartNew(() =>
         {
            string[] tResults = Employees
                    .Where(x => x.Select == true)
                    .AsParallel()
                    .Select(x => this.DoSomethingWithThisEmployeee(x.LastName, x.FirstName).Result)
                    .ToArray();
        });



        Task.WaitAll(task);
        System.Windows.MessageBox.Show("Done");
    }

    private void Cancel_Click(object sender, RoutedEventArgs e)
    {
    }

    private Task<string> DoSomethingWithThisEmployeee(string lastName, string firstName)
    {
        TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
        Random rand = new Random();
        System.Threading.Thread.Sleep(rand.Next(30000, 60000));
        tcs.SetResult(lastName + " - " + firstName);
        return tcs.Task;
    }

}

}

In the code, for each selected employee I have to do some long running operations. Once long running operations are done for all selected employees, the app will do something else. However when these long running operations are running I would like Cancel button to be available so that user can cancel operation. Problem is that Task.WaitAll block the UI and as a result Cancel button is not clickable. Is there any better way of doing this?

whoami
  • 1,689
  • 3
  • 22
  • 45
  • 2
    At first you can't `WaitAll` for a single task.Anyway, you should use continuation. Or move to .NET 4.5 and use asynchronous waiting. – Hamlet Hakobyan Dec 29 '14 at 17:22
  • @Hamlet: I cannot move to .NET 4.5. so continuation will not cause a block? – whoami Dec 29 '14 at 17:29
  • 1
    Continuation will be triggered after task completion. – Hamlet Hakobyan Dec 29 '14 at 17:35
  • Use cancellationtokensource to pass cancellationtoken to each task. You'll need to handle the cancellation within the code for each task, but this allows you to start the tasks and return control to the UI. The cancel button would then cancel the cancellationtokensource. This all works from .Net 4 – VoteCoffee Dec 29 '14 at 22:01
  • http://msdn.microsoft.com/en-us/library/dd997364%28v=vs.100%29.aspx – VoteCoffee Dec 29 '14 at 22:04

1 Answers1

2

There are a few things you are doing wrong:

  1. With async-await you need to use Task.Run instead of Task.TaskFactory.StartNew.
  2. Calling Task.Result, Task.Wait or Task.WaitAll from the UI thread will deadlock your UI.

Using async-await it will be a lot easier if easier if you completely split your logic in UI and non-UI:

private async void Process_Click(object sender, RoutedEventArgs e)
{
    var results = await ProcessAsync();

    System.Windows.MessageBox.Show("Done");
}

Asynchronous methods provide asynchronous execution logic that, most of the time, won't even require Task.Run.

If I understood your example, you are trying to parelelise what DoSomethingWithThisEmployeee does. You shouldn't be mixing AsParallel with Tasks. Your processing is either CPU bound (and for that you should use AaParallel, if processing each item is not dependent on processing another) or blocking (and for that you should use Tasks).

Assuming your processing code is blocking, you can write it like this:

private async Task<IEnumerable<string>> ProcessAsync()
{
    var tasks =
        from e in this.Employees
        where e.Select
        select this.DoSomethingWithThisEmployeee(e.LastName, e.FirstName);

    return await Task.WhenAll(runningTasks);
} 

In order to support cancelation, you need to use a CancellationToken and pass it along to all code that accepts a CancellationToken and use it to cancel execution:

private CancellationTokenSource tcs;
private async void Process_Click(object sender, RoutedEventArgs e)
{
    try
    {
        this.tcs = new CancellationTokenSource();
        var results = await ProcessAsync(tcs.Token);

        System.Windows.MessageBox.Show("Done");
    }
    catch OperationCanceledException ex
    {
        System.Windows.MessageBox.Show("Canceled");
    }
    catch Exception ex
    {
        System.Windows.MessageBox.Show("Error");
    }
}

private async void Cancel_Click(object sender, RoutedEventArgs e)
{
    this.tcs.Cancel();
}

private async Task<IEnumerable<string>> ProcessAsync(CancellationToken ct)
{
    var tasks =
        from e in this.Employees
        where e.Select
        select this.DoSomethingWithThisEmployeee(e.LastName, e.FirstName, ct);

    return await Task.WhenAll(runningTasks);
} 

There's also a problem with your DoSomethingWithThisEmployeee method. It always returns a completed task after having "processed" the data.

I believe what you wanted was something more along these lines:

private Task<string> DoSomethingWithThisEmployeee(string lastName, string firstName, CancellationToken ct)
{
    TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();

    Random rand = new Random();
    System.Threading.Timer t;
    t = new System.Threading.Timer(
        () =>
        {
            try
            {
                t.Dispose();

                tcs.ThrowIfCancellationRequested();

                tcs.TrySetResult(lastName + " - " + firstName);
            }
            catch OperationCanceledException ex
            {
            }
            catch Exception ex
            {
                tcs.TrySetException(ex);
            }
        },
        rand.Next(30000, 60000),
        Timeout.Infinite);

    return tcs.Task;
}

You might also need to use Task.ConfigureAwait(false), but I'll leave that to you.

Read the articles on my async-await curation to learn the basics (and not so basics) about async-await.

Note: Some code might not compile and require minor adjustments.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • Thanks for your detailed response, however, I am constrained to using .NET 4.0. – whoami Dec 30 '14 at 04:02
  • OK! I missed the `C#-4.0` tag. But you did tag the question with `async-await`, which is __C# 5.0__. Can't you use [Microsoft.Bcl.Async](http://www.nuget.org/packages/Microsoft.Bcl.Async/ "Microsoft Async")? – Paulo Morgado Dec 30 '14 at 08:49
  • that was my bad. The organization I am working with have some strange processes that one has to go through when bringing in dependency on a newer library :( I will prefer to do it without going through that painful process, if possible. – whoami Dec 30 '14 at 14:48
  • You can still use `CancellationToken`s and threads instead of tasks. – Paulo Morgado Dec 30 '14 at 16:51