WPF常用技巧-多线程处理

WPF支持单线程单元模型,该模型与在Windows窗体应用程序中使用的模型非常类似,具有以下几条原则:

  • WPF元素具有线程关联性。创建WPF元素的线程拥有所创建的元素,其他线程不能直接与这些WPF元素进行交互。
  • WPF对象都在类层次的某个位置继承自DispatcherObject类,DispatcherObject类提供了少量成员,用于核实访问WPF对象的代码是否在正确的线程上执行,如果没有,是否能切换位置。

Dispatcher类

Disparcher类的实例为一个调度程序,管理在WPF应用程序中发生的操作。调度程序拥有应用程序线程(也就是拥有线程中创建的WPF元素),并管理工作项队列,当应用程序运行时,调度程序接受新的工作请求,并且一次执行一个任务。

在WPF中,有一个基类DisparcherObject,所有WPF组件如WindowButton等都继承自DispatcherObject,当某个线程中第一次实例化DisparcherObject的子类时,会创建一个调度程序。因此,如果开多个线程,每个线程都展示独立的窗体,那么将会创建多个调度程序。但在一般情况下,开发应用程序只使用一个用户界面线程和一个调度程序。

  • 注意区分,DispatcherDispatcherObject并不是父子类。

DispatcherObject类

一、常用成员

Dispatcher:属性成员,返回管理该对象的调度程序。

CheckAccess():如果代码在正确的线程上使用对象,就返回true,否则返回false

VerifyAccess():如果代码在正确的线程上使用对象,就什么也不做,否则抛出InvalidOperationException异常。

  • 一般情况下我们不需要自己去调用VerifyAccess()方法,WPF对象为保护自身会频繁调用VerifyAccess()方法,从而不可能在错误的线程中长时间使用一个对象。

在需要跨线程访问控件时,可以通过控件的调度程序,即Dispatcher对象的Invoke()BeginInvoke()方法来将代码安排为调度程序的任务,然后控件的调度程序会去执行这些代码。

BeginInvoke(DispatcherPriority priority, Delegate method):第一个参数指示任务的优先级,为DispatcherPriority枚举类型,一般情况下使用DispatcherPriority.Normal即可,如果任务不需要被立即完成,也可以使用更低的优先级;第二个参数为一个方法的委托Delegate类型,该委托指向具体任务的方法。

  • DispatcherPriority.ApplicationIdle:等待应用程序在完成所有其他工作时执行指定的任务。
  • DispatcherPriority.SystemIdle:比ApplicationIdle优先级更低,直到整个系统都处于休息状态,并且CPU处于空闲状态才执行。
private void Button_Click(object sender, RoutedEventArgs e)
{
	Task.Run(() =>
	{
	    Change();
	});
}

private void Change()
{
	if (!CheckAccess())
	{
	    Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>{
	        lbl_Test.Content = "Test-ChangeOne";
	    })); ;
	}
	else
	{
	    lbl_Test.Content = "Test-ChangeTwo";
	}
}

Invoke()Invoke()函数的参数是一个ActionFunc类型对象,与BeginInvoke的区别是,BeginInvoke是异步执行的,Invoke同步执行的,使用Invoke时,如果执行任务比较耗时,会导致UI界面卡死。

private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(() =>
    {
        Change();
    });
}

private void Change()
{
    if (!CheckAccess())
    {
        Dispatcher.Invoke(DispatcherPriority.Normal, new Action((
        {
            Thread.Sleep(5000);//这里做延时,会发现UI界面卡住
            lbl_Test.Content = "Test-ChangeOne";
        })); 
    }
    else
    {
        lbl_Test.Content = "Test-ChangeTwo";
    }
}

二、Dispacher调度器对象的获取

常见的获取Dispacher调度器的方式有如下三种:

直接调用Dispatcher属性

由于WPF中的绝大多是类型都是DispatcherObject的子类,因此继承了Dispatcher属性,可以直接在类中通过Dispatcher来获取。(视图的后台代码继承了WindowUserControl等都是DispatcherObject的子类)

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Dispatcher.BeginInvoke(() =>
        {
					
        });
    }
}

通过Dispatcher的静态属性

通过System.Windows.Threading命名空间下的Dispatcher属性可以获得当先线程的调度程序对象。

  • 注意,这里是获取当前线程的调度器对象,并不一定能获得UI的调度器对象。
var dispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;

通过Application获取

如果是在应用程序中,如WPF,可以通过Application.Current.Dispatcher来获取当前UI线程的调度程序对象。

  • 这里可以直接获取到当前应用的UI调度器对象
var dispatcher = Application.Current.Dispatcher;

DispatcherTimer

在WPF中常常会遇到按照一定间隔时间执行同一个任务的场景,这个时候就可以使用定时器DispatcherTimer来进行定时任务的设定了。

  • DispatcherTimer执行任务的线程是在UI调度器所在线程上,所以可以在执行任务中直接访问和操作UI元素,而不会引发线程安全问题。

常用的两种创建方式:

//第一种
private DispatcherTimer? _timer;
private void MyTask(){ ... }
public MainWindowViewModel()
{
    _timer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Loaded, 
								new EventHandler((s, e) => MyTask()), Application.Current.Dispatcher);
}

//第二种
private DispatcherTimer _timer = new DispatcherTimer();
private void MyTask(object? sender, EventArgs e) { ... }
public MainWindowViewModel()
{
    _timer.Interval = TimeSpan.FromSeconds(1);
    _timer.Tick += MyTask;
}

计时器的开始与停止:

_timer?.Start();
_timer?.Stop();

你可能感兴趣的:(WPF常用技巧,wpf)