在没有控制对象的情况下在UI线程上运行代码

13

我目前正在编写一个组件,其中一些部分应该在UI线程上运行(解释会很长)。 最简单的方法是将控件传递给它,并在其上使用InvokeRequired/Invoke。 但我认为将控件引用传递给“数据/后台”组件不是一个好的设计,因此我正在寻找一种在无需控件的情况下在UI线程上运行代码的方法。 类似WPF中的Application.Dispatcher.Invoke的东西...

有什么想法, 谢谢 马丁


7
如果有解决您问题的答案,请将其标记为已接受。 - SandRock
5个回答

20

有一种更好的、更抽象的方法可以在WinForms和WPF上实现这个功能:

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

这能够工作是因为WindowsForms安装了一个WindowsFormsSynchronizationContext对象作为当前的同步上下文。WPF也采取类似的做法,安装它自己专用的同步上下文(DispatcherSynchronizationContext)。

.Post对应于control.BeginInvoke.Send对应于control.Invoke


当我尝试调用SynchronizationContext.Current时,它为null... D: - Ling Xing
2
在 UI 线程上访问 SyncrhonizationContext.Current。当你在另一个线程上时再保存它。 - Judah Gabriel Himango

4

首先,在您的表单构造函数中,保留对SynchronizationContext.Current对象的类级引用(实际上是一个WindowsFormsSynchronizationContext)。

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

然后,在你的类中的任何地方,使用这个上下文来向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
        });
    }
}

你需要以下扩展方法来处理lambda表达式:http://codepaste.net/zje4k6

2
你是对的,将控件传递给线程不好。Winforms控件是单线程的,将它们传递给多个线程可能会导致竞态条件或破坏UI。相反,你应该让线程的功能对UI可用,并在UI准备就绪时调用线程。如果你想让后台线程触发UI更改,请公开一个后台事件并从UI订阅它。线程可以随时触发事件,UI可以在能够响应它们时做出响应。
创建这种双向通信线程而不阻塞UI线程需要大量工作。以下是使用BackgroundWorker类的高度简化示例:
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)
        }
    }
}

1
我更喜欢这种方法,因为它可以清晰地将UI代码与后台工作分离。它还允许多个“监听器”响应后台工作状态更新。 - Mike Caron

1

传递一个 System.ComponentModel.ISynchronizeInvoke 怎么样?这样你就可以避免传递一个 Control。


1
将 UI 操作放在要操作的窗体上的一个方法中,并将委托传递给在后台线程上运行的代码,就像 APM 一样。您不必使用 params object p,可以将其强类型化以适应自己的目的。这只是一个简单的通用示例。
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引入范围。以下在语义上是相同的。

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
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接