How to keep dialog boxes on top in PowerShell

Not long ago I found myself writing a script that needed to be able to open directories and save files. This was part of a small side project I was working on, so it didn't really warrant building out a UI or anything, I just wanted the user to press the menu number for save file and have the familiar save file dialog appear.

That'll mostly stop them from typing paths incorrectly, trying to save somewhere they don't have permission to, and generally give it a nice experience.

What followed was a needlessly complicated rabbit-hole of Win32 API research and figuring out what people developing for Windows Forms in C# were doing in pursuit of something I've frankly always considered should be a given with this sort of thing. You see when I presented the glorious little box for the first time something happened that infuriated me.

It's behind the damn terminal

This is outrageous. It's unfair.

Of course I thought "yeah no problem, this'll be an easy fix. Trivial even.", but I couldn't seem to find anybody who had an answer, some in fact even said it couldn't be done in PowerShell!

Eventually I dug up an old reddit thread that linked to a site that's long since been taken offline, luckily I am a modern man of the internet and time is no longer of consequence to me, here's a copy of the original post on the wayback machine: PowerShell Code Repository - Modal File Dialogs (archive.org)
Here lies version 1, after today I hope it rests in peace:

Add-Type -TypeDefinition @"
using System;
using System.Windows.Forms;

public class Win32Window : IWin32Window
{
    private IntPtr _hWnd;
    
    public Win32Window(IntPtr handle)
    {
        _hWnd = handle;
    }

    public IntPtr Handle
    {
        get { return _hWnd; }
    }
}
"@ -ReferencedAssemblies "System.Windows.Forms.dll"

$owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.ShowDialog($owner)

Now this will work in a lot of environments, and is lightweight enough that you could include it in your script without a second thought. You Add-Type at the start of your script & set the $owner variable, then just provide it whenever you call the ShowDialog() method.

Not. Good. Enough.

I thought the job was done; this was created years before me, and will function for years after me, until the Win32 API is dead and buried.

I was wrong.
A few months passed, I had an idea for something and thought to myself  "I should use some dialogs for this!", but I wasn't deploying it to a server, this was a script for my workstation.
That's when I discovered the above snippet just won't work for the Windows Terminal app.

This is the single best terminal app I think I'm ever going to really use on Windows, so I need it to work.

The story of how I got there is a bit of a haze, so I can't share too many details of the religious journey that is trying to figure out the Win32 API without really sitting down and learning it in any in-depth way, but I arrived at something that I think will suffice.

Lo and behold, Get-ConsoleHandle

This is derived from the Microsoft documentation Obtain a Console Window Handle - Windows Server | Microsoft Docs, but with a sprinkle of some StackOverflow on top.

Lines 1 - 20

We start by adding a new type that will let us pass create a proper IntPtr for the ShowDialog() function, this is the same snippet we inherited from that old code repository. If there's a way around it I've not found it yet.

Lines 21 - 26

Now we're pulling in the FindWindow() method from user32.dll, just like the good book that is the Microsoft doc says, although we're using a string overload for the first argument lpClassName due to the fact that it will not take a null IntPtr in PowerShell because we don't seem to be able to handle that data type very well most of the time.

Lines 27 - end

The rest is basically a PowerShell converted copy of Microsoft's own documentation on the matter, there are some small changes like using the [System.Console] class to interact with the window title instead of relying solely on the Win32 API and casting Get-Date to a filetime instead of getting ticks, normal stuff like that.

Line 46 is where we call the FindWindow() method that we went to the trouble of importing user32.dll for

Line 49 is where we cast it to the Win32Window type from ye olde code repository

Afterthought

I'm not saying better native support for Win32 classes and Windows Forms is something that should be a high priority for this language.
What I'm saying is the thing that's giving me so much grief here is the fact that any time I try to use PowerShell for something serious I hit a roadblock like this one, but as long as people like you keep ending up on these articles, I'm going to keep building the tools; if time has taught me anything you're going to need them.

Like I always say, I'm sure this the last time I'll have to write it.

Rory Maher

Rory Maher