Write-FormatTreeView.ps1

function Write-FormatTreeView
{
    <#
    .Synopsis
        Writes the format XML for a TreeView
    .Description
        Writes the .format.ps1xml fragement for a tree view, or a tree node.
    .Link
        Write-FormatCustomView
    .Link
        Write-FormatViewExpression
    .Example
        Write-FormatTreeView -TypeName System.IO.FileInfo, System.IO.DirectoryInfo -NodeProperty Name -HasChildren {
            if (-not $_.EnumerateFiles) { return $false }
            foreach ($f in $_.EnumerateFiles()) {$f;break}
        },
        {
            if (-not $_.EnumerateDirectories) { return $false }
            foreach ($f in $_.EnumerateDirectories()) {$f;break}
        } -Children {
            $_.EnumerateFiles()
        }, {
            foreach ($d in $_.EnumerateDirectories()) {
                if ($d.Attributes -band 'Hidden') { continue }
                $d
            }
        } -Branch ('' + [char]9500 + [char]9472 + [char]9472) -Trunk '| ' |
            Out-FormatData |
            Add-FormatData
 
        Get-Module EZOut | Split-Path | Get-Item | Format-Custom
    #>

    param(
    # One or more properties to be displayed.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateScript({
        foreach ($i in $_) {
            if ($i -isnot [string] -and $i -isnot [ScriptBlock] -and $i -isnot [Collections.IDictionary]) {
                throw "Properties must be a [string], [scriptblock], [collections.IDictionary]"
            }
        }
        return $true
    })]
    [Alias('NodeProperty')]
    [PSObject[]]
    $Property,

    # The separator between one or more properties.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Separator,


    # The Tree View's branch.
    # This text will be displayed before the node.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Branch = '',
    # The Tree View's Trunk.
    # This will be displayed once per depth.
    # By default, this is four blank spaces.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Trunk = ' ',

    # One or more type names.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $TypeName,

    # The name of the selection set. Selection sets are an alternative way to specify a list of types.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('SelectionSetName')]
    [string]
    $SelectionSet,

    # The name of the tree node control.
    # If not provided, this will be Typename1/TypeName2.TreeNode or SelectionSet.TreeNode
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $ControlName,

    # If provided, the table view will only be used if the the typename includes this value.
    # This is distinct from the overall typename, and can be used to have different table views for different inherited objects.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $ViewTypeName,

    # If provided, the table view will only be used if the the typename is in a SelectionSet.
    # This is distinct from the overall typename, and can be used to have different table views for different inherited objects.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $ViewSelectionSet,

    # If provided, will selectively display items.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [ScriptBlock]
    $ViewCondition,

    # Text displayed at the end of each branch.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $EndBranch,

    # A script block displayed at the end of each branch.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ScriptBlock]
    $EndBranchScript,

    # A set of script blocks that determine if the node has children.
    # If these script blocks return a value (that is not 0 or $false),
    # then the associated Children scriptblock will be called.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ScriptBlock[]]
    $HasChildren,

    # A set of script blocks to populate the next generation of nodes.
    # By default, the values returned from these script blocks will become child ndoes
    # of the same type of tree control.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ScriptBlock[]]
    $Children,

    # If provided, child nodes will be rendered with a different custom custom control.
    # This control must exist in the same format file.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string[]]
    $ChildNodeControl)


    begin {
        $allOut = @()
    }

    process {
        $allOut += @(
        '<CustomEntry>'
        if ($ViewSelectionSet -or $ViewTypeName) {
            '<EntrySelectedBy>'
            if ($ViewCondition) {
                '<SelectionCondition>'
            }
            if ($ViewTypeName) {
                "<TypeName>$([Security.SecurityElement]::Escape($ViewTypeName))</TypeName>"
            } elseif ($ViewSelectionSet) {
                "<SelectionSetName>$([Security.SecurityElement]::Escape($ViewSelectionSet))</SelectionSetName>"
            }
            if ($ViewCondition) {
                if ($viewCondition) {
                    "<ScriptBlock>$([Security.SecurityElement]::Escape($viewCondition))</ScriptBlock></SelectionCondition>"
                }
            }
            '</EntrySelectedBy>'
        }
        '<CustomItem>'
        Write-FormatViewExpression -ScriptBlock ([ScriptBLock]::Create(@"
`$Branch,`$trunk = '$($Branch.Replace("'","''"))', '$($trunk.Replace("'","''"))'
if (`$script:treeDepth) {
    [Environment]::Newline + (`$trunk * `$script:TreeDepth)+ `$Branch
} else {
    `$Branch
}
`$script:TreeDepth++;
"@
))
#

        if ($Property) {
            @(foreach ($p in $Property) {
                if ($p -is [string]) {
                    Write-FormatViewExpression -Property $P
                } elseif ($p -is [ScriptBlock]) {
                    Write-FormatViewExpression -ScriptBlock $p
                } elseif ($p -is [Collections.IDictionary]) {
                    Write-FormatViewExpression @p
                }
            }) -join $(
                if ($Separator) {
                    "<Text>$([Security.SecurityElement]::Escape($Separator))</Text>"
                } else {
                    ''
                }
            )
        }

        if ($Children) {
            for ($i =0 ;$i -lt $Children.Count;$i++) {
                if ("$($HasChildren[$i])") {
                    $theChildControl = if ($ChildNodeControl) {
                        if ($ChildNodeControl[$i]) {
                            $ChildNodeControl[$i]
                        } else {
                            $ChildNodeControl[-1]
                        }
                    } elseif ($controlName) {
                        $controlName
                    } elseif ($TypeName) {
                        "$($TypeName -join '/').TreeNode"
                    } elseif ($SelectionSet) {
                        "$SelectionSet.TreeNode"
                    }
                    Write-FormatViewExpression -If $HasChildren[$i] -ScriptBlock $Children[$i] -Enumerate -ActionName $theChildControl
                }
            }
        }

        if ($EndBranch) {
            "<Text>$([Security.SecurityElement]::Escape($EndBranch))</Text>"
        } elseif ($EndBranchScript) {
            Write-FormatViewExpression -ScriptBlock $EndBranchScript
        }

        @'
        <ExpressionBinding>
            <ItemSelectionCondition><ScriptBlock>$script:TreeDepth--;</ScriptBlock></ItemSelectionCondition>
            <ScriptBlock>$null</ScriptBlock>
        </ExpressionBinding>
</CustomItem></CustomEntry>
'@

        )
    }

    end {
        $ControlName = if (-not $ControlName) {
            if ($TypeName) {
                "$($TypeName -join '/').TreeNode"
            } elseif ($SelectionSet) {
                "$SelectionSet.TreeNode"
            }
        } else {
            $ControlName
        }
        if (-not $ControlName) {
            Write-Error "Must Provide a Control Name, TypeName, or SelectionSetName"
            return
        }
        $xmlStr=  @("<Control><Name>$([Security.SecurityElement]::Escape($ControlName))</Name>
<CustomControl>
<CustomEntries>
$($allOut -join '')
</CustomEntries>
</CustomControl></Control>"
)
        $xml=[xml]$xmlStr
        if (-not $xml) { return }
        $xOut=[IO.StringWriter]::new()
        $xml.Save($xOut)
        "$xOut".Substring('<?xml version="1.0" encoding="utf-16"?>'.Length + [Environment]::NewLine.Length)
        $xOut.Dispose()
        $rootStart = if ($TypeName) {
            "<View>
                <Name>$([Security.SecurityElement]::Escape(($TypeName -join '/')))</Name>
                <ViewSelectedBy>
                $(foreach ($t in $typename) { "<TypeName>$([Security.SecurityElement]::Escape($t))</TypeName>"})
                </ViewSelectedBy>
            "

        } elseif ($SelectionSet) {
            "<View>
                <Name>$($SelectionSet)</Name>
                <ViewSelectedBy>
                $(foreach ($t in $SelectionSet) { "<SelectionSetName>$([Security.SecurityElement]::Escape($t))</SelectionSetName>"})
                </ViewSelectedBy>
            "

        }

        if ($rootStart) {
            $restOfView = $rootStart +  "
<CustomControl>
<CustomEntries>
    <CustomEntry>
        <CustomItem>
            $(Write-FormatViewExpression -If ([ScriptBlock]::Create('$script:TreeDepth = 0;$true')) -ScriptBlock ([ScriptBlock]::Create('$_')) -ActionName $ControlName -Enumerate)
            $(Write-FormatViewExpression -If ([ScriptBlock]::Create('$executionContext.SessionState.PSVariable.Remove("script:TreeDepth");$false')) -ScriptBlock ([ScriptBlock]::Create('$null')))
        </CustomItem>
    </CustomEntry>
</CustomEntries>
</CustomControl>
</View>"



            $xml=[xml]$restOfView
            if (-not $xml) { return }
            $xOut=[IO.StringWriter]::new()
            $xml.Save($xOut)
            "$xOut".Substring('<?xml version="1.0" encoding="utf-16"?>'.Length + [Environment]::NewLine.Length)
            $xOut.Dispose()
        }

    }
}