none
.Net Object Event Handlers Do Not Seem To Fire Until After Exiting a Windows Form?

    Question

  • Hello,

    I am writing a PowerShell script in which a windows form GUI needs to be refreshed automatically via a .Net object event sink. In order to better communicate / understand the issue I have written a small script that reproduces the issue without cluttering the forums with the script I am working on. The simplified script is a windows form intended to act as a clock. The form has a single label that I would like to update with the current time when the Timer object Elapsed event fires. The behavior I am seeing is that the Timer Elapsed event is triggered, but the event handler does not appear to be running until after the Windows Form is closed.

    How can I update the form based on the Elapsed Event being triggered? I want to do this based on a different .net object event (File system watcher), but chose to use the timer elapsed event for the example. Any help you could provide in understanding the root cause for this issue would be greatly appreciated. Thank you, and happy scripting!

    Function New-Form {
    #Initialize Form/Controls
    $Form_Clock = New-Object System.Windows.Forms.Form
    $Label_Time = New-Object System.Windows.Forms.Label
    
    #Set Form Properties
    $Form_Clock.AutoSizeMode = 0
    $Form_Clock.Name = 'Clock Form'
    $Form_Clock.Text = 'Clock'
    
    #Set LBL_Test Properties
    $Label_Time.Dock = [System.Windows.Forms.Dockstyle]::Fill
    $Label_Time.Name = 'Time Label'
    $Label_Time.TextAlign = 16
    $Label_Time.Text = 'This Text Should Be Replaced By A Timer Event Firing'
    
    #Add Control
    $Form_Clock.Controls.Add($Label_Time)
    
    #Output the Form
    $Form_Clock
    }
    Function Set-FormEvents($Form_Clock){
    #Form Load Event Handler
    [scriptblock]$Form_Clock_Load_Handler={
    	$Timer = new-object Timers.Timer 
    	$Timer.Interval = 500 #.5 second
    	$Timer_Handler = {
    		$Form_Clock = $Event.MessageData
    		$Label_Time = $Form_Clock.controls | ?{$_.name -eq 'Time Label'}
    		Write-Host $Label_Time.Text
    		$Label_Time.text =  get-date -f "MM-dd-yyyy hh:mm:ss"
    		$Form_Clock.Refresh()
    	}
    	#Register Event Sink For .Net Timer Object
    	Register-ObjectEvent -InputObject $Timer -EventName Elapsed –SourceIdentifier TestTimer -Action $Timer_Handler -MessageData $Form_Clock
    	$Timer.Start() 
    }
    $Form_Clock.Add_Load($Form_Clock_Load_Handler)
    }
    #Begin Main
    Add-Type –AssemblyName System.Windows.Forms
    $Form_Clock = New-Form
    Set-FormEvents -Form $Form_Clock
    $Form_Clock.ShowDialog() | Out-Null


    • Edited by Scriptabit Thursday, June 07, 2012 2:56 PM Typo in Title
    Wednesday, June 06, 2012 11:17 PM

Answers

  • I had a few minutes this morning so I though I would generate a demo of how to grab FileSystemWatcher events in a form.

    Be sure to change the first line to a folder you are going to test with.

    $pathtowatch='e:\test2'
    [void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    [void][reflection.assembly]::Load("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
    [System.Windows.Forms.Application]::EnableVisualStyles()
    $form1 = New-Object 'System.Windows.Forms.Form'
    $listbox1 = New-Object 'System.Windows.Forms.ListBox'
    $buttonOK = New-Object 'System.Windows.Forms.Button'
    $timer1 = New-Object 'System.Windows.Forms.Timer'
    $button1 = New-Object 'System.Windows.Forms.Button'
    $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
    $FormEvent_Load={
    	$global:message_queue=new-object System.Collections.queue(100)
        $watcher = New-Object System.IO.FileSystemWatcher
        $watcher.Path = $pathtowatch
        $watcher.IncludeSubdirectories = $true
        $watcher.EnableRaisingEvents = $true	
        
        Register-ObjectEvent $watcher "Changed" -Action {
        		$message_queue.Enqueue("Changed: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Created" -Action {
        		$message_queue.Enqueue("Created: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Deleted" -Action {
        		$message_queue.Enqueue("Deleted: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Renamed" -Action {
        		$message_queue.Enqueue("Renamed: $($eventArgs.FullPath)")
        }
    }
    $timer1_Tick={
    while($message_queue.Count -gt 0){
    $listbox1.Items.Add($message_queue.Dequeue())
    }
    }
    $Form_StateCorrection_Load=
    {
    $form1.WindowState = $InitialFormWindowState
    }
    $form1.Controls.Add($listbox1)
    $form1.Controls.Add($buttonOK)
    $form1.AcceptButton = $buttonOK
    $form1.ClientSize = '479, 434'
    $form1.FormBorderStyle = 'FixedDialog'
    $form1.MaximizeBox = $False
    $form1.MinimizeBox = $False
    $form1.Name = "form1"
    $form1.StartPosition = 'CenterScreen'
    $form1.Text = "Form"
    $form1.add_Load($FormEvent_Load)
    #
    $listbox1.FormattingEnabled = $True
    $listbox1.Location = '21, 49'
    $listbox1.Name = "listbox1"
    $listbox1.Size = '406, 368'
    $listbox1.TabIndex = 1
    #
    $buttonOK.Anchor = 'Bottom, Right'
    $buttonOK.DialogResult = 'OK'
    $buttonOK.Location = '145, 12'
    $buttonOK.Name = "buttonOK"
    $buttonOK.Size = '75, 23'
    $buttonOK.TabIndex = 0
    $buttonOK.Text = "OK"
    $buttonOK.UseVisualStyleBackColor = $True
    #
    $timer1.Enabled = $True
    $timer1.Interval = 1000
    $timer1.add_Tick($timer1_Tick)
    #
    $button1.Location = '0, 0'
    $button1.Name = "button1"
    $button1.Size = '75, 23'
    $button1.TabIndex = 0
    $button1.Text = "button1"
    $button1.UseVisualStyleBackColor = $True
    $InitialFormWindowState = $form1.WindowState
    $form1.add_Load($Form_StateCorrection_Load)
    $form1.ShowDialog()

    The messages from the events will be nicely placed into the form.


    ¯\_(ツ)_/¯

    Friday, June 08, 2012 12:04 PM
  • Thank you, I believe I can modify this to work for my purposes. I do have a couple of questions though that I'll try to test out further - but you have defintely helped!

    I believe I may be running into some sort of scope issue where I was trying to directly modify the form itself from the timer event handler. I was trying to pass in the form by reference through the messagedata parameter in register-object. Is this something that can be done? I would like to avoid the use of a timer.

    YOu cannot do anything externaly while the form is running.  Everyhthing must be done from inside teh form.  It is alwys MODAL to then PowerShell prompt.  PowerSHell does not allow modeless forms as it is single threaded.  The form consumes that thread while it is visible.  A time exists on a background "system' process and wakes up the form via an asynchronous callback.  The system basically grabs the user thread and forces the execution of some code in teh user space while the user thread is suspended.  YOu cannot modify the form during this process but can when the form thread wakes up.  YOu cannot sen messages to and from Powershell.  You can return the form object back to the POwerShell session to be read.


    ¯\_(ツ)_/¯

    • Marked as answer by Scriptabit Thursday, June 28, 2012 4:22 PM
    Wednesday, June 27, 2012 10:21 PM

All replies

  • The Text you posted is very broken.  It cannot be copied and pasted.

    Can you just explain what it is you are trying to do and what errors you are getting?


    ¯\_(ツ)_/¯


    • Edited by jrv Thursday, June 07, 2012 5:46 AM
    Thursday, June 07, 2012 1:16 AM
  • Here is a demo of a form with a timer.

    function Call-Demo{
    	[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
    	$form1  = New-Object System.Windows.Forms.Form
    	$label1 = New-Object System.Windows.Forms.Label
    	$timer1 = New-Object System.Windows.Forms.Timer
    			
    	$form1.Controls.Add($label1)
    	$form1.StartPosition = 'CenterScreen'
    	$form1.add_Load($FormEvent_Load)
    	$label1.Location = '24, 132'
    	$label1.Name = "label1"
    	$label1.Size = '100, 23'
    	$timer1.Enabled = $True
    	$timer1_Tick={
    		$i+=1
    		$label1.Text = $i
    	}
    	
    	$timer1.add_Tick($timer1_Tick)
    	$form1.ShowDialog()
    }
    Call-Demo


    ¯\_(ツ)_/¯


    • Edited by jrv Thursday, June 07, 2012 5:53 AM
    Thursday, June 07, 2012 5:46 AM
  • Hi,

    I appreciate you taking the time to review my question - thank you!

    1.) I am able to copy / paste my sample code from the forum without an issue. Is the script being truncated/wrapped by a small display or something?

    2.) I am trying to use a .net object outside of the System.Windows.Forms namespace to fire an event and trigger the event handler while a form is open. Events generated by objects within the Windows.Forms namespace (like the windows.forms.timer in your sample) are handled while the form is open, but other .net objects are not. The reason I do not want a form (System.Windows.Forms) object to trigger the event is because ultimately I want to use the System.IO.FileSystemWatcher object, of which there is not a similiar object within the System.Windows.Forms namespace.

    I want to use the System.IO.FileSystemWatcher object to throw an event when a file is modified. The file modification event should occur while the form is open and trigger a modification of the form. The System.Timers.Timer object I used in my sample was an object I figured more people were familiar with that exhibits similiar behavior and that is why I used it in my sample.

    The behavior I am seeing is that the event is triggered and queued, but the event handler is not fired until after the form is closed.

    If you would find it more helpful, I can include a sample with the System.IO.FileSystemWatcher object being used. Let me know. :)

    Thursday, June 07, 2012 2:52 PM
  • As noted in my other reply, I am looking to use objects outside of the System.Windows.Forms namespace. Sorry for the confusion. Thank you for providing a sample though.
    Thursday, June 07, 2012 2:53 PM
  • I Suggest that you fix the title.  I think that is why many are ignoring your request.  The title should say 'EXIT' and not 'EXISTS'.  You can edit the title by editing your first message and selecting the option to change the title.  Thank you.

    You seem to misunderstand how Windows Forms work and how they work in PowerShell.  You have only one thread in PowerShell unless you use a job.  You cannot send events between threads.

    The FileSystemWatcher will work correctly inside of a Windows Form.  You can also park it in a job and then have the form poll the job object for output.

    Your original post says nothing about FIleSystemWatcher.  It is a question about the Timer events in a form.  As you can see that works correctly if it is coded correctly.

    To display a form we need to use $form1.ShowDialog() which is a blocking call.  The PowerShell thread waits on this line until the form is closed then continues execution.  Any code after this call will not execute until the form is closed.  Values from the form can be read from the $form1 object after the form is closed. DialogResult will tell you whether the form was saved or canceled with an Ok/Cancel status which can be used in an if($form1.ShowDialog()){ get data }

    I believe it is possible to cobble together a delegate in PowerShel but have never tried it in PosH V2.  I believe V3 has support for delegates so enhanced eventing in forms should be possible.


    ¯\_(ツ)_/¯

    Thursday, June 07, 2012 8:25 PM
  • I had a few minutes this morning so I though I would generate a demo of how to grab FileSystemWatcher events in a form.

    Be sure to change the first line to a folder you are going to test with.

    $pathtowatch='e:\test2'
    [void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    [void][reflection.assembly]::Load("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
    [System.Windows.Forms.Application]::EnableVisualStyles()
    $form1 = New-Object 'System.Windows.Forms.Form'
    $listbox1 = New-Object 'System.Windows.Forms.ListBox'
    $buttonOK = New-Object 'System.Windows.Forms.Button'
    $timer1 = New-Object 'System.Windows.Forms.Timer'
    $button1 = New-Object 'System.Windows.Forms.Button'
    $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
    $FormEvent_Load={
    	$global:message_queue=new-object System.Collections.queue(100)
        $watcher = New-Object System.IO.FileSystemWatcher
        $watcher.Path = $pathtowatch
        $watcher.IncludeSubdirectories = $true
        $watcher.EnableRaisingEvents = $true	
        
        Register-ObjectEvent $watcher "Changed" -Action {
        		$message_queue.Enqueue("Changed: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Created" -Action {
        		$message_queue.Enqueue("Created: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Deleted" -Action {
        		$message_queue.Enqueue("Deleted: $($eventArgs.FullPath)")
        }
        Register-ObjectEvent $watcher "Renamed" -Action {
        		$message_queue.Enqueue("Renamed: $($eventArgs.FullPath)")
        }
    }
    $timer1_Tick={
    while($message_queue.Count -gt 0){
    $listbox1.Items.Add($message_queue.Dequeue())
    }
    }
    $Form_StateCorrection_Load=
    {
    $form1.WindowState = $InitialFormWindowState
    }
    $form1.Controls.Add($listbox1)
    $form1.Controls.Add($buttonOK)
    $form1.AcceptButton = $buttonOK
    $form1.ClientSize = '479, 434'
    $form1.FormBorderStyle = 'FixedDialog'
    $form1.MaximizeBox = $False
    $form1.MinimizeBox = $False
    $form1.Name = "form1"
    $form1.StartPosition = 'CenterScreen'
    $form1.Text = "Form"
    $form1.add_Load($FormEvent_Load)
    #
    $listbox1.FormattingEnabled = $True
    $listbox1.Location = '21, 49'
    $listbox1.Name = "listbox1"
    $listbox1.Size = '406, 368'
    $listbox1.TabIndex = 1
    #
    $buttonOK.Anchor = 'Bottom, Right'
    $buttonOK.DialogResult = 'OK'
    $buttonOK.Location = '145, 12'
    $buttonOK.Name = "buttonOK"
    $buttonOK.Size = '75, 23'
    $buttonOK.TabIndex = 0
    $buttonOK.Text = "OK"
    $buttonOK.UseVisualStyleBackColor = $True
    #
    $timer1.Enabled = $True
    $timer1.Interval = 1000
    $timer1.add_Tick($timer1_Tick)
    #
    $button1.Location = '0, 0'
    $button1.Name = "button1"
    $button1.Size = '75, 23'
    $button1.TabIndex = 0
    $button1.Text = "button1"
    $button1.UseVisualStyleBackColor = $True
    $InitialFormWindowState = $form1.WindowState
    $form1.add_Load($Form_StateCorrection_Load)
    $form1.ShowDialog()

    The messages from the events will be nicely placed into the form.


    ¯\_(ツ)_/¯

    Friday, June 08, 2012 12:04 PM
  • Hello,

    I wanted to apologize as I haven't had a chance to review / work with the code in your response yet. I've been very busy over the past few days - but I wanted to say thank you for responding. I hope to be able to look at this tommorrow. I haven't forgotten about the thread :).

    -Scriptabit

    Tuesday, June 12, 2012 3:11 PM
  • Thank you, I believe I can modify this to work for my purposes. I do have a couple of questions though that I'll try to test out further - but you have defintely helped!

    I believe I may be running into some sort of scope issue where I was trying to directly modify the form itself from the timer event handler. I was trying to pass in the form by reference through the messagedata parameter in register-object. Is this something that can be done? I would like to avoid the use of a timer.

    Wednesday, June 27, 2012 8:28 PM
  • Thank you, I believe I can modify this to work for my purposes. I do have a couple of questions though that I'll try to test out further - but you have defintely helped!

    I believe I may be running into some sort of scope issue where I was trying to directly modify the form itself from the timer event handler. I was trying to pass in the form by reference through the messagedata parameter in register-object. Is this something that can be done? I would like to avoid the use of a timer.

    YOu cannot do anything externaly while the form is running.  Everyhthing must be done from inside teh form.  It is alwys MODAL to then PowerShell prompt.  PowerSHell does not allow modeless forms as it is single threaded.  The form consumes that thread while it is visible.  A time exists on a background "system' process and wakes up the form via an asynchronous callback.  The system basically grabs the user thread and forces the execution of some code in teh user space while the user thread is suspended.  YOu cannot modify the form during this process but can when the form thread wakes up.  YOu cannot sen messages to and from Powershell.  You can return the form object back to the POwerShell session to be read.


    ¯\_(ツ)_/¯

    • Marked as answer by Scriptabit Thursday, June 28, 2012 4:22 PM
    Wednesday, June 27, 2012 10:21 PM
  • thank you very much - that is the type of information I was looking for. [Most of my scripts don't use forms as you can probably tell :) heh]
    Thursday, June 28, 2012 4:22 PM