private/10-context-events.ps1
|
# --------------------------------------------------------------- context and events -- function New-UiContext { param([string]$Title, [string]$Theme, [string]$Css, [string]$Assets, [scriptblock]$OnError) $root = [pscustomobject]@{ Markup = (New-Object System.Text.StringBuilder); Setups = (New-Object System.Collections.ArrayList) } return [pscustomobject]@{ App = $null; Window = $null; Document = $null; Title = $Title Sequence = 0; Target = $root; Root = $root Handlers = @{}; Subscriptions = @{}; Bindings = (New-Object System.Collections.ArrayList) Timers = (New-Object System.Collections.ArrayList); Tasks = (New-Object System.Collections.ArrayList) ThemeSheet = $null; VisualCss = $null; Css = $Css; Theme = $Theme; Assets = $Assets CloseRequest = (New-Object System.Collections.ArrayList); Closed = (New-Object System.Collections.ArrayList) OnError = $OnError; ErrorCount = 0; LastError = $null; Running = $false; Stopping = $false Owners = @{}; CurrentOwner = $null } } function Write-UiError { param($ErrorRecord, [string]$Context) $ctx = $script:UiBuilder if ($null -eq $ctx) { Write-Warning "UI4ME: $Context failed: $ErrorRecord"; return } $ctx.ErrorCount = [int]$ctx.ErrorCount + 1 $ctx.LastError = "$Context`: $ErrorRecord" if ($ctx.OnError) { try { $null = & $ctx.OnError $ErrorRecord $Context } catch { Write-Warning "UI4ME: OnError handler failed: $_" } return } [Console]::Error.WriteLine("UI4ME: error in $Context`: $ErrorRecord") if ($ctx.Document -and -not $ctx.Stopping) { try { Show-UI4MEUiToast -Text "$Context`: $($ErrorRecord.Exception.Message)" -Tone danger -Seconds 8 } catch { } } } function Invoke-UiHandler { param($Handler, [object[]]$Arguments, [string]$Context) try { $null = Invoke-UiCallable $Handler $Arguments } catch { Write-UiError $_ $Context } } function New-UiCallable { # Internal callback: a module function name plus captured data. Unlike .GetNewClosure() # blocks, these keep access to module-private functions and state. param([string]$Fn, $Data) return [pscustomobject]@{ Fn = $Fn; Data = $Data } } function Invoke-UiCallable { param($Callable, [object[]]$Arguments = @()) if ($Callable -is [scriptblock]) { return (& $Callable @Arguments) } return (& $Callable.Fn $Callable.Data @Arguments) } function Register-UiHandler { # Delegated routing: one Core subscription per event type, dispatched along the event path. param([string]$Element, [string]$Type, $Handler) $ctx = Assert-UiBuilder if (-not $ctx.Handlers.ContainsKey($Type)) { $ctx.Handlers[$Type] = @{} } $byElement = $ctx.Handlers[$Type] if (-not $byElement.ContainsKey($Element)) { $byElement[$Element] = New-Object System.Collections.ArrayList } $entry = [pscustomobject]@{ Element = $Element; Type = $Type; Handler = $Handler; Owner = $ctx.CurrentOwner } [void]$byElement[$Element].Add($entry) if ($null -ne $ctx.CurrentOwner) { [void]$ctx.CurrentOwner.Handlers.Add($entry) } if ($null -ne $ctx.Document) { Initialize-UiSubscription $Type } return $entry } function Initialize-UiSubscription { param([string]$Type) $ctx = $script:UiBuilder if ($ctx.Subscriptions.ContainsKey($Type)) { return } $ctx.Subscriptions[$Type] = Register-UI4MEDocumentEvent -Document $ctx.Document -Type $Type -Callback { param($evt) Invoke-UiDispatch $evt } } function Invoke-UiDispatch { param($evt) $ctx = $script:UiBuilder if ($null -eq $ctx -or $ctx.Stopping) { return } $type = [string]$evt.type if (-not $ctx.Handlers.ContainsKey($type)) { return } $byElement = $ctx.Handlers[$type] $targets = New-Object System.Collections.ArrayList if ($evt.PSObject.Properties['element'] -and $evt.element) { [void]$targets.Add([string]$evt.element) } if ($evt.PSObject.Properties['path'] -and $evt.path) { foreach ($p in @($evt.path)) { if ($p -and -not $targets.Contains([string]$p)) { [void]$targets.Add([string]$p) } } } foreach ($target in $targets) { if (-not $byElement.ContainsKey($target)) { continue } foreach ($entry in @($byElement[$target])) { if ($null -ne $entry.Owner -and $entry.Owner.Disposed) { continue } $previous = $ctx.CurrentOwner $ctx.CurrentOwner = $entry.Owner try { Invoke-UiHandler $entry.Handler @($evt) "$type on #$target" } finally { $ctx.CurrentOwner = $previous } } } } function Remove-UiOwner { # Dispose runtime content: its handlers, bindings, timers and tasks stop. param($Owner) $ctx = $script:UiBuilder if ($null -eq $Owner -or $Owner.Disposed) { return } $Owner.Disposed = $true foreach ($entry in @($Owner.Handlers)) { if ($ctx.Handlers.ContainsKey($entry.Type) -and $ctx.Handlers[$entry.Type].ContainsKey($entry.Element)) { $list = $ctx.Handlers[$entry.Type][$entry.Element] [void]$list.Remove($entry) if ($list.Count -eq 0) { [void]$ctx.Handlers[$entry.Type].Remove($entry.Element) } } } foreach ($cleanup in @($Owner.Cleanups)) { try { $null = Invoke-UiCallable $cleanup } catch { } } foreach ($child in @($Owner.Children)) { Remove-UiOwner $child } # Drop the disposed owner from its parent: a long-lived item whose When branch is rebuilt # repeatedly must not accumulate disposed children. if ($null -ne $Owner.Parent -and -not $Owner.Parent.Disposed) { [void]$Owner.Parent.Children.Remove($Owner) } } function New-UiOwner { param([AllowNull()]$Parent) $owner = [pscustomobject]@{ Handlers = (New-Object System.Collections.ArrayList) Cleanups = (New-Object System.Collections.ArrayList); Children = (New-Object System.Collections.ArrayList) Disposed = $false; Parent = $Parent } if ($null -ne $Parent) { [void]$Parent.Children.Add($owner) } return $owner } function Register-UiBinding { # Apply $Apply (param($ctx, $element, $value)) whenever $State changes, once the document exists. # $Apply: module function name taking ($ctx, $element, $value, $data). param($State, [string]$Element, [string]$Apply, $Data) $ctx = Assert-UiBuilder $binding = [pscustomobject]@{ State = $State; Element = $Element; Apply = $Apply; Data = $Data; Owner = $ctx.CurrentOwner; Token = $null } $binding.Token = $State.Subscribe((New-UiCallable 'Invoke-UiBinding' $binding)) [void]$ctx.Bindings.Add($binding) if ($null -ne $ctx.CurrentOwner) { [void]$ctx.CurrentOwner.Cleanups.Add((New-UiCallable 'Remove-UiBinding' $binding)) } return $binding } function Invoke-UiBinding { param($Binding, $Value) $c = $script:UiBuilder if ($null -eq $c -or $null -eq $c.Document -or $c.Stopping) { return } if ($null -ne $Binding.Owner -and $Binding.Owner.Disposed) { return } # Content rebuilt by this binding (When/Each) belongs to the binding's owner. $previous = $c.CurrentOwner $c.CurrentOwner = $Binding.Owner try { & $Binding.Apply $c $Binding.Element $Value $Binding.Data } finally { $c.CurrentOwner = $previous } } function Remove-UiBinding { param($Binding) $Binding.State.Unsubscribe($Binding.Token) [void]$script:UiBuilder.Bindings.Remove($Binding) } function Set-UiTextBinding { param($ctx, $element, $value, $data) Set-UI4MEDocumentText -Document $ctx.Document -Element $element -Text ([string]$value) } function Set-UiHiddenBinding { param($ctx, $element, $value, $data) if ([bool]$value) { Set-UI4MEDocumentAttribute -Document $ctx.Document -Element $element -Name hidden -Remove } else { Set-UI4MEDocumentAttribute -Document $ctx.Document -Element $element -Name hidden -Value '' } } function Set-UiDisabledBinding { param($ctx, $element, $value, $data) if ([bool]$value) { Set-UI4MEDocumentAttribute -Document $ctx.Document -Element $element -Name disabled -Value '' } else { Set-UI4MEDocumentAttribute -Document $ctx.Document -Element $element -Name disabled -Remove } } function Set-UiValueBinding { param($ctx, $element, $value, $data) # Never echo a value that came from the control itself: the user may already have typed # further characters, and writing the older value back would erase them. if ($null -ne $data -and $data.HasShadow -and (Test-UiSameValue $value $data.Shadow)) { return } $text = if ($null -eq $value) { '' } elseif ($value -is [double] -or $value -is [int]) { [System.Convert]::ToString($value, [System.Globalization.CultureInfo]::InvariantCulture) } else { [string]$value } if ($null -ne $data) { $data.Shadow = $value; $data.HasShadow = $true } Set-UI4MEDocumentValue -Document $ctx.Document -Element $element -Value $text } function Test-UiSameValue { param($a, $b) if ($null -eq $a -or $null -eq $b) { return ($null -eq $a -and $null -eq $b) } return ([string]$a -ceq [string]$b) } function Set-UiCheckedBinding { param($ctx, $element, $value, $data) Set-UI4MEChecked -Document $ctx.Document -Element $element -Checked ([bool]$value) } function Invoke-UiSetups { param($Setups) $ctx = $script:UiBuilder foreach ($setup in @($Setups)) { try { $null = Invoke-UiCallable $setup @($ctx) } catch { Write-UiError $_ 'component setup' } } foreach ($type in @($ctx.Handlers.Keys)) { Initialize-UiSubscription $type } } |