Universal Dialog

Out-GridView produces not just an output window. It is an extremely powerful universal dialog, and with a few tricks it becomes even better!

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-GridViewnot 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-GridViewExI 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
  }