ShowDialog() Sucks: Use ApplicationContext and Run Instead

Update Jan 7: Added KeyboardInterop so that TextBoxes will work.

One of the lessons learned when making Popups and NotifyIcons in PowerShell and WPF is that the WPF window must be run the the proper context.

I was tipped off to this by Denniver Reining's PowerShell / NotifyIcon article and Johnny J's "Doing a (C#) NotifyIcon program the right way".

While neither of the articles were using WPF, extensive testing showed that using ApplicationContexts and Application.Run instead of ShowDialog() made WPF (and WinForms) work far better. The NotifyIcon issue took a number of days to resolve as I battled with unresponsiveness when clicking on the ContextMenu to Exit. Then it would take about 5 seconds to disappear.

For about a week, this was the story of my life:

aero_busy

And it seemed especially true after hiding the Powershell taskbar application. So I scoured the Internet and found that a lot of other people had the same issue. Just search Google for "NotifyIcon doesn't disappear".

In addition, the mouse sometimes showed as busy when hoovering over the popup window itself and sometimes the right clicking worked only once.

notify-wait

System.Windows.Forms to the rescue (!?)

So none of the solutions I found worked, but I remembered that Denniver's NotifyIcon script was responsive, so I went back and noticed his app ended with this important line

[void][System.Windows.Forms.Application]::Run($form1)

This information, along with Johnny J's article about NotifyIcons helped me figure out that the following would probably work, even for WPF. Now these two lines, along with Hide-PowerShell are always included in my finalized PowerShell-based WPF scripts.

$appContext = New-Object System.Windows.Forms.ApplicationContext [void][System.Windows.Forms.Application]::Run($appContext)

According to Microsoft, "Application.Run begins running a standard application message loop on the current thread, with an ApplicationContext, which specifies the contextual information about an application thread."

I'll be honest, I'm probably not the best person to explain ApplicationContexts in depth. If you'd like to learn more, check out this article titled Use the ApplicationContext Class to Fully Encapsulate Splash Screen Functionality where the author goes into detail about the ApplicationContext class.

The code below (which is also the code from my previous post) shows a fully functioning WPF GUI App that runs in an application with an ApplicationContext. It also has a few other cool techniques you may enjoy.

Icon-Overlay

Note that this code should't be copy/pasted into the console because the PowerShell window will disappear before it can complete the paste. Be sure to download the script or paste the code below into a .ps1 file and execute. It does appear to work well in the ISE, though!

 1# THIS CODE CANNOT BE COPY/PASTED INTO THE CONSOLE. PLEASE SAVE IT AS A FILE AND EXECUTE.
 2 
 3# Add required assemblies
 4Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration
 5 
 6# Setup the XAML
 7[xml]$script:xaml = '<Window 
 8        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 9        Title="Acadiana (Cajun) Flag" Height="240" Width="320" Background="Gray">
10 <Window.TaskbarItemInfo>
11         <TaskbarItemInfo/>
12 </Window.TaskbarItemInfo>
13    <Grid>
14        <Image Name="image" Height="64" Width="64"/>
15    </Grid>
16</Window>'
17 
18# Create the form and set variables
19$script:window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))
20$xaml.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $window.FindName($_.Name) -Scope Script }
21 
22# here's the base64 string of the image
23$base64 = "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"
40 
41 
42# Create a streaming image by streaming the base64 string to a bitmap streamsource
43$bitmap = New-Object System.Windows.Media.Imaging.BitmapImage
44$bitmap.BeginInit()
45$bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64)
46$bitmap.EndInit()
47$bitmap.Freeze()
48 
49# This is the pic in the middle
50$image.source = $bitmap
51 
52# This is the icon in the upper left hand corner of the app
53$window.Icon = $bitmap
54 
55# This is the toolbar icon and description
56$window.TaskbarItemInfo.Overlay = $bitmap
57$window.TaskbarItemInfo.Description = $window.Title
58 
59# Add Exit (Thanks, Ryan!)
60$window.Add_Closing({[System.Windows.Forms.Application]::Exit(); Stop-Process $pid})
61 
62# Make PowerShell Disappear 
63$windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' 
64$asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru 
65$null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0) 
66
67# Allow input to window for TextBoxes, etc
68[System.Windows.Forms.Integration.ElementHost]::EnableModelessKeyboardInterop($window)
69
70# Running this without $appContext and ::Run would actually cause a really poor response.
71$window.Show()
72
73# This makes it pop up
74$window.Activate()
75 
76# Create an application context for it to all run within. 
77# This helps with responsiveness and threading.
78$appContext = New-Object System.Windows.Forms.ApplicationContext 
79[void][System.Windows.Forms.Application]::Run($appContext)

Even though I had changed my own usage of WPF forms, it didn't occur to me to blog about it until Doug Finke mentioned on Twitter that his WPF apps are sometimes unstable and crash. When I saw the ShowDialog() in his code, I knew exactly what the problem was, and knew I had to get the word out.

As of today, one of my servers has been running its WPF/PowerShell based-monitor for almost a month!

timespan

In conclusion, you should probably never use ShowDialog() to run your primary window in PowerShell unless you're testing it (and I should probably update my old blog posts to clarify that).

Also, I don't totally know what I'm doing, so take this with a grain of salt ;) It's just worked better for me than ShowDialog(). I will probably revisit this one day with a PowerShell implementation of InitializeComponent().