在WPF中,UI线程是比较特殊的线程,如何实现非UI线程对UI元素的修改是一个常见问题。
本文先介绍UI线程的基本知识,然后以实例方式介绍BackgroundWorker
的使用及其原理。
基本原则
- WPF元素是线程关联的。创建WPF元素的线程拥有该元素,其它线程不能直接与这些WPF元素进行交互
- 具有线程关联性的WPF对象有继承自
DispatcherObject
类
CheckAccess()
:检查DispatcherObject
的派生类对象是否在正确的线程上被修改,返回true
、false
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(); }
|
Invoke
和BeginInvoke
的区别
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); Dispatcher.BeginInvoke(DispatcherPriority.Normal, () => { pb1.Value = progress; btn1.Content = $"Progress: {progress}%"; }); texts.Add($"This is {i + 1}!"); } 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()
。