public/window/New-UiChildWindow.ps1
|
function New-UiChildWindow { <# .SYNOPSIS Creates a child window that automatically inherits the parent's theme. .DESCRIPTION Creates a child/nested window that uses the active theme. Supports modal and non-modal display, and allows data passing between parent and child. Windows are shown automatically: - Modal windows: Shown with ShowDialog() and return DialogResult (bool?) - Non-modal windows: Shown with Show() and return nothing - PassThru: Returns window object for manual control Child windows called from buttons run synchronously on the UI thread. No threading gymnastics required. .PARAMETER Parent Parent window. Omit it and the session's window is picked up automatically; with no session window open, the child stands alone. .PARAMETER Title Window title bar text. .PARAMETER Content ScriptBlock containing child controls. .PARAMETER Width Window width in pixels (150-2000). .PARAMETER Height Window height in pixels (100-1500). Ignored when -SizeToContent is set. .PARAMETER SizeToContent Base the height on the content instead of -Height, capped at the avaiable screen area. The window sets that height once it is on screen, so the grip and the edges still drag. .PARAMETER Modal Display as modal dialog (blocks parent until closed). .PARAMETER Position Window position: CenterOnParent, CenterOnScreen, or Manual. .PARAMETER Left Left position (for Manual positioning). .PARAMETER Top Top position (for Manual positioning). .PARAMETER NoResize Prevent user from resizing the window. Windows are resizable by default. .PARAMETER OnClosed ScriptBlock to execute when window closes. .PARAMETER PassThru Return the window object instead of displaying it automatically. .PARAMETER WPFProperties Hashtable of additional WPF properties to set on the control. Allows setting any valid WPF property not explicitly exposed as a parameter. Bad values warn and get skipped. A property name that does not exist on the control is skipped silently (-Verbose shows it). Nothing stops execution. Supports attached properties using dot notation (e.g., "Grid.Row"). .EXAMPLE # Modal dialog - parent is auto-detected from session New-UiButton -Text "Open Settings" -NoAsync -Action { $result = New-UiChildWindow -Title 'Settings' -Modal -Content { New-UiLabel -Text 'Configure settings' New-UiButton -Text 'Save' -NoAsync -Action { # Setting DialogResult closes a modal by itself. Do NOT also call Close(). The second close lands on a window mid teardown and throws. (Get-UiSession).Window.DialogResult = $true } } if ($result) { Write-Host "User clicked Save" } } .EXAMPLE # Non-modal window - parent auto-detected New-UiButton -Text "Show Monitor" -NoAsync -Action { New-UiChildWindow -Title 'Status Monitor' -Width 300 -Height 200 -Content { New-UiLabel -Text 'Monitoring...' New-UiButton -Text "Close" -NoAsync -Action { Close-UiWindow } } } .EXAMPLE # Shared data between windows via reference type $counter = @{ Value = 0 } New-UiButton -Text "Open Counter" -LinkedVariables 'counter' -NoAsync -Action { New-UiChildWindow -Title "Counter" -Content { New-UiButton -Text "Increment" -NoAsync -Action { $counter.Value++ (Get-UiSession).Window.Title = "Counter: $($counter.Value)" } } } .NOTES Closing a child closes the windows it owns first and an output window still running asks before it closes. A Closing handler of your own that cancels keeps them all open only if it was attached before the child was shown. #> [CmdletBinding()] param( [System.Windows.Window]$Parent, [string]$Title = 'Child Window', [Parameter(Mandatory)] [scriptblock]$Content, [ValidateRange(150, 2000)] [int]$Width = 400, [ValidateRange(100, 1500)] [int]$Height = 300, [switch]$SizeToContent, [switch]$Modal, [ValidateSet('CenterOnParent', 'CenterOnScreen', 'Manual')] [string]$Position = 'CenterOnParent', [System.Nullable[int]]$Left, [System.Nullable[int]]$Top, [switch]$NoResize, [scriptblock]$OnClosed, [switch]$PassThru, [Parameter()] [hashtable]$WPFProperties ) # Reject empty content scriptblock early if ([string]::IsNullOrWhiteSpace($Content.ToString())) { throw "New-UiChildWindow: The -Content scriptblock is empty. Add UI controls inside the block." } # Capture the calling script's session state for variable resolution $callerSessionState = $PSCmdlet.SessionState # Auto-detect parent window if not provided if (!$Parent) { $session = Get-UiSession if ($session -and $session.Window) { $Parent = $session.Window Write-Verbose "[New-UiChildWindow] Auto-detected parent window from session" } } $colors = if (Test-Path variable:__WPFThemeColors) { Get-Variable -Name __WPFThemeColors -ValueOnly -ErrorAction SilentlyContinue } else { $null } if (!$colors) { $colors = Get-ThemeColors } if (!$colors) { $colors = @{ WindowBg = '#FFFFFF' WindowFg = '#1A1A1A' ControlBg = '#F0F0F0' ControlFg = '#000000' } } # The session this child takes the thread from, and hands it back to $parentSessionId = [PsUi.SessionManager]::CurrentSessionId $session = Initialize-UiSession if (!$session) { Write-Error "Failed to initialize WPF session for child window" return } # Capture child session ID for cleanup $childSessionId = [PsUi.SessionManager]::CurrentSessionId $startupLocation = switch ($Position) { 'CenterOnParent' { if ($Parent) { 'CenterOwner' } else { 'CenterScreen' } } 'CenterOnScreen' { 'CenterScreen' } 'Manual' { 'Manual' } default { 'CenterScreen' } } # Create the window with custom chrome for shadow support # Add padding to dimensions to accommodate shadow margin $shadowPadding = 16 $window = [System.Windows.Window]@{ Title = $Title Width = $Width + ($shadowPadding * 2) Height = $Height + ($shadowPadding * 2) MinWidth = 200 + ($shadowPadding * 2) MinHeight = 150 + ($shadowPadding * 2) WindowStartupLocation = $startupLocation FontFamily = [System.Windows.Media.FontFamily]::new('Segoe UI') Background = [System.Windows.Media.Brushes]::Transparent Foreground = ConvertTo-UiBrush $colors.WindowFg ResizeMode = if ($NoResize) { 'NoResize' } else { 'CanResize' } WindowStyle = 'None' AllowsTransparency = $true Opacity = 0 } # Straight from a console there's no Application to hold the styles if (![System.Windows.Application]::Current) { [PsUi.ThemeEngine]::ApplyStandaloneTheme($window, $colors) } # Two field forms leave most of a fixed height window empty, so the height comes off the content unless one is passed. # Capped to the parent's monitor, since SystemParameters.WorkArea is the primary screen only and a tall window on a shorter second screen runs off the bottom. if ($SizeToContent) { $ownerHandle = if ($Parent) { [System.Windows.Interop.WindowInteropHelper]::new($Parent).Handle } else { [IntPtr]::Zero } $ownerSource = if ($Parent) { [System.Windows.PresentationSource]::FromVisual($Parent) } # The monitor's work area comes back in device pixels, MaxHeight wants DIPs $toDip = if ($ownerSource) { $ownerSource.CompositionTarget.TransformFromDevice.M22 } else { 1 } $window.MaxHeight = [PsUi.WindowManager]::GetWorkAreaForWindow($ownerHandle).Height * $toDip $window.SizeToContent = 'Height' } [PsUi.WindowManager]::SetWindowAppId($window, 'PsUi.ChildWindow') # Create custom window icon (inherit parent's custom logo if set) $childWindowIcon = $null try { $parentSession = Get-UiSession if ($parentSession.CustomLogo -and (Test-Path $parentSession.CustomLogo)) { $childWindowIcon = Get-CustomLogoIcon -Path $parentSession.CustomLogo } else { $childWindowIcon = New-WindowIcon -Colors $colors } if ($childWindowIcon) { $window.Icon = $childWindowIcon } } catch { Write-Verbose "Failed to create window icon: $_" } if ($Parent) { try { $window.Owner = $Parent } catch { Write-Verbose "[New-UiChildWindow] Could not set Owner: $_" # Adjust the startup location when the owner could not be set if ($startupLocation -eq 'CenterOwner') { $window.WindowStartupLocation = 'CenterScreen' } } } $shadowBorder = [System.Windows.Controls.Border]@{ Margin = [System.Windows.Thickness]::new($shadowPadding) Background = ConvertTo-UiBrush $colors.WindowBg BorderBrush = ConvertTo-UiBrush $colors.Border BorderThickness = [System.Windows.Thickness]::new(1) } $shadow = [System.Windows.Media.Effects.DropShadowEffect]@{ BlurRadius = 16 ShadowDepth = 4 Opacity = 0.35 Color = [System.Windows.Media.Colors]::Black Direction = 270 } $shadowBorder.Effect = $shadow $window.Content = $shadowBorder # A Grid as the container, so a resize grip can sit on top $containerGrid = [System.Windows.Controls.Grid]::new() $shadowBorder.Child = $containerGrid # Main layout DockPanel inside the shadow border $outerPanel = [System.Windows.Controls.DockPanel]@{ LastChildFill = $true } [void]$containerGrid.Children.Add($outerPanel) # Add resize grip for borderless window (only if resizable) if (!$NoResize) { $resizeGrip = [System.Windows.Controls.Primitives.ResizeGrip]@{ HorizontalAlignment = 'Right' VerticalAlignment = 'Bottom' Cursor = [System.Windows.Input.Cursors]::SizeNWSE } [void]$containerGrid.Children.Add($resizeGrip) # Calculate minimum dimensions (must match window's MinWidth/MinHeight) $minResizeWidth = 200 + ($shadowPadding * 2) $minResizeHeight = 150 + ($shadowPadding * 2) $resizeGrip.Add_MouseLeftButtonDown({ param($sender, $eventArgs) $sender.CaptureMouse() $eventArgs.Handled = $true }) $capturedWindowForResize = $window $resizeGrip.Add_MouseMove({ param($sender, $eventArgs) if ($sender.IsMouseCaptured) { $mousePos = [System.Windows.Input.Mouse]::GetPosition($capturedWindowForResize) $newWidth = [Math]::Max($minResizeWidth, $mousePos.X) $newHeight = [Math]::Max($minResizeHeight, $mousePos.Y) $capturedWindowForResize.Width = $newWidth $capturedWindowForResize.Height = $newHeight $eventArgs.Handled = $true } }.GetNewClosure()) $resizeGrip.Add_MouseLeftButtonUp({ param($sender, $eventArgs) $sender.ReleaseMouseCapture() $eventArgs.Handled = $true }) } # Custom title bar for drag support and close button $titleBar = [System.Windows.Controls.Border]@{ Background = ConvertTo-UiBrush $colors.HeaderBackground Height = 36 Padding = [System.Windows.Thickness]::new(12, 0, 4, 0) } [System.Windows.Controls.DockPanel]::SetDock($titleBar, 'Top') $titleGrid = [System.Windows.Controls.Grid]::new() $titleBar.Child = $titleGrid # WindowStyle='None' on the child window means the OS-native title-bar icon never renders and custom chrome has to draw its own. Copy the same 16x16 pattern as the main window's BuildTitleBar in NewUiWindowCommand.Builder.cs. # Skip the Image if no icon was resolved above. $titleTextLeftMargin = 0 if ($childWindowIcon) { $titleIcon = [System.Windows.Controls.Image]@{ Source = $childWindowIcon Width = 16 Height = 16 HorizontalAlignment = 'Left' VerticalAlignment = 'Center' } [void]$titleGrid.Children.Add($titleIcon) $titleTextLeftMargin = 24 } $titleText = [System.Windows.Controls.TextBlock]@{ FontSize = 13 FontWeight = 'SemiBold' Foreground = ConvertTo-UiBrush $colors.HeaderForeground VerticalAlignment = 'Center' Margin = [System.Windows.Thickness]::new($titleTextLeftMargin, 0, 0, 0) } # Lets (Get-UiSession).Window.Title set from an action retitle the drawn title bar too $titleBinding = [System.Windows.Data.Binding]::new('Title') $titleBinding.Source = $window [void]$titleText.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, $titleBinding) [void]$titleGrid.Children.Add($titleText) # Close button with red hover effect # Foreground is set inside the template - do NOT use a property setter on the button, it creates a local value that overrides template trigger setters after theme changes. $closeBtn = [System.Windows.Controls.Button]@{ Content = [PsUi.ModuleContext]::GetIcon('Close') FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily FontSize = 10 Width = 36 Height = 36 HorizontalAlignment = 'Right' Background = [System.Windows.Media.Brushes]::Transparent BorderThickness = [System.Windows.Thickness]::new(0) Cursor = [System.Windows.Input.Cursors]::Hand } $closeBtn.OverridesDefaultStyle = $true # Apply hover template (red background, white foreground on hover) $closeBtnTemplate = @' <ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" TargetType="Button"> <Border x:Name="border" Background="Transparent"> <ContentPresenter x:Name="content" HorizontalAlignment="Center" VerticalAlignment="Center" TextElement.Foreground="{DynamicResource HeaderForegroundBrush}"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="border" Property="Background" Value="#E81123"/> <Setter TargetName="content" Property="TextElement.Foreground" Value="White"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> '@ $closeBtn.Template = [System.Windows.Markup.XamlReader]::Parse($closeBtnTemplate) $capturedWindow = $window $capturedModal = $Modal $closeBtn.Add_Click({ # Setting DialogResult already closes a modal. Close() on top of it re-enters teardown on a disposing window and throws an NRE out of the second pass. if ($capturedModal) { $capturedWindow.DialogResult = $false } else { $capturedWindow.Close() } }.GetNewClosure()) [void]$titleGrid.Children.Add($closeBtn) $titleBar.Add_MouseLeftButtonDown({ $capturedWindow.DragMove() }.GetNewClosure()) [void]$outerPanel.Children.Add($titleBar) $dockPanel = [System.Windows.Controls.DockPanel]::new() $scrollViewer = [System.Windows.Controls.ScrollViewer]::new() $scrollViewer.VerticalScrollBarVisibility = 'Auto' $scrollViewer.HorizontalScrollBarVisibility = 'Disabled' $contentStack = [System.Windows.Controls.StackPanel]::new() $contentStack.Margin = [System.Windows.Thickness]::new(16, 12, 16, 12) $scrollViewer.Content = $contentStack [void]$dockPanel.Children.Add($scrollViewer) [void]$outerPanel.Children.Add($dockPanel) $session.Window = $window $session.CurrentParent = $contentStack $controlName = "ChildWindow_$([Guid]::NewGuid().ToString('N').Substring(0, 8))" Register-UiControl -Name $controlName -Control $window Register-UiControl -Name "${controlName}_ContentStack" -Control $contentStack # Build the content using dot-sourcing to run in current scope # Capture variables from the calling scope that are referenced in Content try { $capturedVars = @{} $ast = $Content.Ast # Find all variable references in the Content scriptblock $varExpressions = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.VariableExpressionAst] }, $true) # Variables to exclude (built-ins, scope-qualified, etc.) $excludeVars = @( '_', 'args', 'Error', 'false', 'Host', 'input', 'null', 'PSBoundParameters', 'PSCmdlet', 'PSScriptRoot', 'PSVersionTable', 'true', 'env', 'this', 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'session', 'parent', 'parentSession', 'callerSessionState', 'colors' ) foreach ($varExpr in $varExpressions) { $varName = $varExpr.VariablePath.UserPath if ($excludeVars -contains $varName) { continue } if ($capturedVars.ContainsKey($varName)) { continue } # Skip scope-qualified variables if ($varExpr.VariablePath.IsScript -or $varExpr.VariablePath.IsGlobal -or $varExpr.VariablePath.IsLocal -or $varExpr.VariablePath.IsPrivate) { continue } # Try to get the variable from the calling scope try { $var = $callerSessionState.PSVariable.Get($varName) if ($null -ne $var) { $capturedVars[$varName] = $var.Value } } catch { Write-Debug "Variable capture failed for '$varName': $_" } } # Inject captured variables into current scope before running Content foreach ($key in $capturedVars.Keys) { Set-Variable -Name $key -Value $capturedVars[$key] -Scope Local } Write-Verbose "[New-UiChildWindow] Captured $($capturedVars.Count) variables from caller scope" } catch { Write-Verbose "[New-UiChildWindow] Variable capture failed: $_" } try { Write-Debug "Executing content block" # Inside a window the block comes from New-UiWindow's generated text without a file, so Invoke-UiContent takes the file and line from the window's session $parentSession = [PsUi.SessionManager]::GetSession($parentSessionId) if ($parentSession -and !$session.CallerScriptName) { $session.CallerScriptName = $parentSession.CallerScriptName $session.CallerScriptLine = $parentSession.CallerScriptLine } Invoke-UiContent -Content $Content -CallerName 'New-UiChildWindow' } catch { # The window never got shown, so the Add_Closed restore below was never attached. Undo the child session and hand the parent back by hand - Clear-UiSession would leave the calling script on a fresh empty session and every later Get-UiSession on this thread would miss the parent. if ($childSessionId -ne [Guid]::Empty) { [PsUi.SessionManager]::DisposeSession($childSessionId) } if ($parentSessionId -ne [Guid]::Empty -and [PsUi.SessionManager]::GetSession($parentSessionId)) { [PsUi.SessionManager]::SetCurrentSession($parentSessionId) $Global:__PsUiSessionId = $parentSessionId.ToString() } else { Remove-Variable -Name __PsUiSessionId -Scope Global -ErrorAction SilentlyContinue } # After the cleanup, since it throws under Stop. Write-Error $_ rewraps it on 5.1 with a new error id. $PSCmdlet.WriteError($_) return $null } # Resolve private function ahead of time so the closure below carries the resolved CommandInfo. # WPF event handlers fire under the dispatcher's session state, which doesn't see module-private functions even though the script block was defined inside the module. Capturing here sidesteps the lookup at fire time. $setUiResourcesCmd = Get-Command Set-UIResources -ErrorAction SilentlyContinue # Set up window load event for fade-in and theming $window.Add_Loaded({ # Set the height the content came out at, so the grip and the edges drag it from here. if ($this.SizeToContent -ne 'Manual') { $this.Height = $this.ActualHeight $this.SizeToContent = 'Manual' } # Apply manual positioning if specified if ($Position -eq 'Manual') { if ($null -ne $Left) { $this.Left = $Left } if ($null -ne $Top) { $this.Top = $Top } } # Apply title bar theming using Set-UIResources (same as main window) if ($setUiResourcesCmd) { & $setUiResourcesCmd -Window $this -Colors $colors -IconPath $null } # Force the taskbar onto the themed icon (needs the window handle) if ($childWindowIcon) { [PsUi.WindowManager]::SetTaskbarIcon($this, $childWindowIcon) } # Fade-in animation with easing $fadeIn = [System.Windows.Media.Animation.DoubleAnimation]@{ From = 0 To = 1 Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(350)) } $fadeIn.EasingFunction = [System.Windows.Media.Animation.QuadraticEase]@{ EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut } $this.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn) }.GetNewClosure()) if ($OnClosed) { $invokeCallback = ${function:Invoke-UiCallback} # trap, not try/catch/finally. Off the pipeline a finally NREs, and this handler runs after the window is already gone, so a throw here is silent and kills the rest of the teardown. $window.Add_Closed({ trap { Write-Warning "New-UiChildWindow OnClosed error: $_"; continue } $null = & $invokeCallback -ScriptBlock $OnClosed -Label 'New-UiChildWindow OnClosed' }.GetNewClosure()) } # ShowDialog never returns once its owner is gone $closedAlong = [System.Collections.Generic.List[object]]::new() $closingHandler = [System.ComponentModel.CancelEventHandler]{ param($sender, $eventArgs) # The script's own unsaved changes guard already said no if ($eventArgs.Cancel) { return } foreach ($owned in @($sender.OwnedWindows)) { # Close() throws on a window that's already closing, so trap and continue & { trap { Write-Debug "Closing an owned window: $_"; continue } $owned.Close() } # Still up means it said No or it's still asking if ($owned.IsVisible) { $eventArgs.Cancel = $true break } $closedAlong.Add($owned) } }.GetNewClosure() $window.Add_Closing($closingHandler) # Added again from inside Show(), behind any guard the script put on the -PassThru window $window.Add_Loaded({ $this.Remove_Closing($closingHandler) $this.Add_Closing($closingHandler) }.GetNewClosure()) # Nearest opener first if (!$script:_childSessionOpeners) { $script:_childSessionOpeners = @{} } $openerMap = $script:_childSessionOpeners $openers = [System.Collections.Generic.List[Guid]]::new() if ($parentSessionId -ne [Guid]::Empty) { $openers.Add($parentSessionId) if ($openerMap[$parentSessionId]) { $openers.AddRange($openerMap[$parentSessionId]) } } $openerMap[$childSessionId] = $openers $capturedParent = $Parent $window.Add_Closed({ # Leave the thread alone while a later child holds it $holdsThread = [PsUi.SessionManager]::CurrentSessionId -eq $childSessionId -or $Global:__PsUiSessionId -eq $childSessionId.ToString() # Still ours if it sits on an Out-Datagrid this window took down $threadSession = [PsUi.SessionManager]::Current if ($threadSession -and $threadSession.Window -and $closedAlong.Contains($threadSession.Window)) { $holdsThread = $true } if ($childSessionId -ne [Guid]::Empty) { [PsUi.SessionManager]::DisposeSession($childSessionId) } [void]$openerMap.Remove($childSessionId) if ($holdsThread) { # The opener can be gone already when -Parent pointed past it $landing = $null foreach ($openerId in $openers) { if ([PsUi.SessionManager]::GetSession($openerId)) { $landing = $openerId break } } # The implicit window check reads a dead id in the global as a window still open if ($landing) { [PsUi.SessionManager]::SetCurrentSession($landing) $Global:__PsUiSessionId = $landing.ToString() } else { Remove-Variable -Name __PsUiSessionId -Scope Global -ErrorAction SilentlyContinue } } # Activate throws on a closed or unshown parent if ($capturedParent -and $capturedParent.IsVisible) { $capturedParent.Activate() } }.GetNewClosure()) # New-UiWindow's key listener never sees this window's hotkeys $hotkeyInvoker = ${function:Invoke-UiHotkeyAction} $window.Add_PreviewKeyDown({ param($sender, $eventArgs) if ($eventArgs.Handled) { return } $keySession = [PsUi.SessionManager]::GetSession($childSessionId) if (!$keySession -or @($keySession.GetRegisteredHotkeys()).Count -eq 0) { return } # Plain keys in an editable box are typing, except F1 to F24 $modifiers = [System.Windows.Input.Keyboard]::Modifiers $key = if ($eventArgs.Key -eq 'System') { $eventArgs.SystemKey } else { $eventArgs.Key } $target = $eventArgs.OriginalSource $isTyping = ($target -is [System.Windows.Controls.Primitives.TextBoxBase] -and !$target.IsReadOnly) -or $target -is [System.Windows.Controls.PasswordBox] $isFnKey = $key -ge [System.Windows.Input.Key]::F1 -and $key -le [System.Windows.Input.Key]::F24 if ($isTyping -and !$isFnKey -and !($modifiers -band ([System.Windows.Input.ModifierKeys]::Control -bor [System.Windows.Input.ModifierKeys]::Alt))) { return } $combo = [System.Collections.Generic.List[string]]::new() if ($modifiers -band [System.Windows.Input.ModifierKeys]::Control) { $combo.Add('Ctrl') } if ($modifiers -band [System.Windows.Input.ModifierKeys]::Alt) { $combo.Add('Alt') } if ($modifiers -band [System.Windows.Input.ModifierKeys]::Shift) { $combo.Add('Shift') } $combo.Add("$key") $hotkeyContext = $keySession.GetHotkeyAction($combo -join '+') if (!$hotkeyContext) { return } $eventArgs.Handled = $true & $hotkeyInvoker -Context $hotkeyContext }.GetNewClosure()) if ($WPFProperties) { Set-UiProperties -Control $window -Properties $WPFProperties } if ($PassThru) { return $window } elseif ($Modal) { # Until ShowDialog returns, the child's handlers write to the opener's error stream, and a -NoAsync opener would list their errors in its own dialog return Invoke-UiCallback -ScriptBlock { $window.ShowDialog() } -Label 'New-UiChildWindow' } else { [void]$window.Show() } } |