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

前言

  在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

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<TextBlock, String> updateAction = new Action<TextBlock, string>(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;
        }
    }

结果也是与第一种方法一致。(此方法也适用于WinForm)


转载:http://www.cnblogs.com/Jarvin/p/3756061.html?utm_source=tuicool&utm_medium=referral







  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WPF中,UI元素的更新只能在UI线程上进行,这意味着如果我们在其他线程上进行数据操作,然后再尝试更新UI元素,会出现异常或者UI不会立即更新的情况。 要在WPF中实现线程安全的UI更新,可以使用Dispatcher类。Dispatcher类可以让我们将操作在UI线程上执行,即使我们是在其他线程上调用的。 首先,我们需要获取到当前UI元素所在的Dispatcher对象,可以通过UI元素的Dispatcher属性来获取。然后,我们可以使用Dispatcher对象的Invoke或BeginInvoke方法将操作委托给UI线程执行。 例如,如果我们有一个DataGrid控件需要更新,我们可以首先获取DataGrid控件的Dispatcher对象,并在其他线程上进行数据操作后,使用Dispatcher对象的Invoke方法更新DataGrid控件的数据源。 示例代码如下: ```csharp // 获取DataGrid的Dispatcher对象 var dispatcher = dataGrid.Dispatcher; // 在其他线程上进行数据操作 Task.Run(() => { // 模拟一些数据操作 Thread.Sleep(1000); // 更新DataGrid的数据源 var newData = GetData(); // 通过Dispatcher对象的Invoke方法,在UI线程上更新DataGrid的数据源 dispatcher.Invoke(() => { dataGrid.ItemsSource = newData; }); }); ``` 在这个示例中,我们使用了Task.Run()方法来模拟在其他线程上进行数据操作,然后通过Dispatcher对象的Invoke方法,在UI线程上更新DataGrid控件的数据源。 通过使用Dispatcher类,我们可以确保在任何线程上进行的UI更新操作都会在UI线程上执行,保证了数据操作和UI更新线程安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值