TreeControl/SRxSchemaTreeControl.ps1

class SRxSchemaTreeControl : SRxTreeControl {

    hidden [string[]] $_roleKeys = @(
        'ts_KeyProvisioning','ts_TermGroup','ts_Site','ts_Sites',
        'ts_SubSite','ts_SubSites','ts_Design','ts_Environment'
    )

    [bool] $ShowCustomizations = $false
    [bool] $Verbose = $false
    [bool] $StoreOuterXml = $false
    [string] $LastLoadStats = ''
    hidden [string[]] $_treeFilter = @('Operations')
    hidden [hashtable] $_groupKeys = @{}

    SRxSchemaTreeControl() : base() { 

        $this.SetSideBorder(
            [SRxBorderSides]::Left,
            [System.Drawing.SystemColors]::ControlDark,
            [float]1.0
        )

        $this.Padding = [System.Windows.Forms.Padding]::new(1, 0, 0, 0)
        $this.LazyRealize = $true
    }

    [void] LoadFromXml($xmlRoot) {
        try {
            $this.ClearModel()
            if ($null -eq $xmlRoot) { $this.SyncToUI(); return }
            $this.prepareBuild()
            $this.addXmlChildren($xmlRoot, '')
            $this.SyncToUI()
        } catch { Write-Host "SRxSchemaTreeControl.LoadFromXml - $($_.Exception.Message)" }
    }

    hidden [void] addXmlChildren($xElement, [string]$parentId) {
        foreach ($xEl in $xElement.ChildNodes) {
            if ($xEl.LocalName -eq 'CustomProperties') { continue }

            $treeFilter = $this._treeFilter
            $nameAttr = ''
            try { if ($null -ne $xEl.Attributes['Name']) { $nameAttr = [string]$xEl.Attributes['Name'].Value } } catch { }
            if ($nameAttr -in $treeFilter) { continue }

            if ($xEl.LocalName -in @('TermSets','Terms')) {
                if ($xEl.HasChildNodes) { $this.addXmlChildren($xEl, $parentId) }
                continue
            }

            $id = ''
            try { if ($null -ne $xEl.Attributes['ID']) { $id = [string]$xEl.Attributes['ID'].Value } } catch { }
            if ([string]::IsNullOrEmpty($id)) {
                $pos = 0
                for ($k = 0; $k -lt $xElement.ChildNodes.Count; $k++) {
                    if ($xElement.ChildNodes[$k] -eq $xEl) { $pos = $k; break }
                }
                $id = ('syn:{0}:{1}' -f $parentId, $pos)
            }            

            $text = $nameAttr
            if ([string]::IsNullOrEmpty($text)) {
                $text = [string]$xEl.LocalName
                if ($text -eq 'Property') {
                    try { $text = [string]$xEl.Attributes['Key'].Value } catch { }
                }
            }

            $line = @($text -split '--')
            $title = [string]$line[0]
            $tooltip = $null
            if ($line.Count -gt 1) {
                $tooltip = [string]$line[1]
                [datetime]$logDate = New-Object DateTime
                if ([DateTime]::TryParseExact($tooltip, 'o',
                        [System.Globalization.CultureInfo]::InvariantCulture,
                        [System.Globalization.DateTimeStyles]::None, [ref]$logDate)) {
                    $tooltip = $logDate.ToString('yyyy-MM-dd HH:mm:ss')
                }
            }

            $deprecated = $false
            try {
                $s = [string]$xEl.Attributes['IsDeprecated'].Value
                if (-not [string]::IsNullOrEmpty($s)) { $deprecated = ($s -eq 'true') }
            } catch { }

            $props = @{}
            foreach ($cp in $xEl.ChildNodes) {
                if ($cp.LocalName -ne 'CustomProperties') { continue }
                foreach ($p in $cp.ChildNodes) {
                    if ($p.LocalName -ne 'Property') { continue }
                    try { $props[[string]$p.Attributes['Key'].Value] = [string]$p.Attributes['Value'].Value } catch { }
                }
            }

            $role = 'Unknown'
            foreach ($rk in $this._roleKeys) {
                if ($props.ContainsKey($rk) -and -not [string]::IsNullOrEmpty([string]$props[$rk])) {
                    if     ($rk -eq 'ts_KeyProvisioning') { $role = 'ProvisioningSchema' }
                    elseif ($rk -eq 'ts_TermGroup')       { $role = 'Root' }
                    elseif ($rk -eq 'ts_Site')            { $role = 'Site' }
                    elseif ($rk -eq 'ts_SubSite')         { $role = 'SubSite' }
                    elseif ($rk -in @('ts_Sites','ts_SubSites')) { $role = 'SitesCollection' }
                    elseif ($rk -eq 'ts_Design')          { $role = 'Design' }
                    elseif ($rk -eq 'ts_Environment')     { $role = 'Environment' }
                    break
                }
            }

            $hostId = $parentId
            if ($props.ContainsKey('ts_Group') -and -not [string]::IsNullOrEmpty([string]$props['ts_Group'])) {
                $gText = [string]$props['ts_Group']
                $gKey  = $gText
                if ($this._groupKeys.ContainsKey($gText)) { $gKey = [string]$this._groupKeys[$gText] }
                else {
                    try { $gKey = Remove-SRxStringSpecialCharacter -String $gText } catch { }
                    $this._groupKeys[$gText] = $gKey
                }
                $gid = ('{0}#{1}' -f $parentId, $gKey)
                if ($null -eq $this.GetNode($gid)) {
                    $this.AddNode($gid, $parentId, $gText, @{ role = 'Group'; synthetic = $true })
                }
                $hostId = $gid
            }

            $tag = @{
                termID           = $id
                role             = $role
                CustomProperties = $props
                Operations       = @{}
                Xml              = $xEl
                deprecated       = $deprecated
                tooltip          = $tooltip
                title            = $title
                synthetic        = ($id -like 'syn:*')
            }

            if ($this.StoreOuterXml) { $tag['OuterXml'] = [string]$xEl.OuterXml }
            $this.AddNode($id, $hostId, $text, $tag)
            if ($xEl.HasChildNodes) { $this.addXmlChildren($xEl, $id) }
        }
    }

    [string] GetRole([string]$id) {
        $n = $this.GetNode($id)
        if ($null -eq $n -or $null -eq $n.Tag) { return 'Unknown' }
        if ($n.Tag -is [hashtable] -and $n.Tag.ContainsKey('role')) { return [string]$n.Tag['role'] }
        return 'Unknown'
    }

    [string] GetOwnProp([string]$id, [string]$key) {
        $n = $this.GetNode($id)
        if ($null -eq $n -or $null -eq $n.Tag) { return '' }
        if ($n.Tag -is [hashtable] -and $n.Tag.ContainsKey('CustomProperties')) {
            $cp = $n.Tag['CustomProperties']
            if ($cp -is [hashtable] -and $cp.ContainsKey($key)) { return [string]$cp[$key] }
        }
        return ''
    }

    [string] GetEffectiveProp([string]$id, [string]$key) {
        $own = $this.GetOwnProp($id, $key)
        if (-not [string]::IsNullOrEmpty($own)) { return $own }
        foreach ($a in $this.AncestorIds($id)) {
            $v = $this.GetOwnProp($a, $key)
            if (-not [string]::IsNullOrEmpty($v)) { return $v }
        }
        return ''
    }

    [string] GetOwningId([string]$id, [string]$key) {
        if (-not [string]::IsNullOrEmpty($this.GetOwnProp($id, $key))) { return $id }
        foreach ($a in $this.AncestorIds($id)) {
            if (-not [string]::IsNullOrEmpty($this.GetOwnProp($a, $key))) { return $a }
        }
        return ''
    }

    [string] GetRoleValue([string]$id) {
        $role = $this.GetRole($id)
        switch ($role) {
            'ProvisioningSchema' { return $this.GetOwnProp($id, 'ts_KeyProvisioning') }
            'Root'               { return $this.GetOwnProp($id, 'ts_TermGroup') }
            'Site'               { return $this.GetOwnProp($id, 'ts_Site') }
            'SubSite'            { return $this.GetOwnProp($id, 'ts_SubSite') }
            'Design'             { return $this.GetOwnProp($id, 'ts_Design') }
            'Environment'        { return $this.GetOwnProp($id, 'ts_Environment') }
        }
        return ''
    }

    [boolean] IsSynthetic([string]$id) {
        if ([string]::IsNullOrEmpty($id)) { return $true }
        if ($id.Contains('#')) { return $true }
        if ($id.StartsWith('syn:')) { return $true }
        return $false
    }

    [void] ApplyDeprecatedOverlay() {
        try {
            $styles = @{}
            foreach ($id in @($this._model.Keys)) {
                $n = $this._model[$id]
                if ($null -eq $n -or -not ($n.Tag -is [hashtable])) { continue }
                if ($n.Tag.ContainsKey('deprecated') -and [bool]$n.Tag['deprecated']) {
                    $styles[[string]$id] = @{ Fore = [System.Drawing.Color]::LightGray }
                }
            }
            $this.SetOverlayStyles('deprecated', $styles)
        } catch { Write-Host "ApplyDeprecatedOverlay - $($_.Exception.Message)" }
    }
    [void] ApplyScopeOverlay([string]$scopeId) {
        try {
            $this.PushOverlay('scope')
            $this.ClearOverlayStyles('scope')
            if (-not [string]::IsNullOrEmpty($scopeId)) {
                $this.SetOverlayStyle('scope', $scopeId, @{
                    Fore      = [System.Drawing.Color]::Red
                    FontStyle = [System.Drawing.FontStyle]::Bold
                })
            }
            $this.applyAppearance()
        } catch { Write-Host "ApplyScopeOverlay - $($_.Exception.Message)" }
    }

    [void] ClearScopeOverlay() { $this.PopOverlay('scope') }

    [string[]] AllIds() {
        $out = New-Object System.Collections.ArrayList
        foreach ($rid in $this.RootIds()) { $this.collectIds($rid, $out) }
        return @($out)
    }
    hidden [void] collectIds([string]$id, $acc) {
        [void]$acc.Add($id)
        foreach ($c in $this.ChildIds($id)) { $this.collectIds($c, $acc) }
    }

    [void] LoadFromRoot($termGroupElement, [string]$rootKey, [string]$rootText) {
        try {
            $swModel = [System.Diagnostics.Stopwatch]::StartNew()
            $this.ClearModel()
            $this.prepareBuild()
            $outer = ''
            if ($null -ne $termGroupElement -and $this.StoreOuterXml) { $outer = [string]$termGroupElement.OuterXml }

            $rootTermId = ''
            if ($null -ne $termGroupElement) { $rootTermId = $rootKey }

            $this.AddNode($rootKey, '', $rootText, @{
                termID           = $rootTermId
                role             = 'Root'
                CustomProperties = @{ 'ts_TermGroup' = $rootText }
                Operations       = @{}
                OuterXml         = $outer
                Xml              = $termGroupElement
                deprecated       = $false
                tooltip          = $null
                title            = $rootText
                synthetic        = $false
            })

            if ($null -ne $termGroupElement) { $this.addXmlChildren($termGroupElement, $rootKey) }
            $modelMs = $swModel.ElapsedMilliseconds
            $swView = [System.Diagnostics.Stopwatch]::StartNew()
            $this.SyncToUI()
            $this.LastLoadStats = ('model {0} ms ({1} nodes) + view {2} ms ({3} TreeNodes realized)' -f $modelMs, $this.Count(), $swView.ElapsedMilliseconds, $this.RealizedCount())
            if (Get-Command TR -ErrorAction Ignore) { TR ('[TREE] LoadFromRoot: ' + $this.LastLoadStats) }
            if ($this.Verbose) { Write-Host ("LoadFromRoot - model built: {0} nodes" -f $this.Count()) }
        }
        catch {
            Write-Host "LoadFromRoot - FAILED (model is PARTIAL): $($_.Exception.Message)"
            try { $this.SyncToUI() } catch { }
        }
    }
    
    [object] FindUiNode([string]$id) {
        try {
            if (-not [string]::IsNullOrEmpty($id) -and $this._uiIndex.ContainsKey($id)) {
                $n = $this._uiIndex[$id]
                if ($null -ne $n -and $null -ne $n.TreeView) { return $n }
            }
            if (-not [string]::IsNullOrEmpty($id) -and $this._model.Contains($id)) { return $null }
        } catch { }
        try {
            $found = $this.InnerTree().Nodes.Find($id, $true)
            if ($found.Count -gt 0) { return $found[0] }
        } catch { }
        return $null
    }

    [object] SelectedTag() {
        $id = $this.SelectedId()
        if ([string]::IsNullOrEmpty($id)) { return $null }
        $n = $this.GetNode($id)
        if ($null -eq $n) { return $null }
        return $n.Tag
    }  
    hidden [void] prepareBuild() {
        $f = @('Operations')
        if (-not $this.ShowCustomizations) { $f += 'Customizations' }
        $this._treeFilter = $f
    }
    static [string] OuterXmlOf($tag) {
        if ($null -eq $tag -or -not ($tag -is [hashtable])) { return '' }
        $o = [string]$tag['OuterXml']
        if (-not [string]::IsNullOrEmpty($o)) { return $o }
        $x = $tag['Xml']
        if ($x -is [System.Xml.XmlNode]) { return [string]$x.OuterXml }
        return ''
    }
    [string] GetOuterXml([string]$id) {
        $n = $this.GetNode($id)
        if ($null -eq $n) { return '' }
        return [SRxSchemaTreeControl]::OuterXmlOf($n.Tag)
    }
    [void] ShowCustomizationBranches([bool]$show) {
        try {
            if ($show) {
                $this.PopOverlay('customizations-hidden')
                return
            }
            $this.PushOverlay('customizations-hidden')
            $this.ClearOverlayStyles('customizations-hidden')
            foreach ($id in $this.AllIds()) {
                if ($this.GetRole($id) -ne 'Customizations') { continue }
                $this.SetOverlayStyle('customizations-hidden', $id, @{ Hidden = $true })
            }
            $this.RebuildUI()
        } catch { Write-Host "ShowCustomizationBranches - $($_.Exception.Message)" }
    }

}