TreeControl/SRxXmlTreeControl.ps1
|
<# ================================================================================ SRxXmlTreeControl.psm1 STEP 8 - XML template tree extraction ----------------------------------------------------------------------------- The XML TEMPLATE wrapper around the generic SRxTreeControl. TWO TREES, TWO IDENTITY MODELS (the Step-1 finding, applied) Schema tree : term GUID identity + ts_* ROLE markers (semantic model) THIS tree : xPath identity + element names (XML document) There are no roles here - a PnP XML node means what its element name says. WHY xPath AND NOT THE OLD INTEGER KEY The legacy builder named nodes with a running counter ($this._key++), while the xPath - the value every lookup actually needs - lived only in .Tag. Using xPath as the model id makes computeTemplateNodeState a direct dictionary hit instead of walking TreeNodes, and it survives a rebuild. Collisions (same xPath twice) are disambiguated with a #n suffix; the TRUE xPath is always preserved in the tag, so existing lookups are unchanged. SIMULATION BECOMES AN OVERLAY applyCompositionSimulation used to REMOVE nodes, and restoreFromSimulation had to rebuild the tree (hence the _simulationActive flag and the reload it forced). Here it is PushOverlay(*simulation*) + Hidden styles, so restoring is PopOverlay - the model is never mutated and the flag is gone. REQUIRES: WinForms/Drawing loaded BEFORE Import-Module. SRxTreeControl.psm1 beside this file. ================================================================================ #> class SRxXmlTreeControl : SRxTreeControl { [bool] $Verbose = $false # xPath -> model id (ids may carry a #n suffix when an xPath repeats) hidden [System.Collections.Hashtable] $_byXPath = @{} SRxXmlTreeControl() : base() { $this.SetSideBorder( [SRxBorderSides]::Right, [System.Drawing.SystemColors]::ControlDark, [float]1.0 ) # Reserve one pixel so a DockStyle.Fill child does not cover the border. $this.Padding = [System.Windows.Forms.Padding]::new(0, 0, 1, 0) } # ======================================================== MODEL BUILD # Build the whole model from the ProvisioningTemplate element. # $xmlElement : the <pnp:ProvisioningTemplate> element # $rootXPath : "/pnp:Provisioning/pnp:Templates/pnp:ProvisioningTemplate" # $xPathFn : scriptblock { param($xEl,$parentXPath) -> [string] } supplied # by the host so formatXPath stays where the domain lives. [void] LoadFromXml($xmlElement, [string]$rootXPath, $xPathFn) { try { $this.ClearModel() $this._byXPath.Clear() if ($null -eq $xmlElement) { $this.SyncToUI(); return } $rootId = $this.reserveId($rootXPath) $this.AddNode($rootId, "", $this.headText($xmlElement, $true), ( $this.newTag($rootXPath, $xmlElement))) $this.addXmlChildren($xmlElement, $rootXPath, $rootId, $xPathFn) $this.SyncToUI() if ($this.Verbose) { Write-Host ("SRxXmlTreeControl - model built: {0} nodes" -f $this.Count()) } } catch { # a mid-walk failure leaves a PARTIAL model that still renders - say so Write-Host "SRxXmlTreeControl.LoadFromXml - FAILED (model is PARTIAL): $($_.Exception.Message)" try { $this.SyncToUI() } catch { } } } hidden [void] addXmlChildren($xElement, [string]$parentXPath, [string]$parentId, $xPathFn) { foreach ($xEl in $xElement.ChildNodes) { # skip whitespace / comment / CDATA - only ELEMENTS are tree nodes. # With PreserveWhitespace the indentation around a deleted element # survives as a text node and would render as an empty row. if ($xEl.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } $xPth = "" if ($null -ne $xPathFn) { try { $xPth = [string](& $xPathFn $xEl $parentXPath) } catch { $xPth = "" } } if ([string]::IsNullOrEmpty($xPth)) { $xPth = ($parentXPath + "/" + [string]$xEl.LocalName) } $hasKids = $false try { $hasKids = [bool]$xEl.HasChildNodes } catch { $hasKids = $false } $id = $this.reserveId($xPth) $this.AddNode($id, $parentId, $this.headText($xEl, $hasKids), ($this.newTag($xPth, $xEl))) if ($hasKids) { $this.addXmlChildren($xEl, $xPth, $id, $xPathFn) } } } # node label: opening tag only when it has children, else the whole fragment hidden [string] headText($xEl, [bool]$hasKids) { try { $outer = [string]$xEl.OuterXml if ($hasKids) { return ($outer.Split(">")[0] + ">") } return $outer } catch { return "" } } # the legacy Tag shape - every existing consumer keeps working unchanged hidden [object] newTag([string]$xPth, $xEl) { $outer = "" try { $outer = [string]$xEl.OuterXml } catch { } return (New-Object PSObject -Property @{ xPath = $xPth Filter = @{} SavedFilter = @{} ActiveScopeChanges = @{} ActiveScopeFilter = @{} OuterXml = $outer DeprecatedOnScope_In = -1 }) } # xPaths CAN repeat (same element name, no distinguishing attribute), so the # model id gets a #n suffix. The TRUE xPath always stays in the tag. hidden [string] reserveId([string]$xPth) { $id = $xPth if ($this._byXPath.ContainsKey($xPth)) { $n = [int]$this._byXPath[$xPth] $n++ $this._byXPath[$xPth] = $n $id = ("{0}#{1}" -f $xPth, $n) } else { $this._byXPath[$xPth] = 0 } return $id } # ======================================================== LOOKUPS [string] GetXPath([string]$id) { $n = $this.GetNode($id) if ($null -eq $n -or $null -eq $n.Tag) { return "" } try { return [string]$n.Tag.xPath } catch { return "" } } # first model id carrying this xPath ("" when absent) [string] IdForXPath([string]$xPth) { if ($this.GetNode($xPth)) { return $xPth } foreach ($id in $this.AllIds()) { if ($this.GetXPath($id) -eq $xPth) { return $id } } return "" } [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) } } [object] SelectedTag() { $id = $this.SelectedId() if ([string]::IsNullOrEmpty($id)) { return $null } $n = $this.GetNode($id) if ($null -eq $n) { return $null } return $n.Tag } [object] FindUiNode([string]$id) { try { $found = $this.InnerTree().Nodes.Find($id, $true) if ($found.Count -gt 0) { return $found[0] } } catch { } return $null } # ======================================================== CUSTOMIZATION STYLING # The host computes STATE (it owns the customization semantics); the control # owns PRESENTATION. $stateById : id -> @{ Fore; FontStyle; Back; Hidden } [void] ApplyCustomizationOverlay([hashtable]$styleById) { try { $this.PushOverlay('customizations') $this.ClearOverlayStyles('customizations') if ($null -ne $styleById) { foreach ($id in $styleById.Keys) { $this.SetOverlayStyle('customizations', [string]$id, [hashtable]$styleById[$id]) } } $this.applyAppearance() } catch { Write-Host "ApplyCustomizationOverlay - $($_.Exception.Message)" } } [void] ClearCustomizationOverlay() { $this.PopOverlay('customizations') } # ======================================================== SIMULATION <# # Hide the ids the host says are filtered out. No node is REMOVED, so # restoring is just dropping the overlay - no rebuild, no _simulationActive. [int] ApplySimulation([string[]]$hiddenIds) { try { $this.PushOverlay('simulation') $this.ClearOverlayStyles('simulation') $n = 0 foreach ($id in @($hiddenIds)) { if ([string]::IsNullOrEmpty([string]$id)) { continue } $this.SetOverlayStyle('simulation', [string]$id, @{ Hidden = $true }) $n++ } $this.RebuildUI() return $n } catch { Write-Host "ApplySimulation - $($_.Exception.Message)"; return 0 } } [void] ClearSimulation() { try { $this.PopOverlay('simulation') } catch { Write-Host "ClearSimulation - $($_.Exception.Message)" } } [boolean] IsSimulating() { return $this.HasOverlay('simulation') } #> # ======================================================== ROOT [void] ExpandRoot() { try { $roots = $this.RootIds() if ($roots.Count -eq 0) { return } $ui = $this.FindUiNode([string]$roots[0]) if ($null -ne $ui -and -not $ui.IsExpanded) { $ui.Expand() } } catch { Write-Host "ExpandRoot - $($_.Exception.Message)" } } } |