Add "Enter" and "Escape" key presses to Powershell WPF form - wpf

I used ISE-Sterioids template to create a simple 3 field WPF form that asks for three things:
- ID
- Email
- Reference
I have it sucessfully working with these fields when using the OK and Cancel buttons, but I would like to capter "Enter" to submit the form and "Escape" to cancel the form, but I am having difficulty adding the events.
I have tried similar code to the technet article here; but as this isn't using WPF I think I am missing something
$objForm.KeyPreview = $True
$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter")
{$x=$objTextBox.Text;$objForm.Close()}})
My code here:
#region XAML window definition
# Right-click XAML and choose WPF/Edit... to edit WPF Design
# in your favorite WPF editing tool
# Default Form Values
$123 = 'ID'
$toEmail = 'email address'
$ref = "ref"
$xaml = #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
MinWidth="200"
Width ="400"
SizeToContent="Height"
Title="Proofing script"
Topmost="True">
<Grid Margin="10,2,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- <TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="5">Please enter your details:</TextBlock> -->
<TextBlock Grid.Column="0" Grid.Row="1" Margin="5"><Run Text="Number:"/></TextBlock>
<TextBlock Grid.Column="0" Grid.Row="2" Margin="5"><Run Text="To Email :"/></TextBlock>
<TextBlock Grid.Column="0" Grid.Row="3" Margin="5"><Run Text="Salesforce Ref:"/></TextBlock>
<TextBox x:Name="TxtName" Grid.Column="1" Grid.Row="1" Margin="5"/>
<TextBox x:Name="TxtEmail" Grid.Column="1" Grid.Row="2" Margin="5"/>
<TextBox x:Name="ref" Grid.Column="1" Grid.Row="3" Margin="5"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,5,0,0" Grid.Row="4" Grid.ColumnSpan="2">
<Button x:Name="ButOk" MinWidth="80" Height="22" Margin="5" Content="OK" />
<Button x:Name="ButCancel" MinWidth="80" Height="22" Margin="5" Content="Cancel" IsCancel="True" />
</StackPanel>
</Grid>
</Window>
'#
#endregion
#region Code Behind
function Convert-XAMLtoWindow
{
param
(
[Parameter(Mandatory)]
[string]
$XAML,
[string[]]
$NamedElement=$null,
[switch]
$PassThru
)
Add-Type -AssemblyName PresentationFramework
$reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
$result = [Windows.Markup.XAMLReader]::Load($reader)
foreach($Name in $NamedElement)
{
$result | Add-Member NoteProperty -Name $Name -Value $result.FindName($Name) -Force
}
if ($PassThru)
{
$result
}
else
{
$null = $window.Dispatcher.InvokeAsync{
$result = $window.ShowDialog()
Set-Variable -Name result -Value $result -Scope 1
}.Wait()
$result
}
}
function Show-WPFWindow
{
param
(
[Parameter(Mandatory)]
[Windows.Window]
$Window
)
$result = $null
$null = $window.Dispatcher.InvokeAsync{
$result = $window.ShowDialog()
Set-Variable -Name result -Value $result -Scope 1
}.Wait()
$result
}
#endregion Code Behind
#region Convert XAML to Window
$window = Convert-XAMLtoWindow -XAML $xaml -NamedElement 'ButCancel', 'ButOk', 'ref', 'TxtEmail', 'TxtName' -PassThru
#endregion
#region Define Event Handlers
# Right-Click XAML Text and choose WPF/Attach Events to
# add more handlers
$window.ButCancel.add_Click(
{
$window.DialogResult = $false
}
)
$window.ButOk.add_Click(
{
$window.DialogResult = $true
}
)
#endregion Event Handlers
#region Manipulate Window Content
#$window.TxtName.Text = $env:username
$window.ref.Text = $ref
$window.TxtName.Text = $123
$window.TxtEmail.Text = $toEmail
$null = $window.TxtName.Focus()
#endregion
# Show Window
$result = Show-WPFWindow -Window $window
If i use ISE Steroids to add an event I get similar to this
$window.ButOk.add_KeyDown{
# remove param() block if access to event information is not required
param
(
[Parameter(Mandatory)][Object]$sender,
[Parameter(Mandatory)][Windows.Input.KeyEventArgs]$e
)
# add event code here
}

Working with key down events can be a bit tricky since these are depending on the current UIFocus, therefor your button does not act on any keydown event since it is not in the current focus scope when you´re just editing the Textboxes. Since you only plan on using this simple form, I would suggest adding an event handler to your window that handles this. So your code behind section should look something like this:
$window.add_KeyDown{
param
(
[Parameter(Mandatory)][Object]$sender,
[Parameter(Mandatory)][Windows.Input.KeyEventArgs]$e
)
if($e.Key == $Key.Return)
{
$window.DialogResult = $true
}
if($e.Key -eq $Key.Escape)
{
$window.DialogResult = $false
}
}

The accepted answer didn't work for me yet but it helped me a lot so I'm answering here.
I made a simple form with https://poshgui.com/Editor adding the code in the answer and got the error message:
System.Management.Automation.RuntimeException: Unable to find type [Windows.Input.KeyEventArgs].
My TextBox element has got the focus, so I created the event for it:
TextBoxSearchInput.Add_KeyDown{
param (
[Parameter(Mandatory)][Object]$sender,
[Parameter(Mandatory)][System.Windows.Forms.KeyEventArgs]$e
)
if($e.Key -eq $Key.Return){
$FormOkButton.PerformClick()
}
if($e.Key -eq $Key.Escape){
$Form.close()
}
}
There may be a better way than "PerformClick" but for now it works.

I know this thread here is old but I was also sucessfull with this:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Build Form
$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Any Name"
$Form.Size = New-Object System.Drawing.Size(700,150) #use any size
$Form.StartPosition = "CenterScreen" # I prefer this
$Form.Topmost = $true
$form.KeyPreview = $true #This is the important part
$form.Add_KeyDown{
param (
[Parameter(Mandatory)][Object]$sender,
[Parameter(Mandatory)][System.Windows.Forms.KeyEventArgs]$e
)
if($_.KeyCode -eq "Escape"){
$Form.close()
}
}

Related

Generating/ connecting WPF tabitems dynamically in powershell

Not sure what the best way to word the question is, but how can I dynamically generate all these elements and assign an add_click on and to the generated elements?
I am using a foreach loop in powershell to build a tabbed WPF window based of the contents of a powershell object.
Everything works up until adding the control to the tabbutton to allow it to display the tabitem. I assume it is because in the foreach I am using the same variable name for each element, and I am not sure how to specify which tabitem is assigned to each tabbutton.
Additionally, I am looking for a powershell only solution.
I thought I might be able to use New-Variable/Get-Variable to build the variables with the counter included in the variable name but the add_click still could not call the tabitem.
Added 20 Jan 2023
Add-Type -AssemblyName PresentationCore, PresentationFramework, System.Windows.Forms, System.Drawing
#Region XAML for base gui
$Xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="1000"
Height="700"
Margin="0,0,0,0"
Background="#35333a"
BorderBrush="#666374"
Foreground="#514e5d"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
AllowsTransparency="True"
ResizeMode="CanResize">
<Grid Background="#241b2f" Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="3*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="6*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="6" VerticalAlignment="Center" HorizontalAlignment="Center" Height="20" Width="20" Background="white" BorderThickness="0,0,0,0" Name="CloseButton" />
<ScrollViewer VerticalScrollBarVisibility="Hidden" Grid.Row="1" Grid.RowSpan="4">
<StackPanel Name="QuestionTabsPanel">
</StackPanel>
</ScrollViewer>
<TabControl Background="#241b2f" BorderThickness="0,0,0,0" Padding="-1" Grid.Row="1" Grid.Column="2" Name="TabControlPanel" SelectedIndex="0">
</TabControl>
</Grid>
</Window>
"#
#EndRegion
# get questions from json and convert them to objects
$Json = get-content 'C:\Users\d.eckhout.mil\Desktop\Files\questions.json' | ConvertFrom-Json
$Questions = $Json | foreach{
[pscustomobject]#{
QuestionText = $_.questiontext
Image = $_.image
Answers = $_.answers
CorrectAnswer = $_.correctanswer
Explaination = $_.explaination
}
}
# counting variables
$QuestionsCorrect = 0
$QuestionNumber = 1
# randomize questions
$Questions = $Questions | Sort-Object {Get-Random}
# build window with base xaml
$Window = [Windows.Markup.XamlReader]::Parse($Xaml)
[xml]$xml = $Xaml
$xml.SelectNodes("//*[#Name]") | ForEach-Object { Set-Variable -Name $_.Name -Value $Window.FindName($_.Name) }
# custom close button event
$CloseButton.Add_Click({$Window.Close()})
# Make window moveable
$Window.Add_MouseDown({
if ($_.ChangedButton -eq 'Left'){
$this.DragMove()
}
})
# loop through our question objects and build a tab for each
foreach ($Question in $Questions){
#Region Add a tab button for the question to sidebar
$TabButton = New-Object System.Windows.Controls.Button
$TabButton.Name = "TabButton$QuestionNumber"
$TabButton.content = "$QuestionNumber"
$TabButton.Height="40"
$TabButton.Background="#241b2f"
$TabButton.BorderThickness="0,0,0,0"
$TabButton.Foreground="#ffffff"
$QuestionTabsPanel.Children.Add($TabButton)
#EndRegion
#Region Create tab content panel for the question
$TabContent = New-Object System.Windows.Controls.TabItem
#$TabContent.Visibility="Collapsed"
$TabContent.Name="TabContent$QuestionNumber"
$TabControlPanel.Items.Add($TabContent)
#EndRegion
$TabButton.Add_Click({
$sauceButton=$_ #the button this click event was triggered by
$sauceButtonContent=[Int]$sauceButton.Content #its content casted as Int
$tabIndexToSelect=$sauceButtonContent-1 # the first tabItem has index 0
$TabControlPanel.SelectedIndex = $tabIndexToSelect
})
#Region Add grid to tab content area...grid lets us better control placement of children
$TabContentStack = New-Object System.Windows.Controls.StackPanel
$TabContentStack.Name="TabStack$QuestionNumber"
$TabContent.Content = ($TabContentStack)
#EndRegion
#Region Add question text to grid
$TabContentQuestionText = New-Object System.Windows.Controls.TextBlock
$TabContentQuestionText.HorizontalAlignment="Center"
$TabContentQuestionText.VerticalAlignment="Top"
$TabContentQuestionText.TextWrapping="Wrap"
$TabContentQuestionText.Text=$Question.QuestionText
$TabContentQuestionText.FontSize="14"
$TabContentQuestionText.Height="21"
$TabContentQuestionText.Foreground="#ffffff"
$TabContentStack.Children.Add($TabContentQuestionText)
#EndRegion
$QuestionNumber++
}
$Window.ShowDialog()
[
{
"questiontext": "lorem ipsum?",
"answers": [
"first ipsum",
"second ipsum",
"third ipsum",
"fourth ipsum"
],
"correctanswer": "lorem ipsum",
"explaination": "lorem ipsum"
},
{
"questiontext": "lorem ipsum?",
"answers": [
"first ipsum",
"second ipsum",
"third ipsum",
"fourth ipsum"
],
"correctanswer": "lorem ipsum",
"explaination": "lorem ipsum"
}
]
picture of gui running
You can use selecedIndex with the content of the button your click event originates from:
$TabButton.Add_Click({
$sauceButton=$_ #the button this click event was triggered by
$sauceButtonContent=[Int]$sauceButton.Content #its content casted as Int
$tabIndexToSelect=$sauceButtonContent-1 # the first tabItem has index 0
$TabControlPanel.SelectedIndex = $tabIndexToSelect
})
However I recommend to create a datacontext, add ps-objects to that context for each tabitem and bind it to your tabcontrol, and set the property itemsource to "binging" simular to this example: Powershell XAML - Use XAML datatemplate to create new tabitems

Get clicked MenuItem header value from datagrid context menu in WPF/PowerShell

In a WPF DataGrid via PowerShell, I have added a context menu and MenuItems from an Array. I would like to get the header value of clicked MenuItem to handle the click event further. I had tried add_Click event to the MenuItem object but it does return any value. I'm looking for some ideas to get the Header value in this scenario (or) if there is any different better approach to achieve this goal. Thanks in advance.
[xml]$inputXML=#"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test"
Title="MainWindow" Height="275" Width="375">
<Window.InputBindings>
<KeyBinding x:Name="keyTest" Modifiers="Control" Key="N" Command="{Binding CreateCustomerCommand}" />
</Window.InputBindings>
<Grid>
<DataGrid x:Name="dg" Margin="5,5,0,0" Height="250" Width="350" ColumnWidth="Auto" AlternationCount="1" IsReadOnly="True" SelectionMode="Extended" SelectionUnit="Cell" Background="White" ClipboardCopyMode="IncludeHeader" >
<DataGrid.ContextMenu >
<ContextMenu x:Name="cxmenu" />
</DataGrid.ContextMenu>
</DataGrid>
</Grid>
</Window>
"#
[xml]$XAML = $inputXML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load($reader)
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'x:Name')]]") | %{Set-Variable -Name "$($_.Name)" -Value $Window.FindName($_.Name)}
#sample data
$DataSet = New-Object System.Data.DataSet
$Table = $DataSet.Tables.Add("Table")
$Properties = #("Country","Capital","Population")
$Properties | foreach {
$Column = New-Object System.Data.DataColumn($_)
$Table.Columns.Add($Column)
}
$Null=$Table.Rows.Add("USA","Washington, D.C","658,893")
$Null=$Table.Rows.Add("China PR","Beijing","20,693,000")
$Null=$Table.Rows.Add("India","New Delhi","16,787,949")
$Null=$Table.Rows.Add("Japan","Tokyo","13,189,000")
$Null=$Table.Rows.Add("Philippines","Manila","12,877,253")
$Null=$Table.Rows.Add("Russia","Moscow","11,541,000")
$Null=$Table.Rows.Add("Egypt","Cairo","10,230,350")
#populate datagrid
$DataView = New-Object System.Data.DataView($Table)
$array = New-Object System.Collections.ArrayList
[void] $array.AddRange($DataView)
$dg.clear()
$dg.ItemsSource = $array
$dg.IsReadOnly = $true
#add MenuItem to context menu
$cxMenuitem = New-Object Windows.Controls.MenuItem
$header = ('Test1','Test2','Test3')
for ($i = 0; $i -le $header.Count -1; $i++)
{
$cxMenuitem.Header = [string]$header[$i]
$cxMenu.Items.Add($cxMenuitem.Header)
}
$cxMenuitem.Add_Click({
#Get the Header of the clicked MenuItem from Context Manu here for further handling
[System.Windows.MessageBox]::Show($cxMenu.Items)
})
#Display Form
$Window.ShowDialog() | Out-Null
You should be able to cast $args[0] to a MenuItem and access its Header property directly:
$cxMenuitem.Add_Click({
$sender = [System.Windows.Controls.MenuItem]$args[0]
# Use $sender.Header ...
})

How do you display options from an array in PowerShell when creating a ComboBox in XAML

I am creating a wpf ui in powershell using xaml. I am attempting to fill my combo box with an array and whenever I load the window/combobox, no options appear for me to select from. I have tried a few methods to resolve this, including using the ItemsSource element in xaml scriptblock, using DisplayMemberPath inside and outside the xaml block, creating a function to help display the window and then passing the variables from inside the window to the scriptblock.
Please let me know what I am doing wrong - any and all help is appreciated.
Do {
Connect-AzAccount -ErrorAction SilentlyContinue
$AzureAccount = Get-AzContext
$CurrentAccount = $AzureAccount.Account.Id
$CurrentAccount
} While ([String]$AzureAccount::isnullorempty)
Do {
# Select Subscription
Try {
$azureSubscription = (Get-AzSubscription | Sort-Object Name | Out-GridView -Title "Choose your Azure subscription and click OK." -PassThru)
Write-host "Switching to Azure subscription: $($azureSubscription.Name)" -ForegroundColor Green;
$azureSubscriptionInfo = Select-AzSubscription -SubscriptionId $azureSubscription.Id
} Catch {
Write-Output "Script Terminated"
}
# Select Location
Try {
$azureLocation = (Get-AzLocation | Sort-Object Name | Out-GridView -Title "Chosose your Azure location and click OK." -PassThru)
Write-Host "Switching to Azure location: $($azureLocation.DisplayName)" -ForegroundColor Green
} Catch {
Write-Output "Script Terminated"
}
if ([String]$azureSubscription::isnullorempty) {
$Primary = "False"
} elseif ([String]$azureLocation::isnullorempty) {
$Primary = "False"
}
} while ($Primary -eq "False")
# Get Virtual Networks and associated Subnets
$vnet = Get-AzVirtualNetwork
$vnetList = #()
$subnetsList = #()
foreach ($vn in $vnet) {
if ($vnet.Location -eq $azureLocation.Location) {
$vnetList += $vnet.Name
$subnetsList += $vnet.Subnets.Name }
}
$vnetList = $vnetList | Where-Object { $_ } | Select-Object -Unique
$subnetsList = $subnetsList | Where-Object { $_ } | Select-Object -Unique
Add-Type -AssemblyName PresentationFramework
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" Title="Virtual Machine Deployment Tool" WindowStartupLocation="CenterScreen"
SizeToContent="WidthAndHeight" >
<Grid x:Name="Grid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="10">
<ComboBox x:Name="vnselect" Margin="5" Width="100" />
</StackPanel>
</Grid>
</Window>
"#
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$window.ShowDialog() | Out-Null
$vnselect = $window.FindName("vnselect")
foreach ($item in $vnetList) {
$vnselect.Items.Add($item)
}
The code that added the items to the ComboBox should be placed before $window.Showdialog(). That's the resolution.

Powershell script issue handling drag and drop effects for WPF listbox

I am limiting drag and drop actions to a WPF listbox control in Powershell to only allow text files to be dropped. I would like to use the System.Windows.DragDropEffects property to prevent the drop action on the DragEnter event as it also changes the mouse cursor providing user feedback for the denied drop action. I can still limit the action taken on the dropped file by validating the file extension on the Drop event. But I would prefer to prevent the drop action all together for smoother user interaction.
In debugging I've verified that the DragDropEffect property is being set correctly, however the event handler does not seem to reflect the change. I believe it might be a limitation trying to use the DragEventArgs Class to monitor the event through the Powershell pipeline.
Code for the WPF listbox DragEnter event is below. I noticed that the object passed in the $_ pipeline is of the System.Windows.DragEventArgs class.
$listbox.Add_DragEnter({
if ($_.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) {
foreach ($filename in $_.Data.GetData([Windows.Forms.DataFormats]::FileDrop)) {
if(([System.IO.Path]::GetExtension($filename).ToUpper() -eq ".TXT")) {
$_.Effects = [System.Windows.DragDropEffects]::All
Write-Host 'Dropfile is a .TXT'
}
else {
$_.Effects = [System.Windows.DragDropEffects]::None
Write-Host 'Dropfile is NOT a .TXT'
}
}
}
})
Setting DragDropEffect property using WinForms listbox works as expected. The mouse changes and the drop event is prevented. However here, the object passed in the $_ pipeline is of the System.Windows.Forms.DragEventArgs class.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object Windows.Forms.Form
$listbox = New-Object Windows.Forms.ListBox
$listbox.AllowDrop = $true
$listbox.Add_DragEnter({
$_.Effect = [Windows.Forms.DragDropEffects]::None
})
$form.Controls.Add($listbox)
$form.ShowDialog()
Full test code below for WPF:
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
[xml]$xaml = #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Remote Execution Toolkit" Height="300" Width="300">
<Grid>
<ListBox x:Name="listBox" AllowDrop="True" Height="250" HorizontalAlignment="Center" Width="250">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Path=IsSelected}">
<TextBlock Text="{Binding}" TextAlignment="Left" Width="Auto" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
'#
# Load XAML Reader
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )
# Map XAML Controls
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force
}
# Drag Event to validate file extensions for drop effect
$listbox.Add_DragEnter({
if ($_.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) {
foreach ($filename in $_.Data.GetData([Windows.Forms.DataFormats]::FileDrop)) {
if(([System.IO.Path]::GetExtension($filename).ToUpper() -eq ".TXT")) {
$_.Effects = [System.Windows.DragDropEffects]::All
Write-Host 'Dropfile is a .TXT'
}
else {
$_.Effects = [System.Windows.DragDropEffects]::None
Write-Host 'Dropfile is NOT a .TXT'
}
}
}
})
$Window.ShowDialog()
Any thoughts or suggestions is appreciated!!
After tons of Google-Foo, running Snoop WPF to monitor events, and trial and error, I realized that I was simply subscribing to the wrong Drag Event. To achieve the result of continuously displaying the operation not allowed cursor, you must use the DragOver Event.
$listbox.Add_DragOver({
...
$_.Effects = [System.Windows.DragDropEffects]::None
...
})
Apparently, when using WPF code in Powershell, the DragEnter event only fires once allowing the cursor to change back, whereas the DragOver event continuously fires while the mouse is over the control maintaining display of the operation not allowed cursor.
Hope this is able to save another fellow developer down the road save some time.
This worked for me by adding $_.Handled = $true after changing the $_.Effects.
This was taken from here: https://stackoverflow.com/a/44321363/8262102
# Drag and drop UI example to a list box.
Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration
[xml]$xaml = #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test Drop Form" Height="300" Width="500">
<Grid>
<ListBox x:Name="listBox" AllowDrop="True" Height="250" HorizontalAlignment="Center" Width="475">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}, Path=IsSelected}">
<TextBlock Text="{Binding}" TextAlignment="Left" Width="Auto" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
'#
# Load XAML Reader
$reader = New-Object System.Xml.XmlNodeReader $xaml
$Form = [Windows.Markup.XamlReader]::Load($reader)
# Map XAML Controls
$formNamedNodes = $xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | Sort
$formNamedNodes | ForEach-Object {
Set-Variable -Name $_.Name -Value $Form.FindName($_.Name) # Set the variable names to the same as that of the controls.
}
# Drop event to add the files to the list box.
$listbox.Add_Drop({
if ($_.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) {
foreach ($filename in $_.Data.GetData([Windows.Forms.DataFormats]::FileDrop)) {
if (([System.IO.Path]::GetExtension($filename).ToUpper() -eq ".TXT")) {
Write-Host "Dropped file extension: $filename is .TXT"
$listBox.Items.Add($filename)
}
else {
Write-Host "Dropped file extension: $filename is NOT .TXT"
}
}
}
})
# The DragOver event is there to handle changing the dropped effects.
$listbox.Add_DragOver({
if ($_.Data.GetDataPresent([Windows.Forms.DataFormats]::FileDrop)) {
foreach ($filename in $_.Data.GetData([Windows.Forms.DataFormats]::FileDrop)) {
if (([System.IO.Path]::GetExtension($filename).ToUpper() -eq ".TXT")) {
$_.Effects = [System.Windows.DragDropEffects]::All
Write-Host "$filename is a .TXT"
}
else {
$_.Effects = [System.Windows.DragDropEffects]::None
Write-Host "$filename is NOT a .TXT"
}
$_.Handled = $true # This is there to handle the effect. This needs to be below $_.Effect.
}
}
})
$Form.WindowStartupLocation = "CenterScreen"
$Form.ShowDialog()

Write PowerShell Output (as it happens) to WPF UI Control

I've been reading blogs about writing to the UI from different runspaces (http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/).
I'm basically trying to make it so I can click a button in the UI and run a PowerShell script and capture the output of that script as it happens and update the WPF UI control without freezing up the UI.
I've tried a basic example of just writing some output directly, but it seems to hang the UI. I'm using runspaces and dispatcher, but I seem to be stuck on something.
Any ideas?
Thanks.
Add-Type –assemblyName PresentationFramework
Add-Type –assemblyName PresentationCore
Add-Type –assemblyName WindowsBase
$uiHash = [hashtable]::Synchronized(#{})
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable('uiHash',$uiHash)
$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" Title="Patcher" Height="350" Width="525" Topmost="True">
<Grid>
<Label Content="A Builds" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="88" RenderTransformOrigin="0.191,0.566"/>
<ListBox HorizontalAlignment="Left" Height="269" Margin="10,41,0,0" VerticalAlignment="Top" Width="88"/>
<Label Content="New Build" HorizontalAlignment="Left" Margin="387,10,0,0" VerticalAlignment="Top"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="387,41,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<Label Content="B Builds" HorizontalAlignment="Left" Margin="117,10,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.528,-0.672"/>
<ListBox HorizontalAlignment="Left" Height="269" Margin="103,41,0,0" VerticalAlignment="Top" Width="88"/>
<Label Content="C Builds" HorizontalAlignment="Left" Margin="185,10,0,0" VerticalAlignment="Top"/>
<ListBox HorizontalAlignment="Left" Height="269" Margin="196,41,0,0" VerticalAlignment="Top" Width="88"/>
<Button x:Name="PatchButton" Content="Patch!" HorizontalAlignment="Left" Margin="426,268,0,0" VerticalAlignment="Top" Width="75"/>
<RichTextBox x:Name="OutputTextBox" HorizontalAlignment="Left" Height="194" Margin="289,69,0,0" VerticalAlignment="Top" Width="218">
<FlowDocument>
<Paragraph>
<Run Text=""/>
</Paragraph>
</FlowDocument>
</RichTextBox>
</Grid>
</Window>
"#
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$uiHash.Window = [Windows.Markup.XamlReader]::Load($reader)
$uiHash.Button = $uiHash.Window.FindName("PatchButton")
$uiHash.OutputTextBox = $uiHash.Window.FindName("OutputTextBox")
$uiHash.OutputTextBox.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler] {$uiHash.OutputTextBox.UpdateLayout()}, $null, $null)
$uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
# The next line fails (null-valued expression)
<#
$uiHash.OutputTextBox.Dispatcher.Invoke("Normal", [action]{
for ($i = 0; $i -lt 10000; $++) {
$uiHash.OutputTextBox.AppendText("hi")
}
})#>
Out of curiosity, have you considered that instead of PowerShell firing up WPF and displaying some output, it should be the other way around?
Perhaps you should be invoking PowerShell from within a WPF application, and capturing the output to be displayed?
http://www.codeproject.com/Articles/18229/How-to-run-PowerShell-scripts-from-C
I ran into the same question and seen quiet some suggestions that either refer to C# classes or examples where the UI is placed in the worker instead of the actual worker task.
Anyway, after spending quite some time I figured it out:
Add-Type -AssemblyName PresentationFramework
$Xaml = New-Object System.Xml.XmlNodeReader([XML]#"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Test" WindowStartupLocation = "CenterScreen" ShowInTaskbar = "True">
<Grid>
<TextBox x:Name = "TextBox"/>
</Grid>
</Window>
"#)
Function Start-Worker {
$TextBox = $Window.FindName("TextBox")
$SyncHash = [hashtable]::Synchronized(#{Window = $Window; TextBox = $TextBox})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
for($Progress=1; $Progress -le 10; $Progress++){
$SyncHash.Window.Dispatcher.Invoke([action]{$SyncHash.TextBox.AppendText($Progress)})
Start-Sleep 1 # Some background work
}
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
$Window = [Windows.Markup.XamlReader]::Load($Xaml) # Requires -STA mode
$Window.Add_Loaded({Start-Worker})
[Void]$Window.ShowDialog()
For a windows control example see: PowerShell: Job Event Action with Form not executed
I made some adjustments in your code. I wouldn't run your For loop within the dispatcher and instead would run the dispatcher block in the For loop. I tested and it didn't seem to lock up for me (had some moments where the performance wasn't the best, but that was expected) and kept writing to the window.
for ($i = 0; $i -lt 10000; $i++) {
$uiHash.OutputTextBox.Dispatcher.Invoke("Normal", [action]{
$uiHash.OutputTextBox.AppendText("hi")
})
}
This may be a little late but try downloading WPFRunspace , which provides a Powershell backgroundworker cmdlet for WPF and Win Forms GUI scripts that will execute in the background and update your GUI either as output is received or on completion.

Resources