Run code on UI thread without control object prese

2019-01-17 13:22发布

问题:

I currently trying to write a component where some parts of it should run on the UI thread (explanation would be to long). So the easiest way would be to pass a control to it, and use InvokeRequired/Invoke on it. But I don't think that it is a good design to pass a control reference to a "data/background"-component, so I'm searching for a way to run code on the UI thread without the need of having a control available. Something like Application.Dispatcher.Invoke in WPF...

any ideas, thx Martin

回答1:

There's a better, more abstract way to do this that works on both WinForms and WPF:

System.Threading.SynchronizationContext.Current.Post(theMethod, state);

This works because WindowsForms installs a WindowsFormsSynchronizationContext object as the current sync context. WPF does something similar, installing it's own specialized synchronization context (DispatcherSynchronizationContext).

.Post corresponds to control.BeginInvoke, and .Send corresponds to control.Invoke.



回答2:

You are right, it is not good to pass controls to threads. Winforms controls are single-threaded, passing them to multiple threads can cause race conditions or break your UI. Instead, you should make your thread's features available to the UI and let it call the thread when the UI is good and ready. If you want to have background threads trigger UI changes, expose a background event and subscribe to it from the UI. The thread can fire off events whenever it wants and the UI can respond to them when it is able to.

Creating this bidirectional communication between threads that does not block the UI thread is a lot of work. Here is a highly abbreviated example using a BackgroundWorker class:

public class MyBackgroundThread : BackgroundWorker
{
    public event EventHandler<ClassToPassToUI> IWantTheUIToDoSomething;

    public MyStatus TheUIWantsToKnowThis { get { whatever... } }

    public void TheUIWantsMeToDoSomething()
    {
        // Do something...
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        // This is called when the thread is started
        while (!CancellationPending)
        {
            // The UI will set IWantTheUIToDoSomething when it is ready to do things.
            if ((IWantTheUIToDoSomething != null) && IHaveUIData())
                IWantTheUIToDoSomething( this, new ClassToPassToUI(uiData) );
        }
    }
}


public partial class MyUIClass : Form
{
    MyBackgroundThread backgroundThread;

    delegate void ChangeUICallback(object sender, ClassToPassToUI uiData);

    ...

    public MyUIClass
    {
        backgroundThread = new MyBackgroundThread();

        // Do this when you're ready for requests from background threads:
        backgroundThread.IWantTheUIToDoSomething += new EventHandler<ClassToPassToUI>(SomeoneWantsToChangeTheUI);

        // This will run MyBackgroundThread.OnDoWork in a background thread:
        backgroundThread.RunWorkerAsync();
    }


    private void UserClickedAButtonOrSomething(object sender, EventArgs e)
    {
        // Really this should be done in the background thread,
        // it is here as an example of calling a background task from the UI.
        if (backgroundThread.TheUIWantsToKnowThis == MyStatus.ThreadIsInAStateToHandleUserRequests)
            backgroundThread.TheUIWantsMeToDoSomething();

        // The UI can change the UI as well, this will not need marshalling.
        SomeoneWantsToChangeTheUI( this, new ClassToPassToUI(localData) );
    }

    void SomeoneWantsToChangeTheUI(object sender, ClassToPassToUI uiData)
    {
        if (InvokeRequired)
        {
            // A background thread wants to change the UI.
            if (iAmInAStateWhereTheUICanBeChanged)
            {
                var callback = new ChangeUICallback(SomeoneWantsToChangeTheUI);
                Invoke(callback, new object[] { sender, uiData });
            }
        }
        else
        {
            // This is on the UI thread, either because it was called from the UI or was marshalled.
            ChangeTheUI(uiData)
        }
    }
}


回答3:

First, in your form constructor, keep a class-scoped reference to the SynchronizationContext.Current object (which is in fact a WindowsFormsSynchronizationContext).

public partial class MyForm : Form {
    private SynchronizationContext syncContext;
    public MyForm() {
        this.syncContext = SynchronizationContext.Current;
    }
}

Then, anywhere within your class, use this context to send messages to the UI:

public partial class MyForm : Form {
    public void DoStuff() {
        ThreadPool.QueueUserWorkItem(_ => {
            // worker thread starts
            // invoke UI from here
            this.syncContext.Send(() =>
                this.myButton.Text = "Updated from worker thread");
            // continue background work
            this.syncContext.Send(() => {
                this.myText1.Text = "Updated from worker thread";
                this.myText2.Text = "Updated from worker thread";
            });
            // continue background work
        });
    }
}

You will need the following extension methods to work with lambda expressions: http://codepaste.net/zje4k6



回答4:

Put the UI manipulation in a method on the form to be manipulated and pass a delegate to the code that runs on the background thread, à la APM. You don't have to use params object p, you can strongly type it to suit your own purposes. This is just a simple generic sample.

delegate UiSafeCall(delegate d, params object p);
void SomeUiSafeCall(delegate d, params object p)
{
  if (InvokeRequired) 
    BeginInvoke(d,p);        
  else
  {
    //do stuff to UI
  }
}

This approach is predicated on the fact that a delegate refers to a method on a particular instance; by making the implementation a method of the form, you bring the form into scope as this. The following is semantically identical.

delegate UiSafeCall(delegate d, params object p);
void SomeUiSafeCall(delegate d, params object p)
{
  if (this.InvokeRequired) 
    this.BeginInvoke(d,p);        
  else
  {
    //do stuff to UI
  }
}


回答5:

What about passing a System.ComponentModel.ISynchronizeInvoke? That way you can avoid passing a Control.