Windows Forms Threading Question

This topic contains 4 replies, has 2 voices, and was last updated by Profile photo of Micah Battin Micah Battin 2 years, 1 month ago.

  • Author
    Posts
  • #19908
    Profile photo of Micah Battin
    Micah Battin
    Participant

    To start, I know that this is more .Net than Powershell, but I want to ask anyways.

    I have a specific hurdle in which I am trying to find and easy and elegant approach for. I have a button cashing issue. I have a form that is designed to verify AD credentials. If the credentials are incorrect, I disable the submit button for 3 seconds and provide a countdown. The intent is that the user should not be able to click the submit button during the countdown so as to not accidentally lock the AD account he/she is working with. The problem is that since the thread is locked for the countdown. The submit button clicks on the disabled button is cashed and will wait for the submit button to be enabled again and nothing is accomplished by disabling it in the first place. I have also tried using something like System.Timers.Timer to preform the disable, countdown, and re-enable functions via event so as to try not to lock the thread. That didn't seem to work either as none of the Timer events actually executed until the form was disposed.

    Has anyone encountered this hurdle before? And the better question is, does anyone have a solution?

    Here is some test code to demonstrate my issue.

    Add-Type -AssemblyName System.Drawing,System.Windows.Forms
    
    ##############################    Form Objects    ##############################
    $Main = [Windows.Forms.Form]@{
        ClientSize = "520,600"
        Text = "`"Click Cashe`" Test"
    }
    $ListBox = [Windows.Forms.Listbox]@{
        Size = "500,530"
        Location = "10,60"
    }
    $Button = [Windows.Forms.Button]@{
        Size = "100,40"
        Location = "10,10"
        Text = "Test"
    }
    $Label = [Windows.Forms.Label]@{
        Size = "100,20"
        Location = "120,10"
        Text = "Enter 'Correct':"
    }
    $TextBox = [Windows.Forms.TextBox]@{
        Size = "280,20"
        Location = "230,10"
    }
    
    $Main.Controls.AddRange(@($ListBox,$Button,$Label,$TextBox))
    
    ##############################    Form Events    ##############################
    $Button.Add_Click({
        If ($TextBox.Text -eq "Correct"){
            $ListBox.Items.Add("Correct Entry")
        }
        Else{
            # Disable the button.
            $Button.Enabled = $False
            $ListBox.Items.Add("Incorrect Entry")
    
            # For 3 seconds, the button should not be able to be clicked.
            For ($i = 3;$i -gt 0;$i--) {
                $Button.Text = $i
                Sleep 1
            }
    
            # Re-enable the button.
            $Button.Text = "Test"
            $Button.Enabled = $True
    
            # The problem is, since the thread is locked up, the button clicks are cashed while disabled. 
            # When the thread is no longer in use, all the cashed clicks go through.
        }
    
    })
    
    [Windows.Forms.Application]::Run($Main)
    
  • #19910
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    A timer is the right solution here. Do you have a copy of the code where you were attempting to use that approach? It'll be easier to start from there, and explain why it didn't work the first time.

  • #19913
    Profile photo of Micah Battin
    Micah Battin
    Participant

    Here is what I got thus far.

    Add-Type -AssemblyName System.Drawing,System.Windows.Forms
    Remove-Variable -Name i -ErrorAction SilentlyContinue
    
    ##############################    Form Objects    ##############################
    $Main = [Windows.Forms.Form]@{
        ClientSize = "520,600"
        Text = "`"Click Cashe`" Test"
    }
    $ListBox = [Windows.Forms.Listbox]@{
        Size = "500,530"
        Location = "10,60"
    }
    $Button = [Windows.Forms.Button]@{
        Size = "100,40"
        Location = "10,10"
        Text = "Test"
    }
    $Label = [Windows.Forms.Label]@{
        Size = "100,20"
        Location = "120,10"
        Text = "Enter 'Correct':"
    }
    $TextBox = [Windows.Forms.TextBox]@{
        Size = "280,20"
        Location = "230,10"
    }
    
    $Main.Controls.AddRange(@($ListBox,$Button,$Label,$TextBox))
    
    ##############################    Form Events    ##############################
    $Timer = New-Object System.Timers.Timer(1000)
    $Timer_Elapsed = {
        If ($i -gt 0) {
            $Button.Text = $i
        }
        ElseIf($i -eq 0){
            $Button.Text = "Test"
            $Button.Enabled = $True
            $Timer.Stop()
            Remove-Variable -Name i
        }
        $i--  # Powershell.org forums do not like $--, ($iMinusMinus)
    }
    Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action $Timer_Elapsed
    
    $Button.Add_Click({
        If ($TextBox.Text -eq "Correct"){
            $ListBox.Items.Add("Correct Entry")
        }
        Else{
            If (!($i)){
                $i = 4
                $Button.Enabled = $False
                $ListBox.Items.Add("Incorrect Entry")
                $Timer.Start()
            }
    
        }
    
    })
    
    [Windows.Forms.Application]::Run($Main)
    
  • #19914
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    Ah, yes. Register-ObjectEvent is a bit funny here; PowerShell only processes those events at certain points in its REPL loop. In this case, PowerShell is waiting for the [Windows.Forms.Application]::Run() method to return, and the event action never gets executed.

    You can work around this by using System.Windows.Forms.Timer instead, and using add_Tick() to handle the timer events instead of Register_ObjectEvent. (To be honest, I'm not entirely sure why this works, but it does. Found the answer on StackOverflow. 🙂 )

    One other thing to be careful of is making variable assignments from within your event handler. In this example, I used a $script: scoped variable to make sure everything was reading / writing from the correct place:

    Add-Type -AssemblyName System.Drawing,System.Windows.Forms
    
    ##############################    Form Objects    ##############################
    $Main = [Windows.Forms.Form]@{
        ClientSize = "520,600"
        Text = "`"Click Cashe`" Test"
    }
    $ListBox = [Windows.Forms.Listbox]@{
        Size = "500,530"
        Location = "10,60"
    }
    $Button = [Windows.Forms.Button]@{
        Size = "100,40"
        Location = "10,10"
        Text = "Test"
    }
    $Label = [Windows.Forms.Label]@{
        Size = "100,20"
        Location = "120,10"
        Text = "Enter 'Correct':"
    }
    $TextBox = [Windows.Forms.TextBox]@{
        Size = "280,20"
        Location = "230,10"
    }
    
    $Main.Controls.AddRange(@($ListBox,$Button,$Label,$TextBox))
    
    $timer = New-Object System.Windows.Forms.Timer
    $timer.Enabled = $false
    $timer.Interval = 1000
    $timer.add_Tick({
        $script:Countdown = $script:Countdown-1 # Making forums happy by not using the decrement operator.
        if ($script:Countdown -le 0)
        {
            $Button.Enabled = $true
            $Button.Text = "Test"
            $timer.Stop()
        }
        else
        {
            $Button.Text = $script:Countdown
        }
    })
    
    ##############################    Form Events    ##############################
    $Button.Add_Click({
        If ($TextBox.Text -eq "Correct"){
            $ListBox.Items.Add("Correct Entry")
        }
        Else{
            # Disable the button.
            $Button.Enabled = $False
            $ListBox.Items.Add("Incorrect Entry")
    
            $Button.Text = "3"
    
            $script:Countdown = 3
            $timer.Start()
        }
    })
    
    [Windows.Forms.Application]::Run($Main)
    
  • #19916
    Profile photo of Micah Battin
    Micah Battin
    Participant

    Perfect!

    I guess this is a lesson for me regarding events and the existence of Windows.Forms.Timer. I rearranged the code for sure so as to not set variables inside of events.

    Thanks for your help Dave!

You must be logged in to reply to this topic.