powershellevent-handlingtoast

How to consume a click-event in a Toast-Message?


I made a powershell code that shows a toast-message with a "yes/no" option. Now I am looking for a way to handle that click-event properly. See below the code I have so far:

cls
Remove-Variable * -ea 0
$ErrorActionPreference = 'stop'

Add-Type -AssemblyName System.Runtime.WindowsRuntime
$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]

# now lets define a toast-message:
$toastXml = [Windows.Data.Xml.Dom.XmlDocument]::new()
$xmlString = @"
  <toast launch = "Test1" scenario='alarm'>
    <visual>
      <binding template="ToastGeneric">
        <text>Title</text>
        <text>Message</text>
      </binding>
    </visual>
    <audio src="ms-winsoundevent:Notification.Looping.Alarm" />
    <actions>
        <action content="yes" arguments="yes" />
        <action content="no"  arguments="no"  />
    </actions>
  </toast>
"@
$toastXml.LoadXml($XmlString)
$toast = [Windows.UI.Notifications.ToastNotification]::new($toastXml)
$appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
$notify = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId)
$notify.Show($toast)

Here I need a way to wait for the event, when the users clicks the "yes/no" buttons in the toast (or the "x" to close all). It should work without an external DLL, but using e.g. "add_Activated" and an eventHandler like in this c# code: https://github.com/david-risney/PoshWinRT/blob/master/PoshWinRT/EventWrapper.cs

I thought it should be similar like for the other cs-Wrapper from that PoshWinRT project, which I could finally convert into this:

# replacement for PoshWinRT - AsyncOperationWrapper:
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = foreach($method in [System.WindowsRuntimeSystemExtensions].GetMethods()) {
    if ($method.name -ne 'AsTask') {continue}
    if ($method.GetParameters().Count -ne 1) {continue}
    if ($method.GetParameters().ParameterType.Name -ne 'IAsyncOperation`1') {continue}
    $method
    break
}

function Await($WinRtTask, $ResultType) {
    $asTask  = $asTaskGeneric.MakeGenericMethod($ResultType)
    $netTask = $asTask.Invoke($null, @($WinRtTask))
    $null    = $netTask.Wait(-1)
    $netTask.Result
}

# sample for async operation:
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
$file = 'c:\windows\notepad.exe'
$info = await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($file)) ([Windows.Storage.StorageFile])
$info

Unfortunately, the coding for that eventWrapper seesm to be a bit more tricky. These are the code-snippets that I could not bring to life, but they may point into the right direction:

$method  = [Windows.UI.Notifications.ToastNotification].GetMethod('add_Activated')
$handler = [Windows.Foundation.TypedEventHandler[Windows.UI.Notifications.ToastNotification,System.Object]]::new($toast, $intPtr1)
$handler = [System.EventHandler]::new($toast, $intPtr2)

Especially the second IntPtr-parameter required for creating an eventHandler confuses me totally.

Any input is more than welcome. Many thanks.


Solution

  • After spending some days of investigations and testing the different options I am now happy to finally share a Powershell code-snippet which is able to react on any button being pressed in a Toast-GUI. Here the code...

    Param($data)
    
    cls
    $ErrorActionPreference = 'stop'
    
    # here we handle any input-parameter coming from the below Toast-Action:
    if ($data) {
        # (optional) show the value of the parameter for 5 seconds:
        $objShell = New-Object -ComObject "WScript.Shell"
        $objShell.Popup("Parameter from Toast-GUI: $data", 5)
    
        # insert your own code here...
    
    }
    
    # make sure not to run into a loop:
    if ($data) {break}
    
    # define global variables:
    $temp = $env:Temp
    $launchFile = "$temp\ToastAction.exe"
    
    # make sure we re-create a new launchFile:
    if (Test-Path $launchFile -PathType Leaf){Remove-Item $launchFile -Force}
    if (Test-Path $launchFile -PathType Leaf){break}
    
    # get the full path of this script:
    $script = $myInvocation.MyCommand.Definition
    if (!$script) {$script = $psISE.CurrentFile.Fullpath}
    
    # create the VBS-file as a hidden launcher for Powershell:
    $vbs = @"
    set objWsh = CreateObject("WScript.Shell")
    powershell = objWsh.ExpandEnvironmentStrings("%windir%\system32\WindowsPowershell\v1.0\powershell.exe")
    set objFs  = CreateObject("Scripting.FileSystemObject")
    if WScript.Arguments.Count > 0 Then
       arg = " """ + wscript.arguments(0) + """"
       objWsh.Run powershell + " -NoP -NonI -W Hidden -Exec Bypass -File ""$script""" + arg, 0
       wscript.sleep 1000
    end if
    "@
    $vbs | out-file -LiteralPath "$temp\start.vbs" -Encoding ascii -Force
    
    # create an IEXPRESS SED-file:
    $SedFile = [System.IO.Path]::GetTempFileName()
    $body = @"
    [Version]
    Class=IEXPRESS
    SEDVersion=3
    [Options]
    ShowInstallProgramWindow=0
    HideExtractAnimation=1
    UseLongFileName=1
    RebootMode=N
    TargetName=$launchFile
    FriendlyName=ToastAction
    AppLaunched=wscript.exe /b
    PostInstallCmd=<None>
    SourceFiles=SourceFiles
    [Strings]
    FILE0="start.vbs"
    [SourceFiles]
    SourceFiles0=$temp\
    [SourceFiles0]
    %FILE0%=
    "@
    $body | out-file -LiteralPath $SedFile -Encoding ascii -Force
    
    # create an executable via IEXPRESS as a target for our custom protocol:
    $fso = New-Object -ComObject Scripting.FileSystemObject
    $shortName = $fso.getfile($SedFile).ShortPath
    start-process -FilePath "$env:windir\SYstem32\iexpress.exe" -ArgumentList "/N $shortName /Q" -Wait
    
    # cleanup temporary files:
    Remove-Item -LiteralPath $SedFile
    Remove-Item -LiteralPath "$temp\start.vbs"
    
    # register launchFile as a handler for the custom protocol:
    $protocolName = "ToastAction"
    $regPath = "HKCU:\Software\Classes\$protocolName"
    $fullName = [System.IO.FileInfo]::new($launchfile).FullName
    New-Item "$regPath\shell\open\command" -Force -ErrorAction SilentlyContinue | Out-Null
    New-ItemProperty -LiteralPath $regPath -Name 'URL Protocol' -Value '' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
    New-ItemProperty -LiteralPath $regPath -Name '(default)' -Value "URL:$($protocolName)" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
    New-ItemProperty -LiteralPath "$regPath\shell\open\command" -Name '(default)' -Value """$fullname"" /C:""wscript start.vbs """"%1""""""" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
    
    Add-Type -AssemblyName System.Runtime.WindowsRuntime
    $null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
    $null = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
    
    # now lets define a toast-message:
    $toastXml = [Windows.Data.Xml.Dom.XmlDocument]::new()
    
    # here comes the toast-message using the custom protocol:
    $xmlString = @"
    <toast scenario="reminder">
        <visual>
            <binding template="ToastGeneric">
                <image placement="hero" src="file:///C:\Windows\Web\Wallpaper\Spotlight\img50.jpg"/>
                <image id="1" placement="appLogoOverride" hint-crop="circle" src="file:///C:\Windows\System32\WindowsSecurityIcon.png"/>
                <text>Good evening</text>
                <text placement="attribution">www.mycompany.com</text>
                <group>
                    <subgroup>
                        <text hint-style="title" hint-wrap="true">Make your choice!</text>
                    </subgroup>
                </group>
                <group>
                    <subgroup>
                        <text hint-style="body" hint-wrap="true">Take the blue Pill and remain a slave to the illusion. </text>
                    </subgroup>
                </group>
                <group>
                    <subgroup>
                        <text hint-style="body" hint-wrap="true">Take the red Pill and become a slave to reality.</text>
                    </subgroup>
                </group>
            </binding>
        </visual>
        <actions>
            <action activationType="protocol" arguments="ToastAction://bluePill?value=123" content="Blue Pill"/>
            <action activationType="protocol" arguments="ToastAction://redPill?value=456"  content="Red Pill"/>
            <action activationType="system" arguments="dismiss" content="Dismiss"/>
        </actions>
    </toast>
    "@
    
    # (optional) set custom Toast app-Id:
    $appId = 'Toast.MyCompany.App'
    $AppDisplayName = 'My User Notification'
    $IconUri = "C:\Windows\System32\InputSystemToastIcon.png"
    $RegPath = "HKCU:\Software\Classes\AppUserModelId\$appId"
    if (!(Test-Path $RegPath)) {$null = New-Item -Path $AppRegPath -Name $appId -Force}
    $null = New-ItemProperty -Path $RegPath -Name DisplayName -Value 'My User Notification' -PropertyType String -Force
    $null = New-ItemProperty -Path $RegPath -Name ShowInSettings -Value 0 -PropertyType DWORD -Force
    $null = New-ItemProperty -Path $RegPath -Name IconUri -Value $IconUri -PropertyType ExpandString -Force
    $null = New-ItemProperty -Path $RegPath -Name IconBackgroundColor -Value 0 -PropertyType ExpandString -Force
    
    # create/launch the Toast-GUI:
    $toastXml.LoadXml($XmlString)
    $toast  = [Windows.UI.Notifications.ToastNotification]::new($toastXml)
    $toast.ExpirationTime = [datetime]::Now.AddSeconds(30)
    $notify = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId)
    $notify.Show($toast) 
    

    A custom URL protocol can only trigger a batch-file or an executable. And because the batch file will always result into a CMD-window popping up, I decided to create a VBS-file as a script-launcher and I do "compile" that VBS-file via windows-integrated tool "iexpress" into a standalone executable.

    It sounds more complex than it is at the end and finally that was the solution I was always looking for ;-)