PoshWPF.psm1
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase Class PoshWPFBinding { hidden [string]$Pointer [object]$Value PoshWPFBinding($Pointer, $Value) { $this.Pointer = $Pointer $this.Value = $Value } UpdateValue($NewValue) { $this.Value = $NewValue $Global:PoshWPFHashTable.Bindings[$this.Pointer][0] = $NewValue } } Function New-WPFWindow { <# .SYNOPSIS Creates new WPF window in background thread .DESCRIPTION Creates new WPF window in background thread to allow you to keep using the PowerShell console .PARAMETER xaml XAML of window .EXAMPLE New-WPFWindow -XAML $XAML .NOTES .Author Ryan Ephgrave #> Param( [Parameter(Mandatory=$true, Position=0, HelpMessage="XAML code of UI")] [ValidateNotNullOrEmpty()] [xml]$xaml, [Parameter(Mandatory=$false, Position=1, HelpMessage="Name of window")] [ValidateNotNullOrEmpty()] [string]$WindowName ) $FormattedXAML = Format-WPFXAML -xaml $xaml $Hash = '' if([string]::IsNullOrEmpty($WindowName)) { $Global:PoshWPFHashTable = [HashTable]::Synchronized(@{}) $Hash = $Global:PoshWPFHashTable } else { New-Variable -Name "PoshWPFHashTable_$WindowName" -Value ([HashTable]::Synchronized(@{})) -Scope 'Global' $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value } $Hash.ErrorList = New-Object System.Collections.ArrayList try { $Hash['ErrorTimer'] = New-Object Timers.Timer $ErrorAction = { $VariableNames = (Get-Variable -Name 'PoshWPFHashTable*' -Scope 'Global').Name Foreach($Name in $VariableNames) { $Hash = Get-Variable -Name $Name -Scope 'Global' If($Hash.ErrorList.Count -ne 0) { $ErrorObj = $Hash.ErrorList[0] $Hash.ErrorList.RemoveAt(0) Write-Host $ErrorObj } } } $Hash['ErrorTimer'].Interval = 500 $null = Register-ObjectEvent -InputObject $Hash['ErrorTimer'] -EventName Elapsed -SourceIdentifier 'Timer' -Action $ErrorAction -ErrorAction 'Stop' $Hash['ErrorTimer'].Start() } catch { } $Hash.Host = $Host $Hash.xaml = $FormattedXAML $Hash.Actions = New-Object System.Collections.ArrayList $Hash.ActionsMutex = New-Object System.Threading.Mutex($false, 'ActionsMutex') $Hash.WindowShown = $false $Hash.WaitEvent = $true $Hash.ScriptDirectory = $PSScriptRoot $Runspace = [RunspaceFactory]::CreateRunspace() $Runspace.ApartmentState = 'STA' $Runspace.ThreadOptions = "ReuseThread" $Runspace.Open() $Runspace.SessionStateProxy.SetVariable('PoshWPFHashTable', $Hash) $PS = [PowerShell]::Create() $PS.Runspace = $Runspace $null = $PS.AddScript({ $ScriptDirectory = $PoshWPFHashTable.ScriptDirectory . "$ScriptDirectory\PoshWPF-UI-Code.ps1" [xml]$xaml = $PoshWPFHashTable.xaml Show-WPFWindow -xaml $xaml }) $Hash.Handle = $PS.BeginInvoke() $Hash.Runspace = $Runspace $Hash.PowerShell = $PS while(!$Hash.WindowShown) { Start-Sleep -Milliseconds 10 } $null = New-WPFEvent -ControlName 'Window' -EventName 'Closing' -Action { $null = $Hash.PowerShell.EndInvoke($Hash.Handle) $null = $Hash.PowerShell.Dispose() $null = $Hash.Runspace.Close() $null = $Hash.Runspace.Dispose() $Hash.WaitEvent = $false } } Function Format-WPFXAML { <# .SYNOPSIS Removes Visual Studio specific XAML properties .DESCRIPTION Removes the properties Visual Studio adds to XAML which causes crashing outside of VS .PARAMETER xaml XAMl of the window .EXAMPLE Format-WPFXAML -XAML $xaml .NOTES .Author: Ryan Ephgrave #> param( [xml]$xaml ) if($xaml.Window) { $Attributes = $xaml.Window.Attributes $AttributesToRemove = @() foreach($Attribute in $Attributes) { Switch($Attribute.LocalName) { 'Class' { $AttributesToRemove += @($Attribute.Name) } 'Local' { $AttributesToRemove += @($Attribute.Name) } 'Ignorable' { $AttributesToRemove += @($Attribute.Name) } } } foreach($Attribute in $AttributesToRemove){ $xaml.Window.RemoveAttribute($Attribute) } $xaml } else { Throw 'No window object!' } } Function Invoke-WPFAction { <# .SYNOPSIS Runs an action in the UI thread .DESCRIPTION Runs a scriptblock in the UI thread .PARAMETER Action Scriptblock to run in the UI thread .EXAMPLE Invoke-WPFAction -Action $Scriptblock .NOTES .Author: Ryan Ephgrave #> param( [ScriptBlock]$Action, [string]$WindowName ) $Hash = '' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value } $Hash.Action = $Action while($Hash.Action -ne $null) { Start-Sleep -Milliseconds 10 } if($Hash['ActionError']) { $ErrorObj = $Hash['ActionError'] $Hash['ActionError'] = $null throw $ErrorObj } } Function Get-WPFControl { <# .SYNOPSIS Returns a hash of properties of the WPF control .DESCRIPTION Returns a hash because if you try to interact with the objects in the HashTable you'll get errors .PARAMETER ControlName Name of WPF control .PARAMETER PropertyName Name of the property you want .EXAMPLE Get-WPFControl -ControlName 'Window' -PropertyName 'Title' Only returns Title from Window .EXAMPLE Get-WPFControl -ControlName 'Window' Returns all properties from Window .NOTES .Author: Ryan Ephgrave #> Param( [string]$ControlName, [string]$PropertyName, [string]$WindowName ) $Hash = '' $strWindowName = 'PoshWPFHashTable' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value $strWindowName = $strWindowName + "_$WindowName" } if($ControlName -ne 'Window') { $ControlName = "Window_$($ControlName)" } $strAction = @" `$Control = `$Global:WindowControls['$ControlName'] `$Global:$($strWindowName).GetControlObject = @{} `$ControlNames = (`$Control | Get-Member -MemberType Property).Name foreach(`$Name in `$ControlNames) { `$Global:$($strWindowName).GetControlObject[`$Name] = `$Control."`$Name" } "@ $action = [ScriptBlock]::Create($strAction) Invoke-WPFAction -Action $action -WindowName $WindowName if($Hash.GetControlObject.count -ne 0) { if([string]::IsNullOrEmpty($PropertyName)) { $Hash.GetControlObject } else { $Obj = $Hash.GetControlObject $obj."$PropertyName" } $Hash.GetControlObject = $null } } Function Set-WPFControl { <# .SYNOPSIS Updates a property on a WPF control .DESCRIPTION Will update the property by running Invoke-WPFAction .PARAMETER ControlName Name of the control .PARAMETER PropertyName Name of the property .PARAMETER Value Object with the new value .EXAMPLE Set-WPFControl -ControlName 'Window' -PropertyName 'Title' -Value 'My new title!' .NOTES .Author: Ryan Ephgrave #> Param( [string]$ControlName, [string]$PropertyName, [object]$Value, [string]$WindowName ) $Hash = '' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value } if($ControlName -ne 'Window') { $ControlName = "Window_$ControlName" } $Guid = (New-Guid).Guid $Hash[$guid] = $Value $strScriptBlock = "`$WindowControls['$($ControlName)'].$($PropertyName) = `$PoshWPFHashTable['$guid'];" + ` "`$null = `$PoshWPFHashTable.Remove('$guid')" $ScriptBlock = [ScriptBlock]::Create($strScriptBlock) Invoke-WPFAction -Action $ScriptBlock -WindowName $WindowName } Function New-WPFEvent { <# .SYNOPSIS Creates an event to run in the main thread when a UI action is run in the UI thread .DESCRIPTION Creates an event to run in the main thread when a UI action is run in the UI thread .PARAMETER ControlName Name of the control .PARAMETER EventName Name of the event on the control .PARAMETER Action Action to run .EXAMPLE New-WPFEvent -ControlName 'Button' -EventName 'Click' -Action { Write-Host 'Button clicked!' } .NOTES .Author: Ryan Ephgrave #> Param( [string]$ControlName, [string]$EventName, [scriptblock]$Action, [string]$WindowName ) $Hash = '' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value } if($ControlName -ne 'Window') { $ControlName = "Window_$ControlName" } $GUID = (New-Guid).Guid $strEventAction = "`$Global:WindowControls['$ControlName'].Add_$($EventName)({`$Global:PoshWPFHashTable.Host.Runspace.Events.GenerateEvent('$GUID',`$Global:WindowControls['$ControlName'],`$null,'')})" $EventAction = [scriptblock]::Create($strEventAction) Invoke-WPFAction -Action $EventAction -WindowName $WindowName $null = Register-EngineEvent -SourceIdentifier $Guid -Action $Action } Function Start-WPFSleep { <# .SYNOPSIS Waits for action to be done .DESCRIPTION When running the UI in a separate thread, you may want to pause the main thread until an action is done in the UI. This is very necessary if you run the script without the -NoExit switch. The PowerShell session will simply close! .EXAMPLE Start-WPFSleep .NOTES .Author: Ryan Ephgrave #> Param( [string]$WindowName ) $Hash = '' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value } while($Hash.WaitEvent){ Wait-Event -Timeout 2 } } Function New-WPFBinding { Param( [string]$ControlName, [object]$PropertyName, [string]$Mode = 'TwoWay', [string]$WindowName ) $Hash = '' $strWindowName = 'PoshWPFHashTable' if([string]::IsNullOrEmpty($WindowName)) { $Hash = $Global:PoshWPFHashTable } else { $Hash = (Get-Variable -Name "PoshWPFHashTable_$WindowName" -Scope 'Global').Value $strWindowName = $strWindowName + "_$WindowName" } If($ControlName -ne 'Window') { $ControlName = "Window_$ControlName" } if($null -eq $Hash['Bindings']) { $Hash['Bindings'] = @{} } $BindingName = "$($ControlName)_$PropertyName" $Hash.Bindings[$BindingName] = New-Object System.Collections.ObjectModel.ObservableCollection[Object] $strBinding = @" `$ControlType = (`$WindowControls['$ControlName'].GetType()).UnderlyingSystemType `$Binding = New-Object System.Windows.Data.Binding `$Binding.Path = '[0]' `$Binding.Mode = [System.Windows.Data.BindingMode]::$($Mode) `$null = `$Global:$($strWindowName).Bindings['$BindingName'].Add(`$WindowControls['$ControlName'].$PropertyName) `$Binding.Source = `$Global:$($strWindowName).Bindings['$BindingName'] `$null = [System.Windows.Data.BindingOperations]::SetBinding(`$WindowControls['$ControlName'],`$ControlType::$($PropertyName)Property,`$Binding) "@ $BindingAction = [ScriptBlock]::Create($strBinding) Invoke-WPFAction -Action $BindingAction [PoshWPFBinding]::New($BindingName, $Hash.Bindings[$BindingName][0]) } |