浅谈BackgroundWorker的使用

这是弹幕派开发文档系列的第一篇!在开发弹幕派的过程中,通过网上的资料、MSDN学习到了很多WPF和C#的相关知识,在这里一并写出来,希望能够帮助到C#特别是WPF开发者。
弹幕派是我们开发的一个桌面弹幕小程序,说它小,但是它的开发周期可不短,在开发过程中学到了很多东西,今天我要说的便是第一个,如何运用后台进程连接网络。

浅谈BackgroundWorker在WPF中的使用

弹幕派在刚开始UI的渲染(即弹幕的产生和刷新)以及弹幕内容的获取都是在一个进程中完成的,这样导致一个问题就在于每当从网络获取数据时就会出现明显卡顿,如果网络失去连接就会导致程序假死无法继续进行。很明显这样是不行的,因此必须要引入多线程,通过后台线程获取数据,再将数据更新到UI中。

在WPF中,为了保证线程安全,Windows只允许创建UI元素的线程访问这些元素。如果在其他线程中尝试修改UI元素的属性,就会触发STA错误,导致程序崩溃。这样做是为了保证内容渲染的一致性。但是也会导致一个问题——我们无法通过后台线程直接修改UI元素的属性。WPF通过Dispatcher机制解决了这一问题。WPF为UI渲染设置了一个Dispatcher,这个Dispatcher我们可以理解为调度员,它与UI渲染相关的事件排成一个队列,按优先级对其队列中的元素进行排序,并且按序执行,这样可以保证UI在渲染时只执行一个任务,保证UI内容的一致性。如果我们的后台线程需要对界面元素的属性进行修改,可以请求UI线程代替它完成这一操作。那么如何请求UI线程帮忙呢?通过向Dispatcher注册工作项,将想要执行的任务加入队列,这个任务会在某个时间由Dispatcher完成,后台进程无需插手UI渲染。

Dispatcher队列

Dispatcher类提供两种调用方法,一种是Invoke同步调用,调用方必须等待UI进程完成这一任务才会返回并继续下面的操作;另一种是BeginInvoke异步调用,调用方在调用后会立即返回。
在弹幕派原有的代码中对这一部分有所使用。原本弹幕派刷新弹幕是通过每秒钟定时修改所有弹幕TextBlock的Margin属性的值达到移动弹幕的效果,那么在计时器TimerElapse事件触发的函数中,如果直接修改这些Margin会触发STA错误。因此需要通过BeginInvoke来执行这一操作。

1
2
3
4
5
private delegate void DispatcherDelegateTimer(); // 声明委托

private void OnTimedEvent(object sender, EventArgs e) {
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new DispatcherDelegateTimer(UpdateUI)); //通过BeginInvoke注册
}

使用后台进程有三种方式,第一种是Task,第二种是Thread,第三种就是我们今天要介绍的BackgroundWorker了。这三种方法各有千秋,但是BackgroundWorker更适合用于实现后台连接网络下载,因此在弹幕派的弹幕获取、自动更新等地方都主要使用了BackgroundWorker

那么如何用BackgroundWorker实现后台连接网络获取数据呢?
首先我们需要引入命名空间

1
using System.ComponentModel;

之后我们需要添加BackgroundWorker组件,这一组件可以从Xaml界面添加——从工具箱中的“组件”选项卡中,添加BackgroundWorker组件;也可以在代码中声明:

1
private BackgroundWorker fetchBW = new BackgroundWorker();

之后在初始化过程中设置BackgroundWorker的属性,可以在构造函数中,也可以在Loaded函数中。

1
2
3
4
5
fetchBW.WorkerReportsProgress = true; //是否报告工作进度
fetchBW.WorkerSupportsCancellation = true; //是否允许异步取消工作
fetchBW.DoWork += new DoWorkEventHandler(FetchBW_DoWork); //这里声明要做的工作
fetchBW.ProgressChanged += new ProgressChangedEventHandler(FetchBW_ProgressChanged); //当工作进度改变时更新界面
fetchBW.RunWorkerCompleted += new RunWorkerCompletedEventHandler(FetchBW_RunWorkerCompleted); //当工作完成时处理工作结果

首先要设置是否报告工作进度,如果WorkerReportsProgress为true,则可以在ProgressChanged事件的函数中处理进度条等信息。当然Progress的数值要自行在DoWork函数中利用ReportProgress设置数值的变化(例如获取已经下载的进度并更新进度条)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
for (int i = 1; i <= 10; i++)
{
if (worker.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
// Perform a time consuming operation and report progress.
System.Threading.Thread.Sleep(500);
worker.ReportProgress(i * 10);
}
}
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
resultLabel.Text = (e.ProgressPercentage.ToString() + "%");
}

如果允许异步取消(WorkerSupportsCancellation = false),则通过CancelAsync可以取消工作。此时CancellationPending = true
之后再绑定DoWorkProgressChangedRunWorkerCompleted事件。DoWork里写明主要功能,同时需要回报进度和处理取消事件。ProgressChanged里根据进度处理事件(修改进度条等),RunWorkerCompleted事件处理DoWork的结果。

那么如何在RunWorkerCompleted中获取DoWork的结果呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void FetchBW_DoWork(Object sender, DoWorkEventArgs e) {
BackgroundWorker backgroundWorker = sender as BackgroundWorker; //sender即源BackgroundWorker

//......

// 将获得的结果进行封装,然后将解析结果保存至e.Result中供RunWorkerCompleted使用
fetchedData result = new fetchedData(num, contentList);
e.Result = result;
backgroundWorker.ReportProgress(100); // 当Dowork完成时直接将进度设为100%,触发RunWorkerCompleted事件
}

private void FetchBW_ProgressChanged(object sender, ProgressChangedEventArgs e) {
return;
}

private void FetchBW_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if (e.Cancelled == false && e.Error == null) {
fetchedData result = e.Result as fetchedData;
danmuStorage.AddRange(result.contentList);
result.contentList.Clear();
}
else {
Debug.WriteLine("获取时出现错误");
}
bwTimer.Stop();
}

将结果保存至DoWorke.Result中,之后可以在RunWorkerCompletede.Result中获取到结果。处理结果时要处理Cancelled(取消事件)和Error(错误事件)。
这里要注意的是,对于网络访问等操作来说,很有可能会出现网络连接中断导致超时,因此这个时候需要我们设置一个定时器,在开始处理事件前启动定时器,然后在定时器超时时调用CancelAsync即可。

BackgroundWorker不仅可以在WPF中调用,在WinForm中也可以。BackgroundWorker最适合的场景便是后台下载,通过DoWorkReportProgressRunWorkerCompleted三者分开,可以明确地划分执行工作、更新界面、处理结果三个部分,与定时器Timer和按钮Button结合使用还可以保证程序不会由于网络连接中断等原因一直卡住。

参考资料

wpf 多线程

线程处理模型

如何:使用后台辅助线程

如何:在后台下载文件

如何:在后台运行操作

BackgroundWorker

Dispatcher类

委托 delegate

# WPF

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×