Learn more about how WPF Dispatcher works.(Part 2 - PushFrame)

Introduction

In the previous article “Learn more about how WPF Dispatcher works. (Invoke and InvokeAsync)”, we found that Dispatcher.Invoke depends on Dispatcher.PushFrame method to wait without blocking. But how does Dispatcher.PushFrame work?

In this blog, we will introduce more details about Dispatcher.


Details

The original link is 深入了解 WPF Dispatcher 的工作原理(PushFrame 部分). But this blog was written in Chinese, so I translate its content to English. Waterlv is an MVP(Microsoft Most Valuable Professional), and he is good at .NET Core\WPF\.NET. Here is his Blog.


Dispatcher.PushFrame

If you are a WPF developer, you must have known the ShowDialog method of the Window class. But do you know how does ShowDialog method work? Why does the method which calls ShowDialog continue to execute after the window returns?

1
2
3
var w = new FooWindow();
w.ShowDialog();
Debug.WriteLine(w.Bar);

To answer these questions mentioned above, we have to read the source code of Dispatcher.PushFrame method. But before reading, we should learn some knowledge about the DoEvents method in Windows Forms.

DoEvents

The DoEvents method in the Windows Forms allows you to insert a UI rendering operation during executing a time-consuming operation, and it makes your application look like it doesn’t stop responding.

Here are the codes of DoEvents.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public void DoEvents()
{
DispatcherFrame 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;
}

Firstly, we should know the conclusion mentioned above that Dispacther.PushFrame method can wait without blocking UI thread. ( The theory will be posted below, but now just remember this conclusion.)

And base on the conclusion, we now analyze the theory of DoEvents which is posted above. And its steps are as followed.

  1. Add an instance of DispatcherOperation with Background priority (4) to execute the ExitFrame method.

  2. Call the Dispatcher.PushFrame method to wait without blocking UI thread.

  3. Due to the priority of user’s input is Input (5) and the priority of the UI response is Loaded (6) and the priority of rendering is Render (7), they all are higher than Background (4), so the ExitFrame method can only be executed after all UI task have been executed.

  4. The value of Dispatcher.Continue will be set to false when executing the ExitFrame method.

Base on the function of DoEvents, we can guess that the goal of setting the value of DispatcherFrame.Continue to false is to end the wait of Dispatcher.PushFrame(frame), so that the subsequent codes can be executed.

So according to the guess, we may know the steps of waiting without blocking UI thread.

  1. Call Dispatcher.PushFrame(frame) to wait without blocking.

  2. Set frame.Continue = false to end the wait and execute the subsequent codes.

It is easier to read the source codes of the Dispatcher.PushFrame with the guess and information above.

Source codes of PushFrame

Here are the codes.

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
50
51
[SecurityCritical, SecurityTreatAsSafe ]
private void PushFrameImpl(DispatcherFrame frame)
{
SynchronizationContext oldSyncContext = null;
SynchronizationContext newSyncContext = null;
MSG msg = new MSG();

_frameDepth++;
try
{
// Change the CLR SynchronizationContext to be compatable with our Dispatcher.
oldSyncContext = SynchronizationContext.Current;
newSyncContext = new DispatcherSynchronizationContext(this);
SynchronizationContext.SetSynchronizationContext(newSyncContext);

try
{
while(frame.Continue)
{
if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
break;

TranslateAndDispatchMessage(ref msg);
}

// If this was the last frame to exit after a quit, we
// can now dispose the dispatcher.
if(_frameDepth == 1)
{
if(_hasShutdownStarted)
{
ShutdownImpl();
}
}
}
finally
{
// Restore the old SynchronizationContext.
SynchronizationContext.SetSynchronizationContext(oldSyncContext);
}
}
finally
{
_frameDepth--;
if(_frameDepth == 0)
{
// We have exited all frames.
_exitAllFrames = false;
}
}
}

There are two points which need to pay attention to :

  1. _frameDepth field.
  2. The codes which are surrounded by while.

Let’s begin with the _frameDepth field. Every time you call the PushFrame method, you should pass an instance of DispatcherFrame, and the _frameDepth field will add 1 when another PushFrame is called during the period of PushFrame. So, one by one, the DispatcherFrame is nested in layers.

Then, we read the codes surrounded by while.

1
2
3
4
5
6
7
while(frame.Continue)
{
if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
break;

TranslateAndDispatchMessage(ref msg);
}

Do you remember the guess mentioned above? After reading these codes we noticed that the condition of the while is frame.Continue, and if its value is false the loop will be exited, and the PushFrame method will be returned, then the _frameDepth field will subtract 1. If all the frame.Continue are set to false, the Main method will be exited.

And, what will happen if the value of frame.Continue is always true? Obviously, it will enter a dead loop. But you can’t insert any UI operations to a dead loop, so how does it execute the UI operations? In the codes of this method, there are two possibilities, one is that GetMessage method allows the application to continue processing window messages, the other is that TranslateAndDispatchMessage method allows us to continue processing window messages. (The task queue of Dispatcher depends on the message mechanism of windows which is mentioned in the previous article).

Unfortunately, both methods call the unmanaged code, it is hard to know their theories by reading the source codes. But, we can debug the .NET Framework source code by source code debugging technology. And we found that GetMessage method is running all the time, and the TranslateAndDispatchMessage does not seem to be called. So we believe that the key to waiting without blocking is in the GetMessage method. For more information about .NET Framework source code debugging technology, visit 调试 ms 源代码 - 林德熙.

After reading the codes of GetMessage, we found messagePump which is an instance of UnsafeNagtiveMethods.ITfMessagePump. And we can know how does message loop work by using the source code debugging technology.

Debugging the source code to get how does PushFrame work

We add OnStylusDown method for MainWindow‘s StylusDown event.

1
2
3
4
5
6
7
8
private void OnStylusDown(object sender, StylusDownEventArgs e)
{
Dispatcher.Invoke(() =>
{
Console.WriteLine();
new MainWindow().ShowDialog();
}, DispatcherPriority.Background);
}

In these codes, both Dispatcher.Invoke and ShowDialog methods will call PushFrame. After running this application, every time we touch the MainWindow, we can found that there will add two PushFrame in the call-stack subwindow in VS. One is called by the Invoke method and the other is called by ShowDialog.

After each PushFrame executes, there is a transition between host and manage. Then message processing is followed, and the touching message is called from the message processing.

So, it’s sure that every time we execute PushFrame the unmanaged code will open a new message loop. When the window showed by ShowDialog closed or the Invoke has finished the message loop which is created by PushFrame will be exited. Therefore, the subsequent codes which are blocked by while can be executed. After the PushFrame of the Main method is exited, this application will close.

Conclusions

  1. Every time the PushFrame executed, a new message loop will be created, and the _frameDepth will add 1.
  2. In the new message loop, it can handle all kinds of Window’s message, some of them are transmitted in the form of events, and some are tasks that are added to the PriorityQueue<DispatcherOperation>.
  3. After exiting the PushFrame method, the message loop which is created by this code will be exited too. And the subsequent code of previous PushFrame will be executed.
  4. If all the PushFrame exited, the application will be closed.
  5. The While loop in PushFrame will block the main thread, but it can handle the messages in the loop, so it looks like the main thread is not blocked.

The defects of PushFrame

  1. PushFrame depends on the Windows’ message loop, and there are some bugs about multiple message loops. Like,

  2. Base on PushFrame‘s block mechanism, the unexpected reentrancy problem may happen is a single thread application. visit 异步任务中的重新进入(Reentrancy) for more information about the Reentrancy.

References