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 { Write-Verbose "Starting $($MyInvocation.MyCommand)" 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 |