Out-GridView
is one of the most popular output cmdlets, and most often it is used to simply display piped data in an extra window:
Get-Process | Out-GridView
With just a little extra effort, Out-GridView
can do so much more and be turned into a truly universal selection dialog. If you’re in a rush, check out Out-GridViewEx
at the end of this article: a fully configurable gridview dialog where you can choose which properties to show.
Enabling Selection Dialog
To turn Out-GridView
into a general purpose selection dialog, add the parameter -OutputType:
# show all processes and allow user to select one or more (hold CTRL to select multiple):
$result = Get-Process | Out-GridView -OutputType Multiple
# return selected processes:
$result
Use the parameter -Title to show text in the dialog title bar. This is a service stopper that stops the service the user selects:
Get-Service | Where-Object CanStop | Out-GridView -Title 'Select Service To Stop' -OutputMode Single | Stop-Service -WhatIf
Allow Single Or Multiple Selection?
Out-GridView
can be restricted to exactly one selection, or allow multiple selections (hold Ctrl
to select multiple items):
Parameter | Value | Description |
---|---|---|
-OutputMode | Single | exactly one selection |
-OutputMode | Multiple | any number of selected items |
-PassThru | any number of selected items |
Serious: Selection Bug
There is one long-standing bug in Out-GridView
that can bite you when you use it as a selection dialog: the buttons to select output in its lower right corner work only once Out-GridView
has received all data. So if the command that is piping data into the grid hasn’t completed yet, clicking OK will not return anything:
# simulate a command that takes a long time to emit data
$received = 1..10 | ForEach-Object {
$_
Start-Sleep -Seconds 1
} |
# pipe output to the grid
Out-GridView -Title 'Click OK before all ten numbers are received to see the effect' -OutputMode Multiple
# the remainder of the script is skipped when you click "OK" too quickly:
[int]$count = $received.Count
"Received $count elements: $received"
When you run this code, you can see the real-time nature of Out-GridView
: it starts displaying results from the upstream cmdlets as they are emitted. That’s good.
Scripts Abort Unexpectedly
However, when you click OK before all ten numbers are received, Out-GridView
not only returns nothing but also aborts the entire script. Any command after the call to Out-GridView
is skipped. Did you know that? It affects both Windows PowerShell and PowerShell 7.
Workaround
To work around this bug, make sure all content is delivered to Out-GridView
before showing the grid. For slow commands, one solution is to skip the real-time feed and instead collect all data before sending it to Out-GridView
. The easiest way is to add Sort-Object
:
# simulate a command that takes a long time to emit data
$received = 1..10 | ForEach-Object {
$_
Start-Sleep -Seconds 1
} |
# add Sort-Object to collect all data before passing it on to
# the grid:
Sort-Object |
# pipe output to the grid
Out-GridView -Title 'Click OK before all ten numbers are received to see the effect' -OutputMode Multiple
# the remainder of the script is skipped when you click "OK" too quickly:
[int]$count = $received.Count
"Received $count elements: $received"
The flip side of course is that now you have to wait for all data to be collected before the grid opens, plus you are losing all the memory-efficiency the PowerShell pipeline normally provides.
Secret GridView Customizations
Out-GridView
is easy to use and can provide all kinds of general purpose selection dialogs. However, because of the way how Out-GridView
works, it often is rather ugly and can even confuse the user.
That’s because Out-GridView
typically shows a lot of properties, including some that aren’t helpful for the user that needs to make a choice. And there doesn’t seem to be a good way to restrict and adjust the properties displayed in the grid.
Which is actually completely untrue. There are awesome ways to customize how Out-GridView
displays data. They are just not very widely known. So I’d like to walk you through these capabilities next.
Starting With An Ugly Tool…
Let’s start with a powerful but ugly tool that by itself is worth looking at some clever coding tricks, and turn it into a much prettier tool at the end of this article.
Here is what the tool does:
The code below opens a gridview with all running main processes (those that have a window) and lets the user select one or more of them (hold Ctrl
to select more than one).
The tool then sends a gentle close notification to the selected processes, so if a process contains unsaved data, the user gets the chance to save it.
The tool then waits another 10 seconds for the selected processes to actually close and shows a progress bar while waiting. If a process isn’t closed when the countdown is up, it will forcefully be killed. Unsaved data is then lost.
If however the selected processes close before the 10 seconds are up, the progress bar is removed immediately.
You may want to manually launch a number of instances of the notepad editor before you run below code to have some processes at hand that you can safely close. Make sure you enter some unsaved text into the editors to see how the tool enables you to save unsaved data!
# get all processes...
Get-Process |
# ...that have a window...
Where-Object MainWindowTitle |
# ...let user choose process(es) to kill...
Out-GridView -Title 'Select Process(es) to Kill' -OutputMode Multiple |
# ...try killing processes gently at first...
ForEach-Object {
# send close message to window:
$null = $_.CloseMainWindow()
# return process
$_
} |
# pass all pipeline objects in ONE array:
& {
,@($input)
} |
ForEach-Object -Begin {
# wait a max of 10 seconds before killing:
$wait = 10
} -process {
# we are receiving ALL processes in ONE chunk here:
$processes = $_
#region wait once for all processes to close gently...
1..$wait | Foreach-Object -Begin { $x = 0 } -Process {
#region calculate progress:
$x++
$secondsRemaining = $wait - $x
$percent = $x * 100 / $wait
#endregion calculate progress:
#region display progress bar:
# (use splatting to better format a long command line)
$parameter = @{
Activity = "Waiting $wait Seconds For Processes To Exit"
Status = "$secondsRemaining Seconds Remaining..."
PercentComplete = $percent
}
Write-Progress @parameter
#endregion display progress bar
#region abort pipeline if all processes have been closed:
# any processes still running?
$runningProcesses = $processes | Where-Object HasExited -eq $false
# if there are no more running processes, prematurely exit pipeline:
if ($runningProcesses.Count -eq 0) { break }
#endregion abort pipeline if all processes have been closed
# wait a second...
Start-Sleep -Seconds 1
}
#endregion wait once for all processes to close gently...
# forcefully kill all remaining processes (remove -WhatIf to actually kill):
$processes | Where-Object HasExited -eq $false | Stop-Process -Force -WhatIf
}
This tool works great. Except the gridview looks rather ugly and shows the default properties for processes. That’s what I’ll fix in the next section. Let’s first look at the code above. It contains a number of coding gems and learning points that you can reuse in a lot of other scenarios.
Intermission: Some Secret Tricks & Tipps
When you call Stop-Process
, this immediately kills processes, and the user won’t get a chance to save unsaved data. Not so good.
However, each process exposes the method CloseMainWindow() which essentially sends a gentle close message to the window, similar to what happens when the user manually closes the window. That’s what the code above tries first:
ForEach-Object {
# send close message to window:
$null = $_.CloseMainWindow()
# return process
$_
} |
Whenever you use Foreach-Object
, always make sure you pass a result on to the next cmdlet. Else, the method CloseMainWindow() would be called for all received processes, but the following downstream cmdlets wouldn’t receive anything anymore, and the pipeline would end. That’s why the received process in $_
is returned at the end of the scriptblock.
Combining All Pipeline Data Into One
Gently sending closing notifications to processes is polite but it doesn’t guarantee the process to close. After all, the user could click Cancel and refuse to close the process window.
That’s why the tool needs to check after a while whether the selected processes did in fact close. However, the tool should only wait once, not for each process.
If you placed the wait into the begin block, the tool would wait only once, but all begin blocks are executed before any pipeline data flows through the pipeline, so the tool would wait before Get-Process
produced any output. Not good.
Another approach is to combine all pipeline objects into one array and pass this chunk of processes on. There is a neat trick to do this: instead of manually creating an empty array or list and add the pipeline objects to it, you can use a simple function, too.
It comes with $input
which already contains all received pipeline objects.
$input
is a so-called enumerator so you need to convert it to an array by placing it into @()
. Else, it could only be read once.
And you need to place a comma before it, essentially wrapping it into another array. The pipeline always unwraps arrays, so it would unwrap the array of collected pipeline objects. By nesting one array in another one, only the outer array gets unwrapped by the pipeline, and the next pipeline command receives the entire array in one chunk:
# pass all pipeline objects in ONE array:
& {
,@($input)
} |
Local Variables
If you require local variables inside a pipeline, you can initialize these in the -begin block of Foreach-Object
. This scriptblock is executed before the pipeline runs and an excellent place to initialize temporary variables, for example $wait
or $x
in the code.
If you initialized temporary variables elsewhere outside the pipeline, when you later copy&paste the code you might miss the initialization code. By placing the initialization inside the pipeline, it becomes part of it.
Progress Bar
The tool displays a progress bar while it waits for all received processes to close. Wait-Progress
creates progress bars easily. For a progress bar to actually display a progress bar, it needs to know how many percent are already completed. That’s why the code uses an iterator variable $x
that gets incremented by one for each iteration of the wait. The percentage can then easily calculated: $percent = $iterator * 100 / $totalIterations
.
Splatting For Better Code
Whenever you need to submit a lot of parameters to a cmdlet, the line becomes long and hard to read. And there are no good ways to add line breaks (except for PowerShells weird backtick escape character).
To format such commands across multiple lines, simply use splatting. This way you can format the parameters nicely line-by-line:
$parameter = @{
Activity = "Waiting $wait Seconds For Processes To Exit"
Status = "$secondsRemaining Seconds Remaining..."
PercentComplete = $percent
}
Write-Progress @parameter
Without splatting, the command line would have been long and hard to read:
Write-Progress -Activity "Waiting $wait Seconds For Processes To Exit" -Status "$secondsRemaining Seconds Remaining..." -PercentComplete $percent
This is a matter of taste and obviously subject to many different opinions.
Aborting Pipeline
Of course, the progress bar should only be displayed while it makes sense, i.e. while there are still processes running. If the user meanwhile closed all processes, it makes no sense for the progress bar to continue to run.
Finding out whether a process still runs is easy: its property HasExited tells you whether the process is still alive. To abort the pipeline prematurely when all processes have been closed in time, use the keyword break.
Prettifying The GridView
The default properties for processes displayed in the gridview looks ugly and confusing. Let’s change that and better control the information shown in the gridview.
Select-Object Breaks Code
A common approach to prettifying gridview output is using Select-Object
. However, that’s often completely breaking your code!
If you added Select-Object
to the tool in order to change the visible properties in the grid, after the initial joy of a prettier gridview display, soon thereafter bad things would happen:
Get-Process |
Where-Object MainWindowTitle |
# ===========================================================
# select better properties to display in the gridview:
# (WARNING: this breaks the code!)
Select-Object -Property ProcessName, Id, StartTime, Company |
# ===========================================================
Out-GridView -Title 'Select Process(es) to Kill' -OutputMode Multiple |
ForEach-Object {
$null = $_.CloseMainWindow()
$_
} |
& {
,@($input)
} |
ForEach-Object -Begin {
$wait = 10
} -process {
$processes = $_
1..$wait | Foreach-Object -Begin { $x = 0 } -Process {
$x++
$secondsRemaining = $wait - $x
$percent = $x * 100 / $wait
$parameter = @{
Activity = "Waiting $wait Seconds For Processes To Exit"
Status = "$secondsRemaining Seconds Remaining..."
PercentComplete = $percent
}
Write-Progress @parameter
$runningProcesses = $processes | Where-Object HasExited -eq $false
if ($runningProcesses.Count -eq 0) { break }
Start-Sleep -Seconds 1
}
$processes | Where-Object HasExited -eq $false | Stop-Process -Force -WhatIf
}
You get exceptions like this one when you run the altered code:
Method invocation failed because [Selected.System.Diagnostics.Process] does not contain a method named 'CloseMainWindow'.
At C:\Users\tobia\OneDrive\Dokumente\powershell\collect pipeline.ps1:13 char:5
+ $null = $_.CloseMainWindow()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (CloseMainWindow:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
And you could run into many related issues. Because Select-Object
always copies objects and creates new custom objects with only the properties you chose. Any methods (such as CloseMainWindow()) are lost. And downstream cmdlets may no longer find the expected object type or properties to bind to.
So tailoring the design of Out-GridView
content via Select-Object
is only an option when you do not plan to return the objects.
Safely Selecting Properties
There is a simple and safe way to chose the visible properties inside Out-GridView
: Out-GridView
uses the PowerShell type system to format objects. So to chose the visible properties, just tell the PowerShell type system how you’d like it to format your data.
Controlling Object Formatting
Before I apply this idea to the tool above, let’s take a look at it in a simple example: I’ll first define a custom type called gridViewSpecial and tell the PowerShell formatting system the properties that should be visible.
Next, I take real service objects and replace their original formatting type with the new type I created.
I am not changing the .NET type of the objects so they stay servicecontroller objects and keep all their properties and methods. I am just changing the hint list that PowerShell uses to format the objects.
When I then output the objects, the PowerShell formatting system displays only the properties I chose. Yet, I can still access any other property, and the .NET type (and all methods) remain intact:
# define new formatting type "gridViewSpecial":
# display only properties DisplayName, StartType, Status
Update-TypeData -TypeName gridviewSpecial -DefaultDisplayPropertySet DisplayName, StartType, Status -Force
$services = Get-Service |
# replace formatting type with my new type
ForEach-Object {
$_.PSTypeNames.Clear()
$_.PSTypeNames.Add('gridviewSpecial')
$_
}
# service objects now show the custom properties by default
$services | Select-Object -First 3
# same applies to gridview:
$services | Out-GridView
# objects remain servicecontroller objects with all properties and methods
$services | Get-Member | Out-GridView
$services[0].GetType().FullName
The PowerShell formatting system now formats the service objects according to my new formatting type, and the same applies to the grid produced by Out-GridView
:
DisplayName StartType Status
----------- --------- ------
AarSvc_8a77f Manual Stopped
Adobe Acrobat Update Service Automatic Running
AllJoyn Router Service Manual Stopped
...
Now you know the route to take to control gridview formatting without using Select-Object
and by preserving the original object types and members. You could fiddle the technique into the code of the process killer tool above, but since a customizable Out-GridView
is something very likely to be generally useful, let’s rather create a reusable new command: Out-GridViewEx
.
Limitations
Always keep in mind that PowerShell uses .NET objects but appends numerous properties and sometimes even methods to them using its type system. To better understand what this means, take a look at the current PowerShell process and dump only the original .NET properties:
# get powershell process
$process = Get-Process -Id $Pid
# dump only original .NET properties:
$process | Get-Member -MemberType Property
These are the properties that you can always access, so when you change the formatting type of objects, these native .NET properties are available to you.
In addition to these, the PowerShell type system has added its own properties:
# get powershell process
$process = Get-Process -Id $Pid
# dump appended PowerShell properties:
$process | Get-Member -MemberType *Property | Where-Object MemberType -ne 'Property'
Any of these properties are defined by the type system. So when you change the formatting type of an object, you lose access to these properties:
TypeName: System.Diagnostics.Process
Name MemberType Definition
---- ---------- ----------
Handles AliasProperty Handles = Handlecount
Name AliasProperty Name = ProcessName
NPM AliasProperty NPM = NonpagedSystemMemorySize64
PM AliasProperty PM = PagedMemorySize64
SI AliasProperty SI = SessionId
VM AliasProperty VM = VirtualMemorySize64
WS AliasProperty WS = WorkingSet64
__NounName NoteProperty string __NounName=Process
Company ScriptProperty System.Object Company {get=$this.Mainmodule.FileVersionInfo.CompanyName;}
CPU ScriptProperty System.Object CPU {get=$this.TotalProcessorTime.TotalSeconds;}
Description ScriptProperty System.Object Description {get=$this.Mainmodule.FileVersionInfo.FileDescription;}
FileVersion ScriptProperty System.Object FileVersion {get=$this.Mainmodule.FileVersionInfo.FileVersion;}
Path ScriptProperty System.Object Path {get=$this.Mainmodule.FileName;}
Product ScriptProperty System.Object Product {get=$this.Mainmodule.FileVersionInfo.ProductName;}
ProductVersion ScriptProperty System.Object ProductVersion {get=$this.Mainmodule.FileVersionInfo.ProductVersion;}
Here is an example:
# get powershell process
$process = Get-Process -Id $Pid
# remove all types and add custom type
Update-TypeData -TypeName custom -DefaultDisplayPropertySet ProcessName, Name, CPU, TotalProcessorTime -Force
$process.PSTypeNames.Clear()
$process.PSTypeNames.Add("custom")
# output altered formatting:
$process
When you look at the new formatting, you can immediately see that only the native .NET properties are available now. Any added property like Name and CPU became inaccessible:
ProcessName Name CPU TotalProcessorTime
----------- ---- --- ------------------
powershell_ise 00:12:00.9531250
Once you restore the original formatting type, PowerShell learns again how to calculate the added members, and they are back:
$process.PSTypeNames.Add('System.Diagnostics.Process')
$process
So the formatting type will never delete information from an object. It just adds and removes hints on how to display and format the native .NET object:
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
3307 121 749004 751652 700,67 12864 1 powershell_ise
Out-GridViewEx - A Formattable Out-GridView
To add new features to the existing Out-GridView
, I am using a proxy function. This way, I can preserve all the nice real-time aspects of the original Out-GridView
.
Proxy Function
Here is the proxy function Out-GridViewEx
I am starting with. It behaves exactly like the original Out-GridView
- except it is expandable:
function Out-GridViewEx
{
# define the original gridview parameters:
[CmdletBinding(DefaultParameterSetName='PassThru', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113364')]
param(
[Parameter(ValueFromPipeline)]
[PSObject]
$InputObject,
[ValidateNotNullOrEmpty()]
[string]
$Title,
[Parameter(ParameterSetName='Wait')]
[switch]
$Wait,
[Parameter(ParameterSetName='OutputMode')]
[Microsoft.PowerShell.Commands.OutputModeOption]
$OutputMode,
[Parameter(ParameterSetName='PassThru')]
[switch]
$PassThru
)
begin
{
# create a stappable pipeline for the original out-gridview command:
$scriptCmd = {& 'Microsoft.PowerShell.Utility\Out-GridView' @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process
{
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
}
end
{
# close the pipeline at the end:
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Out-GridView
.ForwardHelpCategory Cmdlet
#>
}
When you run the code, you can use Out-GridView
and the new Out-GridViewEx
interchangeable. They should perform exactly identical:
Get-Service | Where-Object CanStop | Out-GridViewEx -Title 'Select Service' -OutputMode Single | Stop-Service -WhatIf
Adding New Parameter
Let’s add a new parameter -VisibleProperty that can be a string array and takes the properties that you want to show in your gridview:
function Out-GridViewEx
{
# define the original gridview parameters:
[CmdletBinding(DefaultParameterSetName='PassThru', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113364')]
param(
[Parameter(ValueFromPipeline)]
[PSObject]
$InputObject,
[ValidateNotNullOrEmpty()]
[string]
$Title,
[Parameter(ParameterSetName='Wait')]
[switch]
$Wait,
[Parameter(ParameterSetName='OutputMode')]
[Microsoft.PowerShell.Commands.OutputModeOption]
$OutputMode,
[Parameter(ParameterSetName='PassThru')]
[switch]
$PassThru,
# add new parameter to receive list of visible properties:
[string[]]
$VisibleProperty
)
begin
{
# create a stappable pipeline for the original out-gridview command:
$scriptCmd = {& 'Microsoft.PowerShell.Utility\Out-GridView' @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process
{
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
}
end
{
# close the pipeline at the end:
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Out-GridView
.ForwardHelpCategory Cmdlet
#>
}
When you run this code, Out-GridViewEx
now sports a new parameter -VisibleProperty. However, when you use it, you get an exception:
PS> Get-Service | Out-GridViewEx -VisibleProperty DisplayName, StartType
Out-GridView : A parameter cannot be found that matches parameter name 'VisibleProperty'.
At line:36 char:65
+ ... = {& 'Microsoft.PowerShell.Utility\Out-GridView' @PSBoundParameters }
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Out-GridView], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.OutGridViewCommand
The exception clearly indicates what’s amiss: I have passed all user submitted parameters to the original Out-GridView
, and obviously it did not support it.
Implementing -VisibleParameter
In the final step, I am adding the custom formatting type approach to make Out-GridViewEx
functional:
function Out-GridViewEx
{
# define the original gridview parameters:
[CmdletBinding(DefaultParameterSetName='PassThru', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113364')]
param(
[Parameter(ValueFromPipeline)]
[PSObject]
$InputObject,
[ValidateNotNullOrEmpty()]
[string]
$Title,
[Parameter(ParameterSetName='Wait')]
[switch]
$Wait,
[Parameter(ParameterSetName='OutputMode')]
[Microsoft.PowerShell.Commands.OutputModeOption]
$OutputMode,
[Parameter(ParameterSetName='PassThru')]
[switch]
$PassThru,
# add new parameter to receive list of visible properties:
[string[]]
$VisibleProperty
)
begin
{
# name of custom formatting type:
$customTypeName = 'outgridviewex'
# did the user submit visible properties?
$userDefined = $PSBoundParameters.ContainsKey('VisibleProperty')
if ($userDefined)
{
# create the custom formatting type with the desired display properties:
Update-TypeData -TypeName $customTypeName -DefaultDisplayPropertySet $VisibleProperty -Force
# remove the parameter from $PSBoundParameters to not pass it to the original
# Out-GridView:
$null = $PSBoundParameters.Remove('VisibleProperty')
}
# create a stappable pipeline for the original out-gridview command:
$scriptCmd = {& 'Microsoft.PowerShell.Utility\Out-GridView' @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process
{
if ($userDefined)
{
# remember old types of current pipeline object:
[string[]]$oldType = $_.PSTypeNames
# remove all:
$_.PSTypeNames.Clear()
# add new custom type:
$_.PSTypeNames.Add($customTypeName)
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
# clear custom type:
$_.PSTypeNames.Clear()
# restore original types:
foreach($type in $oldType) { $_.PSTypeNames.Add($type) }
}
else
{
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
}
}
end
{
# close the pipeline at the end:
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Out-GridView
.ForwardHelpCategory Cmdlet
#>
}
Now it’s almost trivial to control the visible properties in your gridview without changing the objects:
Get-Service | Out-GridViewEx -VisibleProperty DisplayName, StartType -PassThru
Note how the selected items are returned as default service objects - Out-GridViewEx
alters object types only transitional and restores the original types once the object was submitted to Process():
Status Name DisplayName
------ ---- -----------
Stopped AppMgmt Application Management
Running AppXSvc AppX Deployment Service (AppXSVC)
End With A Beautiful Tool
Here is the ugly tool from the beginning using the new Out-GridViewEx
, producing a much prettier and easier-to-grasp gridview selection dialog:
function Out-GridViewEx
{
# define the original gridview parameters:
[CmdletBinding(DefaultParameterSetName='PassThru', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113364')]
param(
[Parameter(ValueFromPipeline)]
[PSObject]
$InputObject,
[ValidateNotNullOrEmpty()]
[string]
$Title,
[Parameter(ParameterSetName='Wait')]
[switch]
$Wait,
[Parameter(ParameterSetName='OutputMode')]
[Microsoft.PowerShell.Commands.OutputModeOption]
$OutputMode,
[Parameter(ParameterSetName='PassThru')]
[switch]
$PassThru,
# add new parameter to receive list of visible properties:
[string[]]
$VisibleProperty
)
begin
{
# name of custom formatting type:
$customTypeName = 'outgridviewex'
# did the user submit visible properties?
$userDefined = $PSBoundParameters.ContainsKey('VisibleProperty')
if ($userDefined)
{
# create the custom formatting type with the desired display properties:
Update-TypeData -TypeName $customTypeName -DefaultDisplayPropertySet $VisibleProperty -Force
# remove the parameter from $PSBoundParameters to not pass it to the original
# Out-GridView:
$null = $PSBoundParameters.Remove('VisibleProperty')
}
# create a stappable pipeline for the original out-gridview command:
$scriptCmd = {& 'Microsoft.PowerShell.Utility\Out-GridView' @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process
{
if ($userDefined)
{
# remember old types of current pipeline object:
[string[]]$oldType = $_.PSTypeNames
# remove all:
$_.PSTypeNames.Clear()
# add new custom type:
$_.PSTypeNames.Add($customTypeName)
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
# clear custom type:
$_.PSTypeNames.Clear()
# restore original types:
foreach($type in $oldType) { $_.PSTypeNames.Add($type) }
}
else
{
# forward all pipeline input to the original out-gridview command:
$steppablePipeline.Process($_)
}
}
end
{
# close the pipeline at the end:
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Out-GridView
.ForwardHelpCategory Cmdlet
#>
}
# get all processes...
Get-Process |
# ...that have a window...
Where-Object MainWindowTitle |
# ...sort by name...
Sort-Object -Property ProcessName |
# ...let user choose process(es) to kill...
Out-GridViewEx -VisibleProperty ProcessName, StartTime, MainWindowTitle -Title 'Select Process(es) to Kill' -OutputMode Multiple |
# ...try killing processes gently at first...
ForEach-Object {
# send close message to window:
$null = $_.CloseMainWindow()
# return process
$_
} |
# pass all pipeline objects in ONE array:
& {
,@($input)
} |
ForEach-Object -Begin {
# wait a max of 10 seconds before killing:
$wait = 10
} -process {
# we are receiving ALL processes in ONE chunk here:
$processes = $_
#region wait once for all processes to close gently...
1..$wait | Foreach-Object -Begin { $x = 0 } -Process {
#region calculate progress:
$x++
$secondsRemaining = $wait - $x
$percent = $x * 100 / $wait
#endregion calculate progress:
#region display progress bar:
# (use splatting to better format a long command line)
$parameter = @{
Activity = "Waiting $wait Seconds For Processes To Exit"
Status = "$secondsRemaining Seconds Remaining..."
PercentComplete = $percent
}
Write-Progress @parameter
#endregion display progress bar
#region abort pipeline if all processes have been closed:
# any processes still running?
$runningProcesses = $processes | Where-Object HasExited -eq $false
# if there are no more running processes, prematurely exit pipeline:
if ($runningProcesses.Count -eq 0) { break }
#endregion abort pipeline if all processes have been closed
# wait a second...
Start-Sleep -Seconds 1
}
#endregion wait once for all processes to close gently...
# forcefully kill all remaining processes (remove -WhatIf to actually kill):
$processes | Where-Object HasExited -eq $false | Stop-Process -Force -WhatIf
}