緣由
在以往的 VB6,或者是 Windows Form 應用程式中,更新 UI的方式極為簡單,往往只是 Application.DoEvents 就可以更新。Windows Form 中更有 Invoke 與 BeginInvoke 可以彈性地使用。
那在 WPF 中,要如何更新 UI 的內容呢?
範例1:Bad Example
當然,要從一個不正確的範例開始。
Ex1Bad.xaml
Ex1Bad.xaml.cs
usingSystem.Threading;
usingSystem.Windows;
namespaceWpfApplication10
{
publicpartial class Ex1Bad : Window
{
publicEx1Bad()
{
InitializeComponent();
}
privatevoid btnStart_Click(objectsender, RoutedEventArgs e)
{
lblMsg.Content = "Starting...";
Thread.Sleep(3000);//執行長時間工作
lblMsg.Content = "Doing...";
Thread.Sleep(3000);//執行長時間工作
lblMsg.Content = "Finished...";
}
}
}
這裡以 Thread.Sleep(3000)讓 UI Thread 睡個3秒鐘,來模擬長時間的工作。
這是個常見的程式碼,但卻是沒有用。在 Windows Form 的 API 中有 Application.DoEvents() 可以呼叫。WPF 中沒有類似的嗎?
範例2:使用Windows Form的 DoEvents
原來,WPF 中雖然沒有類似的 api 可以呼叫,但仍可以直接呼叫 Windows Form 的 Application.DoEvents.當然,需要引用 System.Windows.Forms.dll。
Ex2WinformDoEvents.xaml
usingSystem.Threading;
usingSystem.Windows;
usingswf = System.Windows.Forms;
namespaceWpfApplication10
{
publicpartial class Ex2WinformDoEvents : Window
{
publicEx2WinformDoEvents()
{
InitializeComponent();
}
privatevoid btnStart_Click(objectsender, 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是一模一樣的手法。
範例3:WPF DoEvents
哦?WPF 沒有 DoEvents 可以使用,只能呼叫老前輩Windows Form 的 API 嗎?也不是。在 DispacherFrame 文章中就有sample.
Ex3WPFDoEvents.xaml.cs
usingSystem;
usingSystem.Security.Permissions;
usingSystem.Threading;
usingSystem.Windows;
usingSystem.Windows.Threading;
namespaceWpfApplication10
{
publicpartial class Ex3WPFDoEvents : Window
{
publicEx3WPFDoEvents()
{
InitializeComponent();
}
privatevoid btnStart_Click(objectsender, 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)]
publicvoid DoEvents()
{
var frame = newDispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
newDispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
publicobject ExitFrame(objectf)
{
((DispatcherFrame)f).Continue = false;
returnnull;
}
publicstatic void DoEvents2()
{
Action action = delegate{ };
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, action);
}
}
}
DoEvents() 與 DoEvents2() 的效果相同。
DoEvents is Evil
DoEvents 這麼好用,為什麼WPF還要發明 Dispatcher,或 Windows Form 的 BeginInvoke 這些 API 呢?
跑以下的程式碼看看吧。
privatevoid btnEvil_Click(objectsender, RoutedEventArgs e)
{
for(inti = 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
usingSystem.ComponentModel;
usingSystem.Threading;
usingSystem.Windows;
usingSystem.Windows.Controls;
namespaceWpfApplication10
{
publicpartial class Ex4BackgroundWorker : Window
{
publicEx4BackgroundWorker()
{
InitializeComponent();
}
privatevoid btnStart_Click(objectsender, RoutedEventArgs e)
{
ExecuteLongTimeWork(lblMsg,"Starting");
ExecuteLongTimeWork(lblMsg,"Doing");
ExecuteLongTimeWork(lblMsg,"Finished");
}
privatevoid ExecuteLongTimeWork(Label label, stringmessage)
{
var backgroundWorker = newBackgroundWorker();
backgroundWorker.DoWork += (s, o) => {
Thread.Sleep(3000);//執行長時間工作
};
backgroundWorker.RunWorkerCompleted += (s, args) =>
{
Dispatcher.BeginInvoke(newAction(() =>
{
label.Content = message;
}));
};
backgroundWorker.RunWorkerAsync();
}
}
}