WPF多线程UI更新——两种方法

闪电
闪电
闪电
57
文章
0
评论
2020年4月27日15:04:32 评论 152

前言

 
  在WPF中,在使用多线程在后台进行计算限制的异步操作的时候,如果在后台线程中对UI进行了修改,则会出现一个错误:(调用线程无法访问此对象,因为另一个线程拥有该对象。)这是很常见的一个错误,一不小心就会有这个现象。在WPF中,如果不是用多线程的话,例如单线程应用程序,就是说代码一路过去都在GUI线程运行,可以随意更新任何东西,包括UI对象。但是使用多线程来更新UI就可能会出现以上所说问题,怎么解决?本文章提供两个方法:Dispatcher(大部分人使用),TaskScheduler(任务调度器)。

问题再现

 
  可能有的WPF新手不懂这是什么情况,先来个问题的再现,再使用本文章的两个方法进行解决。

  为了演示方便,我使用了最简单的布局,一个开始按钮,三个TextBlock。按一下开始按钮,开一个后台线程随机得到一个数字,并且更新第一个TextBlock。再开另外一个后台线程得到另外一个数字,更新第二个TextBlock。第三个TextBlock处理同理。

  XAML代码:

<Window x:Class="UpdateUIDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="130" Width="363">
    <Canvas>
        <TextBlock Width="40" Canvas.Left="38" Canvas.Top="27" Height="29" x:Name="first" Background="Black" Foreground="White"></TextBlock>
        <TextBlock Width="40" Canvas.Left="128" Canvas.Top="27" Height="29" x:Name="second" Background="Black" Foreground="White"></TextBlock>
        <TextBlock Width="40" Canvas.Left="211" Canvas.Top="27" Height="29" x:Name="Three" Background="Black" Foreground="White"></TextBlock>
        <Button Height="21" Width="50" Canvas.Left="271" Canvas.Top="58" Content="开始" Click="Button_Click"></Button>
    </Canvas>
</Window>

  后台代码:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(Work);
        }

        private void Work()
        {
            Task task = new Task((tb) => Begin(this.first), this.first);
            Task task2 = new Task((tb) => Begin(this.second), this.first);
            Task task3 = new Task((tb) => Begin(this.Three), this.first);
            task.Start();
            task.Wait();
            task2.Start();
            task2.Wait();
            task3.Start();
        }
        private void Begin(TextBlock tb)
        {
            int i=100000000;
            while (i>0)
            {
                i--;
            }
            Random random = new Random();
            String Num = random.Next(0, 100).ToString();
            tb.Text = Num;
        }
    }

  运行一下,在点击开始按钮的时候,得到了一个错误信息:

  果然不出所料,Begin函数是在后台线程执行的,tb这个TextBlock是前台UI线程的对象,所以无法在后台线程改变UI线程拥有的对象,很多有点经验的WPF程序员就会使用下面我要说的Dispatcher了!

问题解决

  * 方法一:Dispatcher

    1. 把UI更新的代码放到一个函数中:

    private void UpdateTb(TextBlock tb, string text)
    {
        tb.Text = text;
    }

    2. 使用Dispatcher,大家看修改后的Begin函数(红色内容):

    private void Begin(TextBlock tb)
    {
        int i=100000000;
        while (i>0)
        {
            i--;
        }
        Random random = new Random();
        String Num = random.Next(0, 100).ToString();
        Action updateAction = new Action(UpdateTb);
        tb.Dispatcher.BeginInvoke(updateAction,tb,Num);
    }

  再运行一次程序,可以看到能正常显示了,并且不会出现假死现象。

  * 方法二:任务调度器(TaskScheduler)

    有很多任务调度器,在CLR Var C#中就提出了线程池任务调度器,I/O任务调度器,任务限时调度器等,调度器的职责就是负责任务的调度,调节任务执行。同步上下文任务调度器就是该方法二所使用的调度器,其作用是将所有任务都调度给应用程序的GUI线程。

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private readonly TaskScheduler _syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task.Factory.StartNew(SchedulerWork);
        }
        private void SchedulerWork()
        {
            Task.Factory.StartNew(Begin, this.first).Wait();
            Task.Factory.StartNew(Begin, this.second).Wait();
            Task.Factory.StartNew(Begin, this.Three).Wait();
        }

        private void Begin(object obj)
        {
            TextBlock tb = obj as TextBlock;
            int i = 100000000;
            while (i>0)
            {
                i--;
            }
            Random random = new Random();
            String Num = random.Next(0,100).ToString();
            Task.Factory.StartNew(() => UpdateTb(tb, Num),
                    new CancellationTokenSource().Token, TaskCreationOptions.None, _syncContextTaskScheduler).Wait();
        }
        private void UpdateTb(TextBlock tb, string text)
        {
            tb.Text = text;
        }
    }

   结果展示:

WPF多线程UI更新——两种方法

总结

 
  任务调度器还有很多种,按照自己喜欢的方法来实现后台多线程更新UI。还有任务调度器也可以应用到Winform中。下面提供示例Demo下载。

完整Demo下载

继续阅读
weinxin
我的微信
微信扫一扫
闪电
  • 本文由 发表于 2020年4月27日15:04:32
  • 除非特殊声明,本站文章均为原创,转载请务必保留本文链接
WPF如何用TreeView制作好友列表、播放列表 .NET

WPF如何用TreeView制作好友列表、播放列表

前言   TreeView这个控件对于我来说是用得比较多的,以前做的小聊天软件(好友列表)、音乐播放器(播放列表)、类库展示器(树形类结构)等都用的是TreeView,下面以一个好友列表为例,说明一下...
WPF 蒙罩层 LoadingPage .NET

WPF 蒙罩层 LoadingPage

前言   无论是在PC客户端,移动端,网站,在遇到长时间处理的时候都会需要用到蒙罩层,让用户有更好的体现。今天上网逛了一下各位前辈网友的蒙罩层的实现方式,觉得有很多都搞复杂了(利用前台代码+后台代码+...
.NET 压缩解压库发布,支持进度查看 .NET

.NET 压缩解压库发布,支持进度查看

发布了一个 .net压缩解压库,基于SharpZipLib开发 ,支持进度查看,支持路径保持与否。 () 是一个基于SharpZipLib的一个压缩和解压的类库,提供给用户在.net环境下使用(VB....
.NET在EF中使用sql,用动态类吧! .NET

.NET在EF中使用sql,用动态类吧!

前言   在.NET中使用Entity Framework能快速、方便地结合LINQ来对数据库进行一系列的增删改查操作。但是由于EF根据表达式最后生成通用的sql来执行,进行具体的数据库操作。根据本人...
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: