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])
}