user405783
user405783

Reputation:

C# async callback still on background thread...help! (preferably without InvokeRequired)

I am writing a very simple asynchronous helper class to go along with my project. The purpose of the class is that it allows a method to be run on a background thread. Here is the code;


    internal class AsyncHelper
    {
        private readonly Stopwatch timer = new Stopwatch();
        internal event DownloadCompleteHandler OnOperationComplete;

        internal void Start(Func func, T arg)
        {
            timer.Start();
            func.BeginInvoke(Done, func);
        }

        private void Done(IAsyncResult cookie)
        {
            timer.Stop();
            var target = (Func) cookie.AsyncState;
            InvokeCompleteEventArgs(target.EndInvoke(cookie));
        }

        private void InvokeCompleteEventArgs(T result)
        {
            var args = new EventArgs(result, null, AsyncMethod.GetEventByClass, timer.Elapsed);
            if (OnOperationComplete != null) OnOperationComplete(null, args);
        }

        #region Nested type: DownloadCompleteHandler

        internal delegate void DownloadCompleteHandler(object sender, EventArgs e);

        #endregion
    }

The result of the task is then returned through the OnOperationComplete event. The problem is that when the event is raised, its still on the background thread. I.e. if I try to run this code (below) I get a cross threading error;

txtOutput.AppendText(e.Result + Environment.NewLine);

Please advise any thoughts.

Upvotes: 7

Views: 3970

Answers (5)

Stephen Cleary
Stephen Cleary

Reputation: 456777

I recommend using the Task class rather than BackgroundWorker, but either would be greatly superior to Control.Invoke or Dispatcher.Invoke.

Example:

internal class AsyncHelper<T>
{ 
  private readonly Stopwatch timer = new Stopwatch(); 
  private readonly TaskScheduler ui;

  // This should be called from a UI thread.
  internal AsyncHelper()
  {
    this.ui = TaskScheduler.FromCurrentSynchronizationContext();
  }

  internal event DownloadCompleteHandler OnOperationComplete; 

  internal Task Start(Func<T> func)
  { 
    timer.Start();
    Task.Factory.StartNew(func).ContinueWith(this.Done, this.ui);
  }

  private void Done(Task<T> task) 
  {
    timer.Stop();
    if (task.Exception != null)
    {
      // handle error condition
    }
    else
    {
      InvokeCompleteEventArgs(task.Result); 
    }
  } 

  private void InvokeCompleteEventArgs(T result) 
  { 
    var args = new EventArgs(result, null, AsyncMethod.GetEventByClass, timer.Elapsed); 
    if (OnOperationComplete != null) OnOperationComplete(null, args); 
  } 

  internal delegate void DownloadCompleteHandler(object sender, EventArgs e); 
} 

This is very similar to a BackgroundWorker, though (except you're adding a timer). You may want to consider just using BackgroundWorker.

Upvotes: 3

Nate
Nate

Reputation: 30646

You need to invoke your event on the UI thread,

WinForms

Form1.BeginInvoke(...);

WPF

Dispatcher.BeginInvoke(...);

Upvotes: 0

dkackman
dkackman

Reputation: 15559

Unless you build your help class to do the context switch internally you will always need to invoke in the event handler because in your code above you are raising the event on the non-ui thread.

To do that your helper needs to know how to get back on the ui thread. You could pass a ISynchronizeInvoke to the helper and then use it when done. Somwthing like:

ISynchronizeInvoke _sync;
internal void Start(Func func, T arg, ISynchronizeInvoke sync)
{
  timer.Start();
  func.BeginInvoke(Done, func);
  _sync = sync;
}

private void InvokeCompleteEventArgs(T result)
{
  var args = new EventArgs(result, null, AsyncMethod.GetEventByClass, timer.Elapsed);
  if (OnOperationComplete != null) 
     _sync.Invoke(OnOperationComplete, new object[]{null, args});
}

The Control class implements ISynchronizeInvoke so you can pass the this pointer from the Form or Control that calls the helper and has the event handler delegate,

Upvotes: 0

Michel Triana
Michel Triana

Reputation: 2526

Use BackgroundWorker class. It essentially does the same you want.

        private BackgroundWorker _worker;

    public Form1()
    {
        InitializeComponent();
        _worker = new BackgroundWorker();
        _worker.DoWork += Worker_DoWork;
        _worker.RunWorkerCompleted += Work_Completed;
    }

    private void Work_Completed(object sender, RunWorkerCompletedEventArgs e)
    {
        txtOutput.Text = e.Result.ToString();
    }

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        e.Result = "Text received from long runing operation";
    }

Upvotes: 5

Ana Betts
Ana Betts

Reputation: 74672

Use the BackgroundWorker class, you're basically reimplementing it here.

Upvotes: 0

Related Questions