看过很多介绍文章介绍如何在后台线程更新主界面的,多数都是使用Control.Invoke, Control.BeginInvoke。这些都是很好的解决方案,不过有两个问题:
1. 必须的引用System.Windows.Forms,然后 using System.Windows.Forms
2. 代码结构比较零乱。(实际上这个也是#1 导致的)
M$提供了另外一个比较优雅的解决方案,就是 System.Threading. SynchronizationContext。 可以看出,它不在 namesapce System.Windows.Forms 里面,因此我们可以理直气壮地用在BusinessLaryer, Controler,甚至 module 里面。
而且它使用非常方便,只需要关注下面两个方法即可:
1. Send:发送界面更新请求至主线程,阻塞当前线程直至返回。
2. Post:发送界面更新请求至主线程,不阻塞当前线程。
实际上,他们都是同一个方法,只是 send 是同步,post 是异步
我们可以看一下下面这个例子:
点击btnThread 的时候,首先 disable btnThread,然后启动线程;接着线程主动报告进度;最后线程结束,重新 enable btnThread.
private void btnThread_Click(object sender, EventArgs e)
{
txtThread.Text = @"Running background thread...";
btnThread.Enabled = false;
Thread t = new Thread(() =>
{
UiState textState = new UiState
{
Control = txtThread,
PropertyName = "Text",
};
for (int i = 0; i <= 10; i++)
{
Thread.Sleep(new TimeSpan(0, 0, 1));
textState.PropertyValue = string.Format("Progress: {0}", i * 10);
_context.Post(UpdateControl, textState);
}
Thread.Sleep(new TimeSpan(0, 0, 1));
UiState enableState = new UiState
{
Control = btnThread,
PropertyName = "Enabled",
PropertyValue = true,
};
_context.Post(UpdateControl, enableState);
}
);
t.Start();
}
private static void UpdateControl(object uiObject)
{
UiState state = uiObject as UiState;
if (state == null)
{
throw new InvalidCastException("Cannot convert object to UiState");
}
Type controlType = state.Control.GetType();
PropertyInfo property = controlType.GetProperty(state.PropertyName);
property.SetValue(state.Control, state.PropertyValue, null);
}
这是一个很简单的例子,也许看过的人都会说:根本就没有摆脱Control 和 From 啊!
诚然如此,为了摆脱Control,我们还需要封装一下,看下面的BackgroundWorker 类:
class BackgroundWorker
{
public EventHandler<EventArgs> WorkerStarted;
public EventHandler<ProgressEventArgs> ReportProgress;
public EventHandler<EventArgs> WorkerCompleted;
public SynchronizationContext Context { get; set; }
public void Start()
{
Thread t = new Thread(() =>
{
Context.Post(SetStartState, null);
for (int i = 0; i <= 10; i++)
{
Thread.Sleep(new TimeSpan(0, 0, 1));
Context.Post(UpdateProgress, i * 10);
}
Thread.Sleep(new TimeSpan(0, 0, 1));
Context.Post(SetCompletedState, null);
}
);
t.Start();
}
private void SetStartState(object state)
{
if (WorkerStarted != null)
{
WorkerStarted(this, new EventArgs());
}
}
private void SetCompletedState(object state)
{
if (WorkerCompleted != null)
{
WorkerCompleted(this, new EventArgs());
}
}
private void UpdateProgress(object state)
{
if (ReportProgress != null)
{
ReportProgress(this, new ProgressEventArgs {Progress = Convert.ToInt32(state)});
}
}
}
有两个特别的地方:
1. 构造的时候,需要提供SynchronizationContext 实例
2. 它的三个事件(WorkerStarted,ReportProgress, WorkerCompleted),都是在 post 方法里面触发的。这样做就可以确保事件的订阅方法脱离后台线程,进入主线程。
使用的方法如下:
protected override void OnLoad(EventArgs e)
{
_context = SynchronizationContext.Current;
}
private void btnWrapper_Click(object sender, EventArgs e)
{
btnWrapper.Enabled = false;
BackgroundWorker worker = new BackgroundWorker {Context = SynchronizationContext.Current};
worker.WorkerStarted += (o, args) => { txtWrapper.Text = "Running background thread..."; };
worker.ReportProgress += (o, args) => { txtWrapper.Text = string.Format("Progress: {0}", args.Progress); };
worker.WorkerCompleted += (o, args) =>
{
txtWrapper.Text = "Background thread completed";
btnWrapper.Enabled = true;
};
worker.Start();
}
在BackgroundWroker 方法里面,再也见不到任何与 Control, Form 有关的东西
在MainForm里面,使用的方法也非常简单。
效果图如下:
可以在这个地方下载全部代码:
https://github.com/IGabriel/CSharpForm/tree/SynchronizationContext-Sample