WPF的UI更新方式

2011年6月22日星期三

WPF的UI更新方式

緣由

在以往的 VB6,或者是 Windows Form 應用程式中,更新 UI的方式極為簡單,每每只是 Application.DoEvents 就能够更新。Windows Form 中更有 Invoke 與 BeginInvoke 能够彈性地使用。html

那在 WPF 中,要如何更新 UI 的內容呢?windows

範例1:Bad Example

當然,要從一個不正確的範例開始。api

Ex1Bad.xamlapp

?
1
2
3
4
5
6
7
8
9
< Window x:Class = "WpfApplication10.Ex1Bad"
         xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
         Title = "Ex1Bad" Height = "300" Width = "300" >
   < StackPanel >
     < Label Name = "lblMsg" Content = "Nothing" />
     < Button Content = "Start" Name = "btnStart" Click = "btnStart_Click" />
   </ StackPanel >
</ Window >

Ex1Bad.xaml.csoop

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Threading;
using System.Windows;
 
namespace WpfApplication10
{
   public partial class Ex1Bad : Window
   {
     public Ex1Bad()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Doing..." ;
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Finished..." ;
     }
   }
}

這裡以 Thread.Sleep(3000)讓 UI Thread 睡個3秒鐘,來模擬長時間的工做。post

這是個常見的程式碼,但卻是沒有用。在 Windows Form 的 API 中有 Application.DoEvents() 能够呼叫。WPF 中沒有類似的嗎?ui

範例2:使用Windows Form的 DoEvents

原來,WPF 中雖然沒有類似的 api 能够呼叫,但仍能够直接呼叫 Windows Form 的 Application.DoEvents.當然,须要引用 System.Windows.Forms.dll。spa

Ex2WinformDoEvents.xamlcode

?
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
using System.Threading;
using System.Windows;
using swf = System.Windows.Forms;
 
namespace WpfApplication10
{
   public partial class Ex2WinformDoEvents : Window
   {
     public Ex2WinformDoEvents()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       swf.Application.DoEvents();
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Doing..." ;
       swf.Application.DoEvents();
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Finished..." ;
     }
   }
}

在更新UI後,呼叫 swf.Application.DoEvents(),就能够更新 UI 了。這樣的方式與以前的 VB6是一模一樣的手法。component

範例3:WPF DoEvents

哦?WPF 沒有 DoEvents 能够使用,只能呼叫老前輩Windows Form 的 API 嗎?也不是。在 DispacherFrame 文章中就有sample.

Ex3WPFDoEvents.xaml.cs

?
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
using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
 
namespace WpfApplication10
{
   public partial class Ex3WPFDoEvents : Window
   {
     public Ex3WPFDoEvents()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       DoEvents();
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Doing..." ;
       DoEvents();
       Thread.Sleep(3000); //執行長時間工做
       lblMsg.Content = "Finished..." ;
     }
 
     [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
     public void DoEvents()
     {
       var frame = new DispatcherFrame();
       Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
           new DispatcherOperationCallback(ExitFrame), frame);
       Dispatcher.PushFrame(frame);
     }
 
     public object ExitFrame( object f)
     {
       ((DispatcherFrame)f).Continue = false ;
 
       return null ;
     }
 
     public static void DoEvents2()
     {
       Action action = delegate { };
       Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, action);
     }
   }
}

DoEvents() 與 DoEvents2() 的效果相同。

DoEvents is Evil

DoEvents 這麼好用,為什麼WPF還要發明 Dispatcher,或 Windows Form 的 BeginInvoke 這些 API  呢?

跑如下的程式碼看看吧。

?
1
2
3
4
5
6
7
8
private void btnEvil_Click( object sender, RoutedEventArgs e)
{
   for ( int i = 0; i < 10000000; i++)
   {
     System.Windows.Forms.Application.DoEvents();
   }
   MessageBox.Show( "Ok" );
}

執行時,記得打開工做管理員看看CPU 的負載,會持續飆高一斷時間。雖然 UI 沒有任何的更新,為何 CPU 會飆高呢?

DoEvent 的原理是execution loop,也就是以迴圈的方式來查詢是否有要更新的訊息(message)。一看到迴圈,各位看倌就知道是怎麼回事了吧。

範例3中的WPF 的 DoEvents 也是相同的道理。

範例4:BackgroundWorker

有沒有較正常的方式來更新 UI 呢?看一下Ex1Bad.xaml.cs的設計方式吧。更新lblMessage後執行一段工做,這基本上是同步的寫做方式。在 UI Thread 上執行工做,本來就會使得 UI 停頓,使用者感到不方變。

正確的方式,是使用BackgroundWorker來執行長時間的工做,並以非同步的方式更新在 UI Tread 上的UI內容。

Ex4BackgroundWorker.xaml.cs

?
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
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
 
namespace WpfApplication10
{
   public partial class Ex4BackgroundWorker : Window
   {
     public Ex4BackgroundWorker()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       ExecuteLongTimeWork(lblMsg, "Starting" );
       ExecuteLongTimeWork(lblMsg, "Doing" );
       ExecuteLongTimeWork(lblMsg, "Finished" );
     }
 
     private void ExecuteLongTimeWork(Label label, string message)
     {
       var backgroundWorker = new BackgroundWorker();
       backgroundWorker.DoWork += (s, o) => {
         Thread.Sleep(3000); //執行長時間工做
       };
 
       backgroundWorker.RunWorkerCompleted += (s, args) =>
                                                {
                                                  Dispatcher.BeginInvoke( new Action(() =>
                                                  {
                                                    label.Content = message;
                                                  }));
                                                };
       backgroundWorker.RunWorkerAsync();
     }
   }
}

BackgroundWorker 工做方式,是创建一個新的 Thread 來執行 DoWork 的event handler,執行完畢後,再執行 RunWorkerCompleted 的event handler。所以,我們须要的是在RunWorkerCompleted 的event handler 中更新的 UI。

更新 UI 時,又使用 Dispatcher 來更新 UI。基本上,Dispatcher 是一個 Queue 的概念,凡是使用 Dispatcher 來更新 UI,WPF 會照 DispatcherPriority 的優先順序來更新 UI。

結論

雖然只是小小的UI 更新方式,也是有很多的學問呢!