I have a xaml listview with a button. The listview shows all processes that have powershell* or pwsh* in them. I want to be able to kill any of the processes by clicking a button but so far nothing I've tried has worked. Here is my code so far:
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$input = @'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PowershellListView"
Title="End Processes" Height="450" Width="870">
<Grid>
<Button Name="btnEndProc" Content="End Process" HorizontalAlignment="Left" Height="45" Margin="112,341,0,0" VerticalAlignment="Top" Width="110"/>
<ListView Name="listdisk" HorizontalAlignment="Left" Height="200" Margin="12,107,-140,0" VerticalAlignment="Top" Width="840">
<ListView.View>
<GridView>
<GridViewColumn Header="Process ID" DisplayMemberBinding ="{Binding ProcessID}" Width="100"/>
<GridViewColumn Header="Name" DisplayMemberBinding ="{Binding Name}" Width="255"/>
<GridViewColumn Header="Handle Count" DisplayMemberBinding ="{Binding HandleCount}" Width="105"/>
<GridViewColumn Header="Working Set Size" DisplayMemberBinding ="{Binding Workingsetsize}" Width="100"/>
<GridViewColumn Header="Virtual Size" DisplayMemberBinding ="{Binding VirtualSize}" Width="105"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
'@
#region code
[xml]$xaml = $input
$input = $input -replace '^window.*', '<window' -replace 'mc:ignorable="d"','' -replace "x:N",'N' -replace 'x:Class="\S+"',''
#Read XAML
$xmlreader=(New-Object System.Xml.XmlNodeReader $xaml)
$xamlForm=[Windows.Markup.XamlReader]::Load($xmlReader)
$xaml.SelectNodes("//*[@Name]") | ForEach-Object -Process {
Set-variable -name ($_.Name) -Value $xamlForm.Findname($_.Name)
}
#endregion code
$cim = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'"
$procids = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'" | select-object -expandproperty ProcessId
#region XAML Controls
#endregion
$xamlForm.activate()
$listdisk.ItemsSource = $cim
$process = Get-Process -id $cim
write-host $process
$xamlForm.showdialog() | out-null
$btnEndProc.add_click({
write-host "$listdisk.items.CurrentItem.ProcessId"
$listdisk.items.currentitem.Processid.remove
[System.Windows.Forms.Application]::DoEvents()
})
As you can see, at the end I was just doing some testing with the write-hosts but they're coming up empty. $procids is returning process ids.
Help please. I've been working on this for days.
Instead of $listdisk.items.currentitem.Processid.remove
, use
Stop-Process -Force -Id $listdisk.items.currentitem.Processid
in order to kill the process corresponding to the currently selected list item showing the running PowerShell and PowerShell ISE processes.
Note: Stop-Process
forcefully kills (terminates) the target process.
taskkill.exe /pid $listdisk.items.currentitem.Processid
instead.Also, Stop-Process
terminates only the targeted process, not the entire process tree, i.e. not also any child processes (and their children) that may have been launched by it.
While powershell.exe
and pwsh.exe
themselves ensures that any child processes are also terminated, the PowerShell ISE does not do that.
To ensure that the entire process tree is terminated, make direct use of the underlying .NET APIs. Caveat: works in PowerShell (Core) 7 only:
[System.Diagnostics.Process]::GetProcessById($PID).Kill($true)
However, to ensure that $listdisk.items.currentitem
truly reflects the currently selected list item, your XAML's <ListView>
element is missing the
IsSynchronizedWithCurrentItem="True"
attribute.
A few asides:
Remove the "..."
around the Write-Host
argument (or use Write-Host "$($listdisk.items.CurrentItem.ProcessId)"
, though you do not need explicit stringification int his case; inside "..."
, expressions such as property access require enclosure in $(...)
.
[System.Windows.Forms.Application]::DoEvents()
, which, despite relating to WinForms, also works in WPF for processing pending GUI events isn't necessary here, given that killing a process executes quickly and therefore doesn't interfere perceptibly with GUI event handling.
Your code mistakenly places $btnEndProc.add_click()
after the .ShowDialog()
call, where it wouldn't have any effect, given that .ShowDialog()
shows the window modally (in a blocking fashion).
Below is a self-contained, streamlined, corrected form of your code that uses Stop-Process
to kill the selected process.
However, to be safe it uses the -WhatIf
common parameter to preview the operation. Remove -WhatIf
once you're sure the the correct process is being targeted.
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"
xmlns:local="clr-namespace:PowershellListView"
Title="End Processes" Height="450" Width="870">
<Grid>
<Button Name="btnEndProc" Content="End Process" HorizontalAlignment="Left" Height="45" Margin="112,341,0,0" VerticalAlignment="Top" Width="110"/>
<!--
NOTE THE REQUIRED USE OF IsSynchronizedWithCurrentItem="True"
Also, to keep this demo simple, SelectionMode="Single" ensures that only ONE process can be selected at time.
-->
<ListView Name="listProcesses" HorizontalAlignment="Left" Height="200" Margin="12,107,-140,0" VerticalAlignment="Top" Width="840" IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Header="Process ID" DisplayMemberBinding ="{Binding ProcessID}" Width="100"/>
<GridViewColumn Header="Name" DisplayMemberBinding ="{Binding Name}" Width="255"/>
<GridViewColumn Header="Handle Count" DisplayMemberBinding ="{Binding HandleCount}" Width="105"/>
<GridViewColumn Header="Working Set Size" DisplayMemberBinding ="{Binding Workingsetsize}" Width="100"/>
<GridViewColumn Header="Virtual Size" DisplayMemberBinding ="{Binding VirtualSize}" Width="105"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
'@ -replace '^window.*', '<window' -replace 'mc:ignorable="d"', '' -replace "x:N", 'N' -replace 'x:Class="\S+"', ''
# Parse the XAML and create PowerShell variables named for each control.
$xmlreader = (New-Object System.Xml.XmlNodeReader $xaml)
$xamlForm = [Windows.Markup.XamlReader]::Load($xmlReader)
$xaml.SelectNodes("//*[@Name]") | ForEach-Object -Process {
Set-variable -name $_.Name -Value $xamlForm.Findname($_.Name)
}
# Get the processes of interest...
# NOTE: Using a [System.Collections.ArrayList] instance allows later modification of the list of processes, which
# in turn allows removing items from the list view.
[System.Collections.ArrayList] $processes = Get-CimInstance -ClassName Win32_Process -Filter "Name like 'Powershell%' or Name like 'pwsh%'"
# ... and bind them to the list control.
$listProcesses.ItemsSource = $processes
$btnEndProc.add_click({
$selectedProcess = $listProcesses.items.CurrentItem
$selectedProcessId = $selectedProcess.ProcessId
Write-Verbose -Verbose "Forcefully terminating process with ID $selectedProcessId..."
# NOTE: -WhatIf *previews* the operation. Remove `-WhatIf` and re-execute once you're sure the operation will do what you want.
Stop-Process -Force -ID $selectedProcessId -WhatIf
# Remove the terminated process from the list view and remove the selection.
$listProcesses.items.SourceCollection.Remove($selectedProcess)
$listProcesses.items.Refresh()
$listProcesses.SelectedIndex = -1
})
# Enable the button only if a list item is selected.
$listProcesses.add_SelectionChanged({
$btnEndProc.IsEnabled = $this.SelectedIndex -ne -1
})
# Make sure that the window receives focus when it is first displayed.
$xamlForm.add_Loaded({ $this.Activate() })
# Show the WPF window modally (blocking call).
$null = $xamlForm.ShowDialog()