functions/ConvertTo-WPFGrid.ps1

<#
There is a limitation with autogenerated columns if the property
contains a period or a slash.
 
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/f5f752f6-f08d-4d46-9e1e-78a4496eaf12/why-does-not-dataGrid-show-values-if-there-are-a-point-82208221-or-82208221-in-header?forum=wpf
 
#>

function ConvertTo-WPFGrid {
    [cmdletbinding(DefaultParameterSetName = 'input')]
    [Alias('cwg')]
    [OutputType('none')]

    param(
        [Parameter(ValueFromPipeline, ParameterSetName = 'Input')]
        [ValidateNotNullOrEmpty()]
        [PSObject]$InputObject,

        [Parameter(ParameterSetName = 'scriptblock', HelpMessage = 'Enter a scriptblock that will generate data to be populated in the form')]
        [ValidateNotNullOrEmpty()]
        [Scriptblock]$Scriptblock,

        [string]$Title = 'ConvertTo-WPFGrid',
        [ValidateScript({ $_ -ge 5 })]
        [int]$Timeout,

        [switch]$Refresh,

        [Parameter(HelpMessage = 'Control how grid lines are displayed')]
        [ValidateSet('All', 'Horizontal', 'None', 'Vertical')]
        [ValidateNotNullOrEmpty()]
        [string]$GridLines = 'All',

        [Parameter(HelpMessage = 'Run this scriptblock to initialize the background runspace')]
        [scriptblock]$InitializationScript,

        [Parameter(HelpMessage = 'Load locally defined variables into the background runspace')]
        [alias('var')]
        [string[]]$UseLocalVariable,

        [Parameter(HelpMessage = 'Load your PowerShell profiles into the background runspace')]
        [alias('profile')]
        [switch]$UseProfile
    )

    begin {
        #tags are used for categorizing the command
        #cmdTags = graphical
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        Write-Verbose "Running under PowerShell version $($PSVersionTable.PSVersion)"
        if ($Refresh -and ($Timeout -lt 5)) {
            Write-Verbose "Detected a timeout value of $Timeout"
            Write-Verbose "Refresh is set to $Refresh"
            throw 'You must specify a timeout value in seconds when using -Refresh'
        }
        #set a flag for the process block
        if ((Test-IsPSWindows)) {
            #attempt to load the WPF related classes which might or might not be available depending
            #on operating system and PowerSell version

            try {
                Write-Verbose 'Attempting to load WPF assemblies'
                Add-Type -AssemblyName PresentationFramework -ErrorAction Stop
                Add-Type -AssemblyName PresentationCore -ErrorAction Stop

            }
            catch {
                Write-Warning "Failed to load WPF required assemblies. This command isn't supported under this version of PowerShell on this platform. $($_.exception.Message)"

                #bail out out of the command
                break
            }

            Write-Verbose 'Define new runspace'
            $newRunspace = [RunspaceFactory]::CreateRunspace()
            if ($newRunspace.ApartmentState) {
                $newRunspace.ApartmentState = 'STA'
            }
            else {
                #This command probably won't run if the ApartmentState can't be set to STA
                #clean up
                $newRunspace.dispose()

                Write-Warning 'Incompatible runspace detected. This command will most likely fail on this platform with this version of PowerShell.'
                #bail out of the command
                break
            }
            $newRunspace.ThreadOptions = 'ReuseThread'
            Write-Verbose 'Opening new runspace'
            $newRunspace.Open()

            if ($UseLocalVariable) {
                Write-Verbose 'Using local variables'
                Get-Variable -Include $UseLocalVariable |
                ForEach-Object {
                    Write-Verbose "...Adding $($_.name)"
                    $newRunspace.sessionStateProxy.SetVariable($_.name, $_.value)
                }
            }

            Write-Verbose 'Defining script command'
            $psCmd = [PowerShell]::Create()
            #code to display the WPF form
            $gridScript = {
                param(
                    [string]$Title = 'ConvertTo-WPFGrid',
                    [ValidateScript( { $_ -ge 5 })]
                    [int]$Timeout = 0,
                    [object[]]$Data,
                    [scriptblock]$cmd,
                    [switch]$Refresh,
                    [string]$GridLines
                )

                # It may not be necessary to add these types but it doesn't hurt to include them
                Add-Type -AssemblyName PresentationFramework
                Add-Type -AssemblyName PresentationCore

                function _refresh {
                    #this is a function to refresh a UI element
                    param($element)

                    $element.Dispatcher.invoke('render', [action] {})
                    [System.Threading.Thread]::Sleep(50)
                }

                #get maximum available working area on the screen
                $s = [System.Windows.SystemParameters]::WorkArea

                $form = New-Object System.Windows.Window
                $form.Title = $Title
                $form.MaxHeight = $s.Height
                $form.MaxWidth = $s.Width

                $form.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight

                #define a handler when the form is loaded. The scriptblock uses variables defined later
                #in the script
                $form.add_Loaded( {
                        $dataGrid.UpdateLayout()

                        foreach ($col in $dataGrid.Columns) {
                            #because of the way I am loading data into the grid
                            #it appears I need to set the sorting on each column
                            $col.CanUserSort = $True
                            $col.SortMemberPath = $col.Header
                        }

                        if ($Timeout -gt 0) {
                            $timer.IsEnabled = $True
                            $Timer.Start()
                        }

                        #calculate screen dimensions to center the form
                        $s = [System.Windows.SystemParameters]::WorkArea
                        $form.top = $s.Height / 2 - $form.ActualHeight / 2
                        $form.left = $s.width / 2 - $form.ActualWidth / 2
                        $form.UpdateLayout()
                        $form.focus
                    })

                $Form.Add_closing( {
                        #reserved for future use
                    })
                $Form.Add_closed( {
                        #reserved for future use
                    })
                #Create a grid to hold the dataGrid
                $Grid = New-Object System.Windows.Controls.Grid

                $Grid.HorizontalAlignment = 'stretch'
                $grid.VerticalAlignment = 'stretch'

                #add buttons to close and manually refresh (Issue #34)
                $btnRefresh = New-Object System.Windows.Controls.Button
                $btnRefresh.Content = 'Refresh Now'
                $btnRefresh.Height = 25
                $btnRefresh.Width = 80
                $btnRefresh.HorizontalAlignment = 'left'
                $btnRefresh.VerticalAlignment = 'Top'
                $btnRefresh.Margin = '15,10,0,0'
                $btnRefresh.ToolTip = 'click to refresh data from the command'

                $btnRefresh.add_click( {
                        [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait

                        $status.Text = '...refreshing content. Please wait.'
                        _refresh $status

                        $timer.stop()
                        Start-Sleep -Seconds 3

                        $dataGrid.ItemsSource = Invoke-Command -ScriptBlock $cmd

                        foreach ($col in $dataGrid.Columns) {
                            #because of the way I am loading data into the grid
                            #it appears I need to set the sorting on each column
                            $col.CanUserSort = $True
                            $col.SortMemberPath = $col.Header
                        }

                        $script:Now = Get-Date

                        if ($script:count) {
                            $script:count = $Timeout
                            [DateTime]$script:terminate = $now.AddSeconds($timeout)
                            $ts = New-TimeSpan -Seconds $script:count
                            $status.text = " Last updated $script:Now - refresh in $($ts.ToString())"
                        }
                        else {
                            $status.text = " last updated $Script:Now"
                        }
                        if ($refresh) {
                            $timer.start()
                        }
                        $form.title = $Title
                        [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Arrow
                    })

                $grid.AddChild($btnRefresh)

                $btnClose = New-Object System.Windows.Controls.Button
                $btnClose.Content = 'Close'
                $btnClose.Height = 25
                $btnClose.Width = 80
                $btnClose.HorizontalAlignment = 'Right'
                $btnClose.VerticalAlignment = 'Top'
                $btnClose.Margin = '0,10,15,0'
                $btnClose.ToolTip = 'close the form and quit'

                $btnClose.add_click( {
                        $form.Close()
                    })
                $grid.AddChild($btnClose)

                #create a dataGrid
                $dataGrid = New-Object System.Windows.Controls.dataGrid
                $dataGrid.width = 'Auto'
                $dataGrid.Height = 'Auto'
                $dataGrid.VerticalAlignment = 'stretch'
                $dataGrid.HorizontalAlignment = 'stretch'
                $dataGrid.margin = '0,50,0,25'

                $dataGrid.ColumnWidth = 'Auto'
                $dataGrid.GridLinesVisibility = $GridLines
                $dataGrid.VerticalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Auto
                $dataGrid.HorizontalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Auto
                $dataGrid.CanUserSortColumns = $True
                $dataGrid.CanUserResizeColumns = $True
                $dataGrid.CanUserReorderColumns = $True
                $dataGrid.AutoGenerateColumns = $False
                $dataGrid.CanUserAddRows = $false
                $dataGrid.CanUserDeleteRows = $false
                $dataGrid.IsReadOnly = $True
                #enable alternating color rows
                $dataGrid.AlternatingRowBackground = 'gainsboro'

                $DataTable = New-Object System.Data.DataTable

                #create headers
                $data[0].PSObject.properties.Name |
                ForEach-Object {
                    if ($DataTable.columns.caption -notcontains $_) {
                        [void]$DataTable.Columns.add($_)
                    }
                }
                foreach ($obj in $data) {
                    $props = $obj.PSObject.Properties
                    [void]$DataTable.rows.add($props.value)
                }

                for ($i = 0; $i -lt $DataTable.Columns.Count; $i++) {
                    $col = New-Object System.Windows.Controls.dataGridTextColumn
                    $col.Header = $DataTable.Columns[$i].ColumnName
                    $col.Binding = New-Object system.Windows.data.binding("[$i]")
                    [void]$dataGrid.Columns.Add($col)
                }
                Write-Verbose "returning $($DataTable.Rows.Count) rows"

                $dataGrid.ItemsSource = $DataTable.DefaultView

                # $dataGrid.ItemsSource = $data

                foreach ($col in $dataGrid.Columns) {
                    #because of the way I am loading data into the grid
                    #it appears I need to set the sorting on each column
                    $col.CanUserSort = $True
                    $col.SortMemberPath = $col.Header
                }
                $Grid.AddChild($dataGrid)

                $status = New-Object System.Windows.Controls.TextBlock
                $status.Height = 20
                $status.Background = 'lightgray'
                $status.VerticalAlignment = 'bottom'
                $status.HorizontalAlignment = 'stretch'
                $status.Width = 'Auto'

                $Grid.AddChild($status)

                $form.AddChild($Grid)

                $script:Now = Get-Date
                # define a timer to automatically dismiss the form. The timer uses a 1 second interval tick
                if ($Timeout -gt 0) {
                    [int]$script:count = $Timeout
                    $timer = New-Object System.Windows.Threading.DispatcherTimer
                    [DateTime]$script:terminate = $now.AddSeconds($timeout)
                    $timer.Interval = [TimeSpan]'0:0:1.00'

                    $timer.add_tick( {
                            $ts = New-TimeSpan -Seconds $script:count
                            if ((Get-Date) -lt $script:terminate -and $Refresh) {
                                $status.text = " Last updated $script:Now - refresh in $($ts.ToString())"
                                $script:count--
                            }
                            elseif ( (Get-Date) -lt $script:terminate) {
                                $status.text = " Last updated $script:Now - closing in $($ts.ToString())"
                                $script:count--
                            }
                            else {
                                $timer.stop()
                                if ($Refresh) {
                                    $form.Title = "$Title ...refreshing content. Please wait."
                                    [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Wait
                                    $dataGrid.ItemsSource = Invoke-Command -ScriptBlock $cmd

                                    foreach ($col in $dataGrid.Columns) {
                                        #because of the way I am loading data into the grid
                                        #it appears I need to set the sorting on each column
                                        $col.CanUserSort = $True
                                        $col.SortMemberPath = $col.Header
                                    }
                                    $script:count = $timeout
                                    $script:now = Get-Date
                                    [DateTime]$script:terminate = $now.AddSeconds($timeout)
                                    $ts = New-TimeSpan -Seconds $script:count
                                    $status.text = " Last updated $script:Now - refresh in $($ts.ToString()) seconds"
                                    $Timer.Start()
                                    $form.title = $Title
                                    [System.Windows.Input.Mouse]::OverrideCursor = [System.Windows.Input.Cursors]::Arrow
                                }
                                else {
                                    $form.close()
                                }
                            }
                        })
                }
                else {
                    $status.text = " last updated $Script:Now"
                }

                $form.ShowDialog()
            }

            if ($UseProfile) {
                Write-Verbose 'Loading user profiles'
                $profiles = $profile.AllUsersAllHosts, $profile.AllUsersCurrentHost,
                $profile.CurrentUserAllHosts, $profile.CurrentUserCurrentHost
                foreach ($file in $profiles) {
                    if (Test-Path -Path $file) {
                        [void]$psCmd.AddScript($file)
                    }
                }
            }
            if ($InitializationScript) {
                Write-Verbose 'Loading an initialization scriptblock'
                [void]$psCmd.AddScript($InitializationScript)
            }

            [void]$psCmd.AddScript($gridScript)

            #initialize a list to hold all processed objects
            $data = [System.Collections.Generic.list[object]]::new()
        }
        else {
            $bail = $True
            Write-Warning 'This command requires a Windows platform'
            break
        }
    } #begin

    process {
        #add each incoming object to the data array
        if ($psCmdlet.ParameterSetName -eq 'Input') {
            Write-Verbose 'Adding input to data'
            if ($InputObject -is [array]) {
                $data.AddRange($InputObject)
            }
            else {
                $data.Add($InputObject)
            }
        }
        else {
            Write-Verbose 'Invoking scriptblock'
            $data = Invoke-Command -ScriptBlock $Scriptblock
        }
    } #process

    end {
        Write-Verbose 'Updating PSBoundParameters'
        $PSBoundParameters.Data = $data
        [void]$PSBoundParameters.remove('InputObject')
        if ($PSBoundParameters.ContainsKey('UseProfile')) {
            [void]$PSBoundParameters.Remove('UseProfile')
        }

        if ($psCmdlet.ParameterSetName -eq 'input') {

            #parse the invocation to get the pipelined expression up to this command
            #Write-verbose $($MyInvocation | Out-string)
            Write-Verbose "Parsing $($MyInvocation.line) into a scriptblock"
            try {
                $cmd = [scriptblock]::Create($MyInvocation.line.substring(0, $MyInvocation.line.LastIndexOf('|')))
            }
            catch {
                Write-Warning "Error created the cmd scriptblock: $($_.exception.message)"
                if ($Refresh) {
                    Write-Warning 'Failed create an invocation scriptblock. In order to refresh run your pipelined expression as a single expression with no breaks.'
                }
            }
        }
        else {
            $cmd = $Scriptblock
        }

        $PSBoundParameters.cmd = $cmd
        Write-Verbose "Refresh command: $cmd"

        Write-Verbose 'Sending PSBoundParameters to runspace'

        [void]$psCmd.AddParameters($PSBoundParameters)
        $psCmd.Runspace = $newRunspace
        Write-Verbose 'Begin Invoke()'
        $handle = $psCmd.BeginInvoke()

        $ThreadJob = New-RunspaceCleanupJob -Handle $handle -powerShell $psCmd -sleep 30 -PassThru
        Write-Verbose "Created monitoring ThreadJob $($ThreadJob.id)"
        Write-Verbose "Ending $($MyInvocation.MyCommand)"

    } #end

} #close function
#EOF