views:

712

answers:

4

I have a lengthy user-interface operation on my form which is triggered whenever an event is fired. Rather than have the UI block while the operation takes place, I'd like to perform the operation in another thread, and abort that thread and start again if the event fires again.

However, to safely alter controls on my form, I need to use the form's Invoke or BeginInvoke methods. If I do that, then I could put all my UI operations in one function like this:

private delegate void DoUIStuffDelegate(Thing1 arg1, Thing2 arg2);
private void doUIStuff(Thing1 arg1, Thing2 arg2)
{
    control1.Visible = false;
    this.Controls.Add(arg1.ToButton());
    ...
    control100.Text = arg2.ToString();
}

...

private void backgroundThread()
{
    Thing1 arg1 = new Thing1();
    Thing2 arg2 = new Thing2();

    this.Invoke(new DoUIStuffDelegate(doUIStuff), arg1, arg2);
}

Thread uiStuffThread = null;

public void OnEventFired()
{
    if (uiStuffThread != null)
        uiStuffThread.Abort();

    uiStuffThread = new Thread(backgroundThread);
    uiStuffThread.Start();
}

but if I do that, then I lose the benefit of working in a separate thread. Alternatively, I could put them each in their own function like this:

private delegate void DoUIStuffLine1Delegate();
private delegate void DoUIStuffLine2Delegate(Thing1 arg1);
...

private delegate void DoUIStuffLine100Delegate(Thing2 arg2);

private void doUIStuffLine1()
{
    control1.Visible = false;
}

private void doUIStuffLine2()
{
    this.Controls.Add(arg1.ToButton());
}

...

private void doUIStuffLine100(Thing2 arg2)
{
    control100.Text = arg2.ToString();
}

...

private void backgroundThread()
{
    Thing1 arg1 = new Thing1();
    Thing2 arg2 = new Thing2();

    this.Invoke(new DoUIStuffLine1Delegate(doUIStuffLine1));
    this.Invoke(new DoUIStuffLine2Delegate(doUIStuffLine2), arg1);
    ...
    this.Invoke(new DoUIStuffLine100Delegate(doUIStuffLine100), arg2);
}

Thread uiStuffThread = null;

public void OnEventFired()
{
    if (uiStuffThread != null)
        uiStuffThread.Abort();

    uiStuffThread = new Thread(backgroundThread);
    uiStuffThread.Start();
}

but that's a horrible, unmaintainable mess. Is there a way to create a thread that can modify the user interface, and that I can abort? So that I can just do something like this:

private void doUIStuff()
{
    Thing1 arg1 = new Thing1();
    Thing2 arg2 = new Thing2();

    control1.Visible = false;
    this.Controls.Add(arg1.ToButton());
    ...
    control100.Text = arg2.ToString();
}

Thread uiStuffThread = null;

public void OnEventFired()
{
    if (uiStuffThread != null)
        uiStuffThread.Abort();

    uiStuffThread = this.GetNiceThread(doUIStuff);
    uiStuffThread.Start();
}

without having to disable cross-thread checks on my form? Ideally I'd like to be able to set some attribute on the thread or the method which individually wrapped all of the operations in delegates that then got invoked on the form's thread.

+2  A: 

First - don't disable the cross-thread checks... forms have thread affinity...

Second - try to avoid aborting threads; it isn't nice - you should prefer clean shutdown (such as the cancellation that BackgroundWorker supports)

One option might be to write a wrapper method that:

  • accepts a typed delegate (so you can call it more simply)
  • does the necessary checking (throwing an exception to terminate and unroll)

For example:

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        try {
          Action<Action> update = thingToDo =>
          {
              if (worker.CancellationPending) throw new SomeException();
              this.Invoke(thingToDo);
          };

          //...
          string tmp = "abc"; // long running
          update(() => this.Text = tmp);

          tmp = "def"; // long running
          update(() => textbox1.Text = tmp);
        } catch (SomeException) {
          e.Cancel = true;
        }
    }

This is still a little messy, but arguably cleaner than aborting threads left and right...

Marc Gravell
A: 

You can avoid the use of Invoke using the object SynchronizationContext that was introduced in framework 2. Ok , it's the same thing, you substitute one thing for another, but in fact, it is more efficient and robust. Anyway, cross-threads need checks, because you can never access a control created in another thread without this.

Read about this in: http://www.codeproject.com/KB/cpp/SyncContextTutorial.aspx http://codingly.com/2008/08/04/invokerequired-invoke-synchronizationcontext/

Some code to expose my idea:

   private Thread workerThread;

        private AsyncOperation operation;

        public event EventHandler SomethingHappened;

        public MySynchronizedClass()
        {
            operation = AsyncOperationManager.CreateOperation(null);

            workerThread = new Thread(new ThreadStart(DoWork));

            workerThread.Start();
        }

        private void DoWork()
        {
            operation.Post(new SendOrPostCallback(delegate(object state)
            {
                EventHandler handler = SomethingHappened;

                if(handler != null)
                {
                    handler(this, EventArgs.Empty);
                }
            }), null);

            operation.OperationCompleted();
        }
netadictos
so to cancel the thread, could I call operation.Post() again with a delegate function that threw an exception? Or would that wait until the first operation.Post() had completed?
Simon
...or will PostOperationCompleted abort the operation?
Simon
It does not abort, it only finishes the async operation. I think you can even omit it.Another way is not using the AsyncOperationManager, but it's said to be better: SynchronizationContext syncContext;this.syncContext = SynchronizationContext.Current; this.syncContext.Post(delegate{},null)
netadictos
I don't want to wait for the operation to finish - I want to kill it as soon as I receive another event.
Simon
I can't see it clearly. bw.CancelAsync() is the only option I see. Another option is to use AOP. Look at this project:http://secure.codeproject.com/KB/cs/AOPInvokeRequired.aspx
netadictos
AOP looks very interesting - but still looks like it applies on a per-function basis. I'm looking for something that I can apply to each line of a function. I have a horrible feeling the only way to do that is to parse the MSIL and use reflection.emit to make a new method.
Simon
A: 

I answer you here because comments are too short. I personally use BackgroundWorker and when the method finishes, the thread is liberated. Basically what you do is:

bw = new BackgroundWorkerExtended();
bw.DoWork += (DoWorkEventHandler)work;
bw.WorkerSupportsCancellation = true;
//bw.WorkerReportsProgress = true;
bw.RunWorkerCompleted += (RunWorkerCompletedEventHandler)workCompleted;
//bw.ProgressChanged+=new ProgressChangedEventHandler(bw_ProgressChanged);
bw.RunWorkerAsync();

Another thing if you use Post, it will be asynchronous, if you use, Send it would be synchronous.

To cancel: bw.CancelAsync();

netadictos
That's nice, but I still need to call Invoke() if I update any of the record's controls. I'm really looking for something that will wrap all the calls which need invoke - perhaps I need to look at dynamic methods.
Simon
A: 

Further to the BackgroundWorker suggestion and the associated comment, BackgroundWorker does marshal the OnProgress callback back onto the UI thread, so you CAN update stuff from there without needing to do Invoke().

I can't really tell whether that's helpful or not, and anyway it's a technique you could use pretty easily without background worker.

I suspect that if the background task needs to know so much about the form that it is doing a bazillion invokes, then you might have a separation-of-concerns issue that would be worth thinking about anyway.

Will Dean
the OnProgress callback isn't useful to me - I'd still have to write 100 different functions, one for each of the 100 lines of code.
Simon