PowerShell icon indicating copy to clipboard operation
PowerShell copied to clipboard

Start-Sleep - unlike System.Threading.Thread.Sleep - in a WinForms DoEvents loop prevents reactivating a minimized window

Open mklement0 opened this issue 1 year ago • 6 comments

Prerequisites

Steps to reproduce

Note:

  • This is clearly not a common use case, but it would still be good to know the cause of the problem.
  • WPF windows are equally affected.
  • Using [System.Threading.Thread]::Sleep() in lieu of Start-Sleep makes the problem go away.
  • See below for additional details regarding what messages are missed.

Repro steps:

  • Run the following code, which shows a WinForms form non-modally and then uses a DoEvents() loop with periodic Start-Sleep calls to process GUI events until the form is closed:
Add-Type -AssemblyName System.Windows.Forms

# Create a sample form.
$form = [System.Windows.Forms.Form] @{
  Text = "Sample"; Size = [System.Drawing.Size]::new(600, 200); StartPosition = 'CenterScreen'
}

# Show the form *non-modally*
$null = $form.Show()

while ($form.Visible) {

  # Allow the form to process events.
  [System.Windows.Forms.Application]::DoEvents()

  # Sleep a little, to avoid a near-tight loop.
  # !! THIS CAUSES THE PROBLEM.
  # !! If you replace the Start-Sleep call with a call
  # !! to [System.Threading.Thread]::Sleep(), the problem goes away.
  Start-Sleep -Milliseconds 50

}
  • Minimize the form.

  • Hover over the PowerShell instance's taskbar button and try to reactivate the form by clicking on its thumbnail.

Expected behavior

The form should be restored and activated.

Actual behavior

Nothing happens (while you hover, the form is previewed in its original position after a brief delay, but clicking the thumbnail is ineffective in restoring and activating it; clicking on the thumbnail's close button does close the form).

Also, trying to Alt-Tab to the window doesn't activate it either.

Error details

No response

Environment data

PowerShell 7.4.0-preview.3 on Windows 11 22H2

Visuals

No response

mklement0 avatar Jun 13 '23 21:06 mklement0

First of all, thank you for sharing your experience. My experience is using timers to do this.

$timer1 = New-Object System.Windows.Forms.Timer
$timer1.Interval = 1000 * 3
$timer1.add_tick($timer1_code)

kasini3000 avatar Jun 14 '23 04:06 kasini3000

I can confirm this behavior for Windows 7 SP1, Powershell 5.1, .NET 4.8 (cannot test on newer OS right now). With Start-Sleep, the dialog misses the (WM_SYSCOMMAND, SC_MINIMIZE=0xF020) message that is sent if the taskbar icon is clicked while the dialog is open - likewise it misses the (WM_SYSCOMMAND, SC_RESTORE=0xF120) when the dialog was minimized, leaving it pretty inoperable in the taskbar. This does not happen when waiting with [Threading.Thread]::Sleep(20).

I also want to note that this behavior is independent from running the Winforms event handler or a WPF pendant.

lippmaje avatar Jun 14 '23 09:06 lippmaje

It would be better to replace Windows.Forms with cross-platformed TUI-toolkit out of the box.

237dmitry avatar Jun 14 '23 11:06 237dmitry

Thanks for the additional details, @lippmaje - I've edited the initial post to include some of them and added a link to your comment.

@kasini3000, yes, in addition to switching from Start-Sleep to [System.Threading.Thread]::Sleep(), using a System.Windows.Forms.Timer instance combined with showing the form modally (.ShowDialog()) is another way to avoid the problem, given that WinForms / WPF is then in full control of the message loop, I presume. (It does, however, require you to organize the PowerShell code to be run while the form is shown differently, given that the code then runs in a child scope.)

mklement0 avatar Jun 14 '23 13:06 mklement0

@mklement0

  • You can't use System.Windows.Forms.Application.DoEvents if you don't run a System.Windows.Forms.Application instance
  • DoEvents is not a recommanded API. See the caution on the remarks system.windows.forms.application.doevents and a google search indicate that it's a problematic API inherited from VBA.
  • A lot of features in WinForm/WPF don't work without managing a two threads model (you need a separate runspace for the thread UI and a dispatcher to execute some actions from the main thread to the UI thread )

fMichaleczek avatar Jun 14 '23 23:06 fMichaleczek

The Cmdlet Start-Sleep uses System.Threading.ManualResetEvent API instead of System.Threading.Thread.Sleep I can reproduce an issue between the method DoEvents and the ManualResetEvent class. It can also be resolved it by using a unique ManualResetEvent instance. (something that Start-Sleep is not doing I guess)

Not Working :

Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
    Text = 'Sample'
    Size = [System.Drawing.Size]::new(600, 200)
    StartPosition = 'CenterScreen'
}
[void] $form.Show()
while ($form.Visible) {
    [void] [System.Windows.Forms.Application]::DoEvents()
    $waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
    [void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
    [void] $waitHandle.Set()
    $waitHandle.Close()
}

Working :

Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
    Text = 'Sample'
    Size = [System.Drawing.Size]::new(600, 200)
    StartPosition = 'CenterScreen'
}
[void] $form.Show()
$waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
while ($form.Visible) {
    [void] [System.Windows.Forms.Application]::DoEvents()
    [void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
    [void] $waitHandle.Set()
} 
$waitHandle.Close()

Working :

Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
    Text = 'Sample'
    Size = [System.Drawing.Size]::new(600, 200)
    StartPosition = 'CenterScreen'
}
[void] $form.Show()
while ($form.Visible) {
    [void] [System.Windows.Forms.Application]::DoEvents()
    if ($form.Focused) {
        $waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
        [void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
        [void] $waitHandle.Set()
        $waitHandle.Close()
    }
}

Working :

Add-Type -AssemblyName System.Windows.Forms

$form = [System.Windows.Forms.Form]@{
    Text = 'Sample'
    Size = [System.Drawing.Size]::new(600, 200)
    StartPosition = 'CenterScreen'
}
[void] $form.Show()

while ($form.Visible) {
    [void] [System.Windows.Forms.Application]::DoEvents()
    $waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
    if ($form.WindowState -ne 'Minimized') {
        [void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
    }
    [void] $waitHandle.Set()
    $waitHandle.Close()
}

fMichaleczek avatar Jun 15 '23 22:06 fMichaleczek