I've tried a lot of "works in ise but not in console" links via Google and especially here on Stackoverflow, but alas nothing I've tried so far worked.
I'll start with the whole code:
Param (
[ValidateRange(2,10)][int]$Time = 5,
[ValidateLength(4,40)][String]$Title = "Attention $($Env:USERNAME.ToUpper())",
[ValidateLength(4,40)][String]$Text = "This is a test!",
[ValidateSet("None","Info","Warning","Error")] $Status = "Info",
[ValidatePattern("[\\:\-\w\d\s]+.exe")][String]$TrayIcon = (Get-Process -id $pid).Path
)
# sec to ms
$TTL = $Time * 1000
if (![System.IO.File]::Exists($TrayIcon)){
Write-Host "File does not exist!"
exit
}
function ClearEvents {
# Perform cleanup actions on balloon tip
$global:balloon.dispose()
Remove-Variable -Name balloon -Scope Global
Unregister-Event -SourceIdentifier TrayClicked
Remove-Job -Name TrayClicked
Unregister-Event -SourceIdentifier BallonClicked
Remove-Job -Name BallonClicked
Unregister-Event -SourceIdentifier BalloonClosed
Remove-Job -Name BalloonClosed
#test if function is called
[System.Windows.MessageBox]::Show('Hello')
}
Add-Type -AssemblyName System.Windows.Forms
$global:balloon = New-Object System.Windows.Forms.NotifyIcon
# Tray-Icon
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($TrayIcon)
# Balloon
# [System.Windows.Forms.ToolTipIcon] | Get-Member -Static -Type Property
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Status
$balloon.BalloonTipTitle = $Title
$balloon.BalloonTipText = $Text
$balloon.Visible = $true
$balloon.ShowBalloonTip($TTL)
[void](Register-ObjectEvent -InputObject $balloon -EventName MouseClick -SourceIdentifier TrayClicked -Action {
ClearEvents
})
[void](Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClicked -SourceIdentifier BallonClicked -Action {
ClearEvents
})
[void](Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClosed -SourceIdentifier BalloonClosed -Action {
ClearEvents
})
Now, when one of the events are triggered (like Balloon clicked), then the "ClearEvents" function gets called (for testing purposes I added a MsgBox).
But in a console these events never trigger! Why is that?
Tested with Win7x64 (PS 3.0) and Win10x64 (PS 5.1)
I've also confirmed with [System.Threading.Thread]::CurrentThread.GetApartmentState() that both consoles run indeed with the default "STA" (Single-Threaded Apartment), which apparently some "System.Windows.Forms" need to run correctly.
Unfortunately that's not the problem...
My script is based on this one: https://mcpmag.com/articles/2017/09/07/creating-a-balloon-tip-notification-using-powershell.aspx
Related
I'm writing a PowerShell script in Visual Studio Code which is intended to run a function for every tick of a timer. Here is the code:
$timer = New-Object Timers.Timer
$timer.Interval = 1000
$timer.Enabled = $true
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {Write-Host "Tick"}
The problem is that I don't seem to be able to stop the script using Ctrl+C which is what I would normally do, or indeed anything other than actually killing the terminal using the 'dustbin' icon. It just continues printing 'Tick'!
What's going on here? How would I stop this script gracefully?
When you subscribe an action to an event using Register-ObjectEvent, it can be unregistered using the Unregister-Event cmdlet:
$timer = New-Object Timers.Timer
$timer.Interval = 1000
$timer.Enabled = $true
Register-ObjectEvent -SourceIdentifier MyElapsedTick -InputObject $timer -EventName Elapsed -Action {Write-Host "Tick"}
<# ... #>
Unregister-Event -SourceIdentifier MyElapsedTick
To enumerate existing subscribers (for when you forget to specify a SourceIdenfitier for example), use Get-EventSubscriber:
# This will unregister all event subscribers
Get-EventSubscriber |Unregister-Event
$timer = [System.Timers.Timer]::new()
$timer.Interval = 5000
$timer.AutoReset = $false
$timer.Enabled = $true
1..20 | % {
if (!$timer.Enabled){
exit
}
write-host $_, $timer.Enabled
Start-Sleep -Milliseconds 1000
}
We have a convoluted solution to some printing issues (caused by citrix and remote servers). Basically from the main server, we force push a pdf file to the remote pc and then have a powershell script which constantly runs on the remote pc to "catch" the file and push it to the local printer
This works "fine"
However we get random dropouts. The powershell script doesn't seem to have crashed because it's still running in windows but the actual action doesn't seem to be processing new files
I have done A LOT of reading today and there's mention of having to name and unregister events when you're done otherwise it can cause a buffer overflow issues and make powershell stop processing the action. But I'm unsure where it should actually go within the code. The idea is that this script will run permanently, so do we unregister or remove the event within the action itself or somewhere else?
I previous had A LOT of dummy logging going on within the action to try to find where it failed, but it seems to stop at different points without any justifiable reason (ie, it would fail at the command to find files, other times at the command to move etc etc)
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "l:\files\cut"
$watcher.Filter = "*.pdf"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$scandir="l:\files\cut"
$scanbackdir="l:\files\cut\back"
$scanlogdir="l:\files\cut\log"
$sumatra="l:\SumatraPDF.exe"
$pdftoprint=""
$printername= "MainLBL"
### Get the List of files in the Directory, print file, wait and then move file
Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % {
$pdftoprint=$_
& $sumatra -silent $scandir\$pdftoprint -print-to $printername
sleep 3
Move-Item -force $scandir\$pdftoprint $scanbackdir
}
}
### Define what happens when script fails
$erroraction = {echo $(get-date) the process crashed | Out-File -Append l:\files\cut\log\errorlog.txt}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Error" -Action $erroraction
Register-ObjectEvent $watcher "Created" -Action $action
while ($true) {sleep 5}
If you want a script to run in the background, then look to PowerShell background jobs.
If you want a script to run permanently, then you want to make it a service ...
See these:
How to Create a User-Defined Service
How to run a PowerShell script as a Windows service
... or attach that to a scheduled task, that would restart it on reboots.
There are two ways to implement a FileSystemWatcher.
Synchronous
Asynchronous
A synchronous FileSystemWatcher, by it nature, when a change is detected, control is returned to your script so it can process the change. If another file change occurs while your script is no longer waiting for events, it gets lost. Hence leading to unexpected outcomes.
Using the FileSystemWatcher Asynchronously, it would continue to log new filesystem changes and process them once PowerShell is done processing previous changes.
* An Sample - Example Asynchronous FileSystemWatcher*
### New-FileSystemWatcherAsynchronous
# Set the folder target
$PathToMonitor = Read-Host -Prompt 'Enter a folder path'
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $PathToMonitor
$FileSystemWatcher.IncludeSubdirectories = $true
# Set emits events
$FileSystemWatcher.EnableRaisingEvents = $true
# Define change actions
$Action = {
$details = $event.SourceEventArgs
$Name = $details.Name
$FullPath = $details.FullPath
$OldFullPath = $details.OldFullPath
$OldName = $details.OldName
$ChangeType = $details.ChangeType
$Timestamp = $event.TimeGenerated
$text = "{0} was {1} at {2}" -f $FullPath, $ChangeType, $Timestamp
Write-Host $text -ForegroundColor Green
# Define change types
switch ($ChangeType)
{
'Changed' { "CHANGE" }
'Created' { "CREATED"}
'Deleted' { "DELETED"
# Set time intensive handler
Write-Host "Deletion Started" -ForegroundColor Gray
Start-Sleep -Seconds 3
Write-Warning -Message 'Deletion complete'
}
'Renamed' {
$text = "File {0} was renamed to {1}" -f $OldName, $Name
Write-Host $text -ForegroundColor Yellow
}
default { Write-Host $_ -ForegroundColor Red -BackgroundColor White }
}
}
# Set event handlers
$handlers = . {
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Changed -Action $Action -SourceIdentifier FSChange
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreate
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Deleted -Action $Action -SourceIdentifier FSDelete
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Renamed -Action $Action -SourceIdentifier FSRename
}
Write-Host "Watching for changes to $PathToMonitor" -ForegroundColor Cyan
try
{
do
{
Wait-Event -Timeout 1
Write-Host '.' -NoNewline
} while ($true)
}
finally
{
# End script actions + CTRL+C executes the remove event handlers
Unregister-Event -SourceIdentifier FSChange
Unregister-Event -SourceIdentifier FSCreate
Unregister-Event -SourceIdentifier FSDelete
Unregister-Event -SourceIdentifier FSRename
# Remaining cleanup
$handlers |
Remove-Job
$FileSystemWatcher.EnableRaisingEvents = $false
$FileSystemWatcher.Dispose()
Write-Warning -Message 'Event Handler completed and disabled.'
}
I have not encountered a script that will run permanently on windows.
So with that in mind we take it for granted that some issue beyond your control such as the network or power or a system shutdown will occur.
With that in mind we have a lifecycle for this script and everything should be properly cleaned up at the end. In this case we have while loop that should theoretically never end however if an exception is thrown it will end. Within the while loop if any of the events have been deregistered we can reregister them. If the watcher has been disposed we can recreate it and the events. If this really is mission critical code, then I would look at .net as an alternative with something like hangfire with nlog as a windows service.
### WRAP Everything in a try finally so we dispose of events
try {
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcherArgs = #{
Path = "l:\files\cut"
Filter = "*.pdf"
IncludeSubdirectories = $false
EnableRaisingEvents = $true
}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watcherArgs.Path
$watcher.Filter = $watcherArgs.Filter
$watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$scandir="l:\files\cut"
$scanbackdir="l:\files\cut\back"
$scanlogdir="l:\files\cut\log"
$sumatra="l:\SumatraPDF.exe"
$pdftoprint=""
$printername= "MainLBL"
### Get the List of files in the Directory, print file, wait and then move file
Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % {
$pdftoprint=$_
if($LASTEXITCODE -ne 0) {
# Do something
# Reset so we know when sumatra fails
$LASTEXITCODE = 0
}
& $sumatra -silent $scandir\$pdftoprint -print-to $printername
if($LASTEXITCODE -ne 0) {
# Do something to handle sumatra
}
sleep 3
# Split up copy and delete so we never loose files
[system.io.file]::Copy("$scandir\$pdftoprint", "$scanbackdir", $true)
[system.io.file]::Delete("$scandir\$pdftoprint")
}
}
### Define what happens when script fails
$erroraction = {
echo "$(get-date) the process crashed" | Out-File -Append "l:\files\cut\log\errorlog.txt"
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
$ErrorEvent = Register-ObjectEvent $watcher "Error" -Action $erroraction
$CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
$ListOfEvents = #(
$ErrorEvent
$CreatedEvent
)
while ($true) {
$eventMissing = $false
$ListOfEvents | % {
$e = $_
if (!(Get-Event -SourceIdentifier $e.Name -ErrorAction SilentlyContinue)) {
# Event does not exist
$eventMissing = $true
}
}
if(!$watcher || $eventMissing -eq $true) {
# deregister events
$ListOfEvents | % {
$e = $_
try {
Unregister-Event -SourceIdentifier $e.Name
} catch {
# Do Nothing
}
}
if($watcher) {
$watcher.Dispose()
$watcher = $null
} else {
# Create watcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watcherArgs.Path
$watcher.Filter = $watcherArgs.Filter
$watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
$ErrorEvent = Register-ObjectEvent $watcher "Error" -Action $erroraction
$CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
$ListOfEvents = #(
$ErrorEvent
$CreatedEvent
)
}
}
if ($watcher.EnableRaisingEvents -eq $false) {
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
}
sleep 5
}
} finally {
$ListOfEvents | % {
$e = $_
try {
Unregister-Event -SourceIdentifier $e.Name
} catch {
# Do Nothing
}
}
if($watcher) {
$watcher.Dispose();
}
}
EDIT : I was able to get it working, see below for my solution. The commenters below are correct that Powershell isn't really ideal for GUI's and threading, but it can be done.
I've got a form in Powershell that uses Start-Job to run functions in the background without freezing the GUI. My goal is to continuously check the status of those jobs for their output. I managed to use the Windows Forms Timer to check the results of the job and update the GUI accordingly.
It's all working fine, but it seems sloppy. Is this the best way to accomplish a GUI refresh? I'm relatively new to Powershell and I want to improve my coding.
Example of what I'm doing:
$jobScript = {
Start-Sleep 5
Write-Output "The job has finished running"
}
$timerScript = {
$timer.Stop()
$jobResult = Get-Job | Receive-Job -Keep
if ($jobResult) {
$btn.text = $jobResult
} else {
$timer.Start()
}
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "The job is still running"
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 100
$timer.add_Tick($timerScript)
$timer.Start()
Start-Job -ScriptBlock $jobScript
$form.ShowDialog()
Update: My solution
Using Register-ObjectEvent did not work, it seemed like it was fighting with the GUI for the thread. Instead I was able to use [System.Windows.Forms.Application]::DoEvents(). This allows the GUI to be moved around, and once it's done being moved, the thread will resume. The big caveat here is that execution is paused as long as the GUI is being moved, so if your code needs to react to the background job on a time limit, this could cause errors.
Example code block:
$jobScript =
{
Start-Sleep 5
Write-Output "The job is completed"
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "..."
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$btn.add_click({
$btn.Text = "Starting job"
$jobby = Start-Job -ScriptBlock $jobScript
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($jobby.State -eq "Completed")
$btn.Text = Get-Job | Receive-Job
})
$form.ShowDialog()
You can use events:
$job = Start-Job {sleep 3; Write-Output "Dummy job completed"}
$callback = {
Write-Host "Event Fired"
Unregister-Event -SourceIdentifier "DummyJob"
Write-Host ($job | Receive-Job)
}
$evt = Register-ObjectEvent -InputObject $job -EventName StateChanged -SourceIdentifier "DummyJob" -Action $callback
# to remove all
Get-Job | Remove-Job -Force # jobs and events
Get-EventSubscriber | Unregister-Event # events
You might want to post this on CodeReview.StackExchange.com.
I sort of hate it when people build UI's in Powershell. If you want a proper Windows forms app, just write it in C#. So I disagree with the design at its premise.
I like your impulse to move away from the polling design; you initiate the job then poll to see if it's completed. I think an event handler might be a better choice. Check out the section "Monitor a Background Job" in the article PowerShell and Events: Object Events. It's an oldie but a goodie.
I have 1809 Windows 10 box with PowerShell Core 6.1.1
Given following code sample:
function Test() {
Write-Host "Test"
}
function Invoke-Test() {
$timer = New-Object System.Timers.Timer
$timer.AutoReset = $false
$timer.Interval = 1
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
Test
}
$timer.Enabled = $true
}
If I invoke "Test" function, I get "Test" output as expected:
But if I schedule invocation with a timer, command prompt is completely messed up:
I vaguely understand that it's something related to internal "readline" and console mechanic, but is it any way to produce newline output followed by a command prompt from a timer/handle in powershell?
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Test -Action {
Test
}
$timer.Enabled = $true
PS C:\> $Subscriber = Get-EventSubscriber -SourceIdentifier Timer.Test
PS C:\> $Subscriber.action | Format-List -Property *
The command property should hold the results of your “test” function.
The url below has detailed explanations.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-eventsubscriber?view=powershell-6
I've been scratching my head on this one for several hours ... The code below triggers an action whether a notification ballon is clicked on or closed. Also I'm new to PowerShell.
Consider this code :
####### Launch as : ##########################
## powershell.exe -sta -file .\balloon.ps1 ##
##############################################
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Title = "This is the title"
$Text = "This is the text"
$EventTimeOut = 5
$balloon = New-Object System.Windows.Forms.NotifyIcon
$balloon.Icon = [System.Drawing.SystemIcons]::Information
$balloon.BalloonTipTitle = $Title
$balloon.BalloonTipText = $Text
$balloon.Visible = $True
$balloon.ShowBalloonTip(1)
Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked `
-Action {
# explorer.exe; `
Write-Host -ForeGround Green "event_BalloonTipClicked occured !"; `
# Gets rid of icon
$balloon.Dispose(); `
}|Out-Null
Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed `
-Action {
Write-Host -ForeGround Green "event_BalloonTipClosed occured !"; `
$balloon.Dispose(); `
}|Out-Null
Wait-Event event_BalloonTipClicked -TimeOut $EventTimeOut
Wait-Event event_BalloonTipClosed -TimeOut $EventTimeOut
Unregister-Event -SourceIdentifier event_BalloonTipClicked
Unregister-Event -SourceIdentifier event_BalloonTipClosed
Write-Host -ForeGround Gray "Should be empty -- start --"
Get-EventSubscriber
Write-Host -ForeGround Gray "Should be empty -- end --"
#[System.Windows.Forms.MessageBox]::Show("Done !!")
Write-Host -ForeGround Yellow " ###### END OF SCRIPT ! ######"
I would like the script to end as soon as "Register-ObjectEvent" finished triggering its actions.
However return only happens after the timeout specified in "Wait-Event", preventing further execution of code. This behavior also prevents me from turning that code into a function.
The script behaves the same way if it listens to one event only.
Any help would be much appreciated !
Tanks for your time Shay Levy. I haven't managed to use your solution properly.
I used this vbscript to launch the file in the background.
Set objShell = CreateObject("Wscript.Shell")
objShell.Run "powershell.exe -NoExit -Sta -File .\balloon.ps1",0
Unfortunately, launching the script 5 times for instance would leave me 5 running powershell instances doing nothing.
I finally realised that mixing "Register-ObjectEvent -Action" and "Wait-Event" is a definite no-no (http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/10983ec3-7aa6-4011-a87e-a30a25ab484a/)
The following code is what I was aiming. It is a synchronous approach to the problem.
###################################################
## Launch as : ##
## cmd /k powershell -Sta [-File] .\balloon.ps1 ##
###################################################
# This post put me on the right track "http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/10983ec3-7aa6-4011-a87e-a30a25ab484a/"
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Title = "This is the title"
$Text = "This is the text"
$EventTimeOut = 5
$balloon = New-Object System.Windows.Forms.NotifyIcon -Property #{
Icon = [System.Drawing.SystemIcons]::Information
BalloonTipTitle = $Title
BalloonTipText = $Text
Visible = $True
}
# Value "1" here is meaningless. $EventTimeOut will force bubble to close.
$balloon.ShowBalloonTip(1)
Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked
Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed
# "Wait-Event" pauses the script here until an event_BalloonTip* is triggered
# TimeOut is necessary or balloon and script hangs there forever.
# This could be okay but event subscription gets messed up by following script instances generating the same event names1.
$retEvent = Wait-Event event_BalloonTip* -TimeOut $EventTimeOut
# Script resumes here.
$retSourceIdentifier = $retEvent.SourceIdentifier
If ($retSourceIdentifier -eq $null){
Write-Host -ForeGround Green "TimeOut occured !"
}Else{
Write-Host -ForeGround Green "$retSourceIdentifier occured !"
}
If ($retSourceIdentifier -eq "event_BalloonTipClicked"){
explorer.exe
}
# Gets rid of icon. This is absolutely necessary, otherwise icon is stuck event if parent script/shell closes
$balloon.Dispose()
# Tidy up, This is needed if returning to parent shell.
Unregister-Event -SourceIdentifier event_BalloonTip*
Get-Event event_BalloonTip* | Remove-Event
Write-Host -ForeGround Gray "Should be empty -- start --"
Get-EventSubscriber
Write-Host -ForeGround Gray "Should be empty -- end --"
#[System.Windows.Forms.MessageBox]::Show("Done !!")
Write-Host -ForeGround Yellow " ###### END OF SCRIPT ! ######"
The script registers two event handlers. Once an event was triggered it
executes the code in its action parameter and un-register the event handler and the job it produces.
If un-register the events is not what you want you can comment out the relevant lines (see inline comment).
When you run powershell from a batch file you may want to add the -NoExit switch so the console will not close (and destroy any events and related jobs).
####### Launch as : ##########################
## powershell.exe -sta -file .\balloon.ps1 ##
##############################################
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
Add-Type -Assembly System.Windows.Forms
$Title = "This is the title"
$Text = "This is the text"
$balloon = New-Object System.Windows.Forms.NotifyIcon -Property #{
Icon = [System.Drawing.SystemIcons]::Information
BalloonTipTitle = $Title
BalloonTipText = $Text
Visible = $True
}
$balloon.ShowBalloonTip(1)
$null = Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked -Action {
Write-Host -ForeGround Green "event_BalloonTipClicked occured !"
Unregister-Event -SourceIdentifier $event.SourceIdentifier -Force
Remove-Job $event.SourceIdentifier -Force
# unregister event and remove job object
Unregister-Event -SourceIdentifier event_BalloonTipClosed -Force
Remove-Job event_BalloonTipClosed -Force
$balloon.Dispose()
}
$null = Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed -Action {
Write-Host -ForeGround Green "event_BalloonTipClosed occured !"
Unregister-Event -SourceIdentifier $event.SourceIdentifier -Force
Remove-Job $event.SourceIdentifier -Force
# unregister event and remove job object
Unregister-Event -SourceIdentifier event_BalloonTipClicked -Force
Remove-Job event_BalloonTipClicked -Force
$balloon.Dispose()
}