简体   繁体   中英

Can a long-running method and a “Working…” dialog be run together using the Task Parallel Library to allow long task to write to a BindingList?

I have a WPF (C# and .NET 4) application that has a long running task in it that blocked the UI giving the impression that it has hung. I decided to put this long-running task onto a separate thread by using a BackgroundWorker thread and showed a BusyIndicator in a separate popup window (named WorkingDialog below). This worked fine until the long running task writes to a BindingList (which is bound to a grid on the UI), and I got the following exception:

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

This is (a very trimmed down version of) the business layer code...

public class CustomMessage
{
    public DateTime TimeStamp { get; private set; }
    public string Message { get; private set; }

    public CustomMessage(string message)
    {
        Message = message;
        TimeStamp = DateTime.Now;
    }
}

public class MyRandomBusinessClass
{
    public BindingList<CustomMessage> LoggingList { get; private set; }

    public MyRandomBusinessClass()
    {
        LoggingList = new BindingList<CustomMessage>();
    }

    public void SomeLongRunningTask()
    {
        System.Threading.Thread.Sleep(5000);

        LoggingList.Add(new CustomMessage("Completed long task!"));
    }
}

... the UI...

<Window x:Class="WpfApplication4.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">

    <StackPanel>
        <DataGrid x:Name="logGrid"
                  AutoGenerateColumns="True"
                  ItemsSource="{Binding Path=LoggingList}" />

        <Button Click="ProcessLongTask_Click"
                Content="Do Long Task" />
    </StackPanel>

</Window>

... The UI code...

using System.ComponentModel;
using System.Windows;

namespace WpfApplication4
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MyRandomBusinessClass RandomBusinessClass { get; set; }
        public MainWindow()
        {
            InitializeComponent();

            RandomBusinessClass = new MyRandomBusinessClass();

            logGrid.DataContext = RandomBusinessClass;
        }

        private void ProcessLongTask_Click(object sender, RoutedEventArgs e)
        {
            ProcessTask1();
        }

        private void ProcessTask1()
        {
            WorkingDialog workingDialog = new WorkingDialog();
            workingDialog.Owner = this;

            BackgroundWorker worker = new BackgroundWorker();

            worker.DoWork += delegate(object s, DoWorkEventArgs args)
            {
                RandomBusinessClass.SomeLongRunningTask();
            };

            worker.RunWorkerCompleted += delegate(object s, RunWorkerCompletedEventArgs args)
            {
                workingDialog.Close();
            };

            worker.RunWorkerAsync();
            workingDialog.ShowDialog();
        }
    }
}

... the worker dialog...

<Window xmlns:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
        x:Class="WpfApplication4.WorkingDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        mc:Ignorable="d"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="Working Dialog"
        WindowStartupLocation="CenterOwner"
        Height="116"
        Width="199">

    <Grid>

        <extToolkit:BusyIndicator x:Name="busyIndicator"
                                  IsBusy="True"
                                  BusyContent="Working ..."
                                  HorizontalAlignment="Left"
                                  VerticalAlignment="Top" />

    </Grid>
</Window>

I've found several threads in this forum pointing to the work around by Bea Stollnitz ), but I am wondering if there is a way of executing the long running task, showing the WorkingDialog and then closing the dialog once the task is complete using the Task Parallel Library? Can the Task Parallel Library achieve the desired result without having to modify the business layer code?

Edit - Workaround 1

Thanks to comments from Ben below, there is a workaround if your long-running task is in the same assembly as the UI or uses the System.Windows namespace. Using the current application dispatcher I can add the message to my list without the exception:

public class MyRandomBusinessClass
{
    public BindingList<CustomMessage> LoggingList { get; private set; }

    public MyRandomBusinessClass()
    {
        LoggingList = new BindingList<CustomMessage>();
    }

    public void SomeLongRunningTask()
    {
        System.Threading.Thread.Sleep(5000);

        Dispatcher myDispatcher = Application.Current.Dispatcher;
        myDispatcher.BeginInvoke((Action)(() => LoggingList.Add(new CustomMessage("Completed long task!"))));
    }
}

I'm still curious whether this would be possible using the Task Parallel Library or am I barking up the wrong tree?. In my actual application the long-running class is in a separate class library.

Edit 2

I've accepted Ben's solution below as it solves my initial problem of getting the long-running task to run without blocking the UI or throwing the exception. I appreciate that the original question was about the Task Parallel Library, so forgive me if you are reading this thinking that it has a TPL solution.

You need to use a dispatcher to add a new element to your logginglist.

public void SomeLongRunningTask()
{
    System.Threading.Thread.Sleep(5000);

    Application.Current.Dispatcher.BeginInvoke((Action)(() => LoggingList.Add(new CustomMessage("Completed long task!"))));
}

Edit :

public class MyRandomBusinessClass
{
  public BindingList<CustomMessage> LoggingList { get; private set; }
  public Action<MyRandomBusinessClass, CustomMessage> CallBackAction { get; private set; }

  public MyRandomBusinessClass(Action<MyRandomBusinessClass, CustomMessage> cba) 
  {
      LoggingList = new BindingList<CustomMessage>();
      this.CallBackAction = cba;
  }

  public void SomeLongRunningTask()
  {
    System.Threading.Thread.Sleep(5000);

    if (cba != null)
      CallBackAction(this, new CustomMessage("Completed long task!"));
  }
}

Then where you create your business class:

MyRandomBusinessClass businessClass = new MyRandomBusinessClass((sender, msg) =>
   Application.Current.Dispatcher.BeginInvoke((Action)(() => 
      sender.LoggingList.Add(msg))));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM