views:

140

answers:

6

Hi there,

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.

A: 

You need to invoke your event on the UI thread,

WinForms

Form1.BeginInvoke(...);

WPF

Dispatcher.BeginInvoke(...);

Nate Bross
Are you suggesting this?;<code>OnOperationComplete.BeginInvoke</code>
Jon Preece
`ISynchronizeInvoke` is an outdated interface and should not be used.
Stephen Cleary
@Jon, no, he's suggesting you call the BeginInvoke or Invoke method found on a visual control. That way you'll marshal the call over to the GUI thread, and that will allow you to manipulate the GUI state. `Delegate.BeginInvoke` and `Control.BeginInvoke` is not the same thing.
Lasse V. Karlsen
A: 

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

Paul Betts
+3  A: 

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";
    }
Michel Triana
You can also report progress setting:_worker.WorkerReportsProgress = true;//AND_worker.ProgressChanged += Progress_Changed;
Michel Triana
I kinda forgot... to actually run the worker you have to call the _worker.RunWorkerAsync() method.
Michel Triana
@Michel Triana: There is a little “edit” link under your answer. :)
Timwi
i went for the BackgroundWorker in the end, but tucked it all away and put in custom events so that nobody will ever know. Assuming they dont look at the source of course. (The fact that the project is open source is a slight flaw, but oh well)
Jon Preece
A: 

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,

dkackman
`ISynchronizeInvoke` is an outdated interface and should not be used.
Stephen Cleary
@Stephen - reference?
dkackman
+2  A: 

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.

Stephen Cleary
Did you read it? My "three pages" include a complete solution with user interface, support for cancellation, proper error marshalling, etc. None of which your solution includes.
Stephen Cleary
I have updated my answer with a simple solution.
Stephen Cleary
I like this but not sure how to implement, could you give usage example?
Jon Preece
I've updated the example to be similar to the code in your question, showing how it would be used in that situation.
Stephen Cleary
hmm it still throws error;Cross-thread operation not valid: Control 'txtOutput' accessed from a thread other than the thread it was created on.
Jon Preece
Did you construct `AsyncHelper` on the UI thread, as the comment indicated?
Stephen Cleary
sorry i must have missed that. the helper class originates from another class which is instantiated from the UI thread. does that count.
Jon Preece
Yes, as long as the UI thread has already created (and shown) a control.
Stephen Cleary
A: 

Why do you require timer? i think , you should only use Asynchronous pattern described here.

http://msdn.microsoft.com/en-us/library/ms228969.aspx

saurabh
timer was just so i could see how long the task was taking
Jon Preece