WPF的UI线程

在WPF中,UI线程是比较特殊的线程,如何实现非UI线程对UI元素的修改是一个常见问题。

本文先介绍UI线程的基本知识,然后以实例方式介绍BackgroundWorker的使用及其原理。

基本原则

  • WPF元素是线程关联的。创建WPF元素的线程拥有该元素,其它线程不能直接与这些WPF元素进行交互
  • 具有线程关联性的WPF对象有继承自DispatcherObject
    • CheckAccess():检查DispatcherObject的派生类对象是否在正确的线程上被修改,返回truefalse
    • VerifyAccess():同上,不是时抛出异常
  • 在新线程中第一次实例化DispatcherObject的派生类时,会创建Dispatcher,在多个线程中创建时会创建多个Dispatcher

用法

错误示范

1
2
3
4
5
6
7
private void btnTest1_Click(object sender, RoutedEventArgs e) {
Thread thread = new(() => {
Thread.Sleep(TimeSpan.FromSeconds(2));
tb1.Text = "Here is some new text.";
});
thread.Start();
}

正确做法

1
2
3
4
5
6
7
8
9
private void btnTest1_Click(object sender, RoutedEventArgs e) {
Thread thread = new(() => {
Thread.Sleep(TimeSpan.FromSeconds(2));
Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => {
tb1.Text = "Here is some new text.";
});
});
thread.Start();
}

InvokeBeginInvoke的区别

  • BeginInvoke()是异步执行,这里会先显示World,再显示Hello,最后修改tb1的值
1
2
3
4
5
6
7
8
9
10
11
private void btnTest1_Click(object sender, RoutedEventArgs e) {
Thread thread = new(() => {
Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => {
Thread.Sleep(TimeSpan.FromSeconds(2));
MessageBox.Show("Hello");
tb1.Text = "Here is some new text.";
});
MessageBox.Show("World");
});
thread.Start();
}
  • Invoke()是同步执行,这里先显示Hello,再修改tb1的值,然后休眠两秒,最后显示World
1
2
3
4
5
6
7
8
9
10
11
12
private void btnTest1_Click(object sender, RoutedEventArgs e) {
Thread thread = new(() => {
Dispatcher.Invoke(DispatcherPriority.Normal, () => {
MessageBox.Show("Hello");
Thread.Sleep(TimeSpan.FromSeconds(2));
tb1.Text = "Here is some new text.";
});
Thread.Sleep(TimeSpan.FromSeconds(2));
MessageBox.Show("World");
});
thread.Start();
}

BackgroundWorker的妙用

在开发WPF项目时,通常涉及后台处理逻辑,如果后台处理逻辑比较耗时,同步代码将导致界面卡顿,这时优选多线程方式。

采用多线程势必面临一个问题:如何在后台线程中更新UI元素。这时,Dispatcher的作用就发挥出来了。

想象一下实际场景,后台处理逻辑执行时,可能会有以下需求:①需要定期更新UI上的进度状态;②后台逻辑执行完成之后需要把处理结果呈现在UI上;③如果后台逻辑耗时较长,最好支持中途取消。

看采用普通多线程方式实现的一个示例,再看官方BackgroundWorker提供的解决方案,后者对前者进行封装而已。

普通做法

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System.Windows;
using System.Windows.Threading;

namespace TestDispacher {
public partial class MainWindow : Window {
// 线程取消相关变量
private CancellationTokenSource cts;
private CancellationToken token;

public MainWindow() {
InitializeComponent();
cts = new CancellationTokenSource();
token = cts.Token;
}

private void btn1_Click(object sender, RoutedEventArgs e) {
int n;
if (!int.TryParse(tb1.Text, out n))
return;

// 启动后台线程
Task task = Task.Run(() => DoWork(n), token);
}

private void btn2_Click(object sender, RoutedEventArgs e) {
// 取消线程
cts.Cancel();
}

private void DoWork(int n) {
List<string> texts = new();
bool cancelled = false;

for (int i = 0; i < n; i++) {
// 中止线程
if (token.IsCancellationRequested) {
cancelled = true;
break;
}

Thread.Sleep(100);
int progress = (int)((i + 1) / (double)n * 100.0);
// ProgressChanged逻辑
Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => {
pb1.Value = progress;
btn1.Content = $"Progress: {progress}%";
});
texts.Add($"This is {i + 1}!");
}
// WorkerReportsProgress逻辑
Dispatcher.Invoke(() => {
if (cancelled)
btn1.Content = "Cancelled!";
else {
foreach (var prime in texts)
lb1.Items.Add(prime);
btn1.Content = "Finished!";
}
});
}
}
}
  • DoWork()是后台逻辑,执行中和执行结束时需要更新UI,通过Dispatcher来达到目的,保证线程安全。
  • 采用非常经典的CancellationTokenSource中止后台线程。

BackgroundWorker界面文件

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
27
28
29
30
<Window x:Class="TestDispacher.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestDispacher"
xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
mc:Ignorable="d"
Title="MainWindow" Height="400" Width="500">

<Window.Resources>
<cm:BackgroundWorker x:Key="backgroundWorker"
DoWork="BackgroundWorker_DoWork"
WorkerReportsProgress="True"
WorkerSupportsCancellation="True"
ProgressChanged="BackgroundWorker_ProgressChanged"
RunWorkerCompleted="BackgroundWorker_RunWorkerCompleted"/>
</Window.Resources>

<DockPanel>
<TextBox x:Name="tb1" DockPanel.Dock="Top"/>
<Button x:Name="btn1" DockPanel.Dock="Top"
Click="btn1_Click" Content="启动"/>
<Button x:Name="btn2" DockPanel.Dock="Top"
Click="btn2_Click" Content="取消"/>
<ProgressBar x:Name="pb1" DockPanel.Dock="Bottom"
Height="20" Minimum="0" Maximum="100"/>
<ListBox x:Name="lb1"/>
</DockPanel>
</Window>
  • 在xaml文件中声明BackgroundWorker,十分方便。

BackgroundWorker后台逻辑

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using System.ComponentModel;
using System.Windows;

namespace TestDispacher {
public partial class MainWindow : Window {
private BackgroundWorker backgroundWorker;

public MainWindow() {
InitializeComponent();
backgroundWorker = (BackgroundWorker)this.FindResource("backgroundWorker");
}

private void btn1_Click(object sender, RoutedEventArgs e) {
int n;
if (!int.TryParse(tb1.Text, out n))
return;
backgroundWorker.RunWorkerAsync(n);
}

private void btn2_Click(object sender, RoutedEventArgs e) {
backgroundWorker.CancelAsync();
}

private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
if (e.Argument == null) return;
List<string> texts = new();
int n = (int)e.Argument;
for (int i = 0; i < n; i++) {
if (backgroundWorker.CancellationPending) {
e.Cancel = true;
return;
}
Thread.Sleep(100);
int progress = (int)((i + 1) / (double)n * 100.0);
backgroundWorker.ReportProgress(progress);
texts.Add($"This is {i + 1}!");
}
e.Result = texts;
}

private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
pb1.Value = e.ProgressPercentage;
btn1.Content = $"Progress: {e.ProgressPercentage}%";
}

private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if (e.Cancelled)
btn1.Content = "Cancelled!";
else if (e.Error != null)
MessageBox.Show("ERROR");
else {
var res = e.Result;
var primes = res as List<string>;
if (primes != null) {
foreach (var prime in primes) {
lb1.Items.Add(prime);
}
}
btn1.Content = "Finished!";
}
}
}
}
  • 后台主逻辑BackgroundWorker_DoWork(),状态更新逻辑BackgroundWorker_ProgressChanged(),执行结束逻辑BackgroundWorker_RunWorkerCompleted()
  • 后台逻辑中不允许直接修改UI元素,而进度更新逻辑和执行结束逻辑支持直接修改UI元素(框架已经将它们封装在Dispatcher逻辑中)。
  • backgroundWorker.ReportProgress(progress)报告进度,e.Result = texts返回后台计算结果,两者分别通过相应事件参数拿到。
  • 中止后台逻辑更加简单backgroundWorker.CancelAsync()

评论