Public/Start-RpRemotePro.ps1
function Start-RpRemotePro { <# .SYNOPSIS Launches the RemotePro WPF GUI for managing remote connections, settings, and Milestone VMS tools. .DESCRIPTION The Start-RpRemotePro function initializes the full RemotePro GUI, including theme loading, XAML parsing, event wiring, and PowerShell runspace infrastructure. It supports parallel task execution via runspaces, responsive UI with Material Design, and integration with modules like MilestonePSTools and ImportExcel. This function can relaunch itself in a separate terminal session (if not already running) and cleanly manages GUI lifecycle, splash screen loading, and system tray integration. .PARAMETER ShowTerminal (Optional) Launches the RemotePro GUI in a PowerShell console window with terminal output visible. Use this for debugging or interactive development. .COMPONENT RemoteProGUI .EXAMPLE Start-RpRemotePro Launches the RemotePro WPF interface in a new hidden PowerShell process. Used for standard user execution. .EXAMPLE Start-RpRemotePro -ShowTerminal Starts the RemotePro GUI in a visible PowerShell terminal window. Useful for development or logging. .EXAMPLE Start-RpRemotePro Initializes the GUI, splash screen, theme resources, runspace infrastructure, and system tray icon. Waits until the window is closed before terminating. .NOTES - Uses a singleton check to ensure only one instance of the WPF application exists per session. - Supports dynamic theme loading from XAML resource dictionaries. - Parses XAML into WPF elements and binds them to `$script:` variables. - Attaches dozens of event handlers from RemotePro.EventHandlers and RemotePro.RunspaceEvents. - Adds system tray icon with menu support for graceful exit behavior. - Manages background runspaces with a dispatcher timer for collecting results. - Uses dispatcher-safe UI updates and avoids blocking calls on the main thread. - Cleans up timers, UI windows, and memory during shutdown or exception. .LINK https://write-verbose.com/2023/03/21/PowerShellWPFPt1/ .LINK https://gist.github.com/proxb/6bc718831422df3392c4 .LINK https://www.milestonepstools.com/ .LINK https://github.com/dfinke/ImportExcel .LINK https://github.com/EvotecIT/PSWriteHTML/blob/master/Docs/Out-HtmlView.md .LINK https://www.remotepro.dev/en-US/Start-RpRemotePro #> [CmdletBinding()] param( [switch]$ShowTerminal ) begin { # Check if we are already in a RemotePro session if (-not $env:REMOTE_PRO_SESSION) { $env:REMOTE_PRO_SESSION = $true $baseCommand = 'if (-not (Get-Module -Name RemotePro)) { Import-Module RemotePro -Force };' if ($ShowTerminal) { $launchCommand = "$baseCommand Start-RpRemotePro -ShowTerminal" Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "`"$launchCommand`"" -WindowStyle Normal } else { $killVar = '$script:KillRemoteProOnExit = $true;' $fullCommand = "$killVar $baseCommand Start-RpRemotePro" Start-Process powershell.exe -ArgumentList "-Command", "`"$fullCommand`"" -WindowStyle Hidden } Stop-Process -Id $PID return } # Show splash screen $splashWindow = Show-RpSplashScreen $splashWindow.Show() # WPF Application Singleton check $app = [System.Windows.Application]::Current if (-not $app) { try { $app = [System.Windows.Application]::new() } catch { if ($_.Exception.Message -like "*Cannot create more than one System.Windows.Application*") { Write-Warning "Detected existing WPF Application. Relaunching in isolated session..." # Fallback: Always show terminal if we hit this (safe default) $launchCommand = 'Import-Module RemotePro -Force; Start-RpRemotePro -ShowTerminal' Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", $launchCommand -WindowStyle Normal Stop-Process -Id $PID return } else { Write-Warning "WPF Application failed to create: $_" } } } } process { try { [System.GC]::Collect() # Ensure scriptRoot exists if (-not $script:scriptRoot) { $thisModule = Get-Module RemotePro if ($thisModule) { $script:scriptRoot = $thisModule.ModuleBase Write-Verbose "Fallback: Set scriptRoot to $($script:scriptRoot) from ModuleBase" } else { throw "scriptRoot is not defined and cannot be resolved." } } # Ensure the RemotePro controller and runspace objects are initialized if (-not $script:RemotePro) { $script:RemotePro = New-RpControllerObject } if (-not $script:RpOpenRunspaces) { $script:RpOpenRunspaces = Initialize-RpOpenRunspaces } if (-not $script:RpRunspaceJobs) { $script:RpRunspaceJobs = Initialize-RpRunspaceJobs } if (-not $script:RpRunspaceResults) { $script:RpRunspaceResults = Initialize-RpRunspaceResults } # Ensure resources dictionary is initialized if ($app -and -not $app.Resources) { $app.Resources = New-Object System.Windows.ResourceDictionary } $appResources = $app.Resources # Load default theme safely $themeRoot = Join-Path $script:scriptRoot "Themes" $defaultsPath = Join-Path $themeRoot "MaterialDesign2.Defaults.xaml" if (Test-Path $defaultsPath) { $defaultsDict = New-Object System.Windows.ResourceDictionary $uri = New-Object System.Uri ((Resolve-Path -Path $defaultsPath).ProviderPath) $defaultsDict.Source = $uri $appResources.MergedDictionaries.Add($defaultsDict) Write-Verbose "Loaded Defaults: $defaultsPath" } # Load additional themes $requiredThemes = @( 'MaterialDesign2.Defaults.xaml', 'MaterialDesignTheme.Button.xaml', 'MaterialDesignTheme.PopupBox.xaml', 'MaterialDesignTheme.ToolTip.xaml', 'MaterialDesignTheme.ToggleButton.xaml', 'MaterialDesignTheme.TextBox.xaml', 'MaterialDesignTheme.Slider.xaml', 'MaterialDesignTheme.TabControl.xaml' ) foreach ($fileName in $requiredThemes) { $filePath = Join-Path $themeRoot $fileName if (Test-Path $filePath) { try { $dict = New-Object System.Windows.ResourceDictionary $uri = New-Object System.Uri("file:///$($filePath -replace '\\','/')") $dict.Source = $uri $app.Resources.MergedDictionaries.Add($dict) Write-Verbose "Loaded theme: $fileName" } catch { Write-Warning "Failed to load theme $fileName - $_" } } else { Write-Warning "Theme not found: $fileName" } } # Load and parse XAML $xamlPath = Join-Path $script:scriptRoot "Xaml\RemoteProUI.xaml" $RemoteProXaml = Get-Content -Path $xamlPath -Raw $RemoteProXaml = $RemoteProXaml -replace '^<Window.*', '<Window' -replace 'mc:Ignorable="d"', '' -replace "x:N", "N" [xml]$script:xaml = $RemoteProXaml $script:xmlreader = New-Object System.Xml.XmlNodeReader $xaml try { $script:window = [Windows.Markup.XamlReader]::Load($xmlreader) } catch { Write-Error "XAML Load Failed: $_" if ($_.Exception.InnerException) { Write-Error "Inner Exception: $($_.Exception.InnerException.Message)" if ($_.Exception.InnerException.InnerException) { Write-Error "Root Cause: $($_.Exception.InnerException.InnerException.Message)" } } throw } finally { $xmlreader.Close() $xmlreader.Dispose() } # Bind named XAML controls to script variables $script:xaml.SelectNodes("//*[@Name]") | ForEach-Object { $name = $_.Name try { $value = $script:window.FindName($name) if ($value) { Set-Variable -Name $name -Value $value -Scope Script Write-Verbose "Bound UI variable: $name" } else { Write-Verbose "No match found for: $name" } } catch { Write-Verbose "Failed to bind: $name - $($_.Exception.Message)" } } # Set the window icon if ($script:window) { Set-RpWindowIcon -window $script:window } else { Write-Warning "Failed to load window." } #endregion #region vms connection and updating main window. Set-RpDefaultConnectionBox # Helper function for clearing textbox Set-DefaultConnectionProfileBox # Helper function for populating connection profile textbox. Get-RemoteProConnections # Fill Connections_Combo_Box with profile names. # Refresh "MilestonePSTools Connection Profile Details" tab. Get-RpConnectionProfileRefresh # Helper function to refresh connection profiles # Refresh profiles by default when the main window loads Set-RpDefaultConfigCommandsDataGrid # Helper function for setting the default data grid Set-RpDefaultSettingsListView # Helper function for setting the default settings list box #endregion #region button-click processing # Attach event handlers to UI elements from RemotePro.EventHandlers # Syntax: $xamlVariableName.event($script:RemotePro.EventType.EventName) # Manage Connections TAB $script:New_Connection_File.Add_Click($script:RemotePro.EventHandlers.NewConnectionFile_Click) $script:OpenFile.Add_Click($script:RemotePro.EventHandlers.OpenFile_Click) $script:Connections_Combo_Box.Add_SelectionChanged($script:RemotePro.EventHandlers.ConnectionsComboBox_SelectionChanged) $script:ExecuteCommand.Add_Click($script:RemotePro.EventHandlers.ExecuteCommand_Click) $script:Connect.Add_Click($script:RemotePro.EventHandlers.Connect_Click) $script:Terminate.Add_Click($script:RemotePro.EventHandlers.Terminate_Click) $script:ConnectionProfileListBox.Add_SelectionChanged($script:RemotePro.EventHandlers.ConnectionProfileListBox_SelectionChanged) $script:PopOutButton.Add_Click($script:RemotePro.EventHandlers.PopOutButton_Click) $script:Connection_Profile_Refresh_Button.Add_Click($script:RemotePro.EventHandlers.Connection_Profile_Refresh_Button_Click) $script:Delete_Profile_Button.Add_Click($script:RemotePro.EventHandlers.DeleteProfileButton_Click) $script:Edit_Profile_Button.Add_Click($script:RemotePro.EventHandlers.EditProfileButton_Click) $script:Add_Profile_Button.Add_Click($script:RemotePro.EventHandlers.AddProfileButton_Click) $script:Connection_Status_Box.Add_TextChanged($script:RemotePro.EventHandlers.CSB_TextChanged) # Toolbar $script:GithubRepositoryCommand.Add_Click($script:RemotePro.EventHandlers.GithubRepositoryButton_Click) $script:PowerShellGalleryCommand.Add_Click($script:RemotePro.EventHandlers.PowerShellGalleryButton_Click) $script:DocsSiteCommand.Add_Click($script:RemotePro.EventHandlers.DocsSiteButton_Click) $script:FlowDirectionToggleButton.Add_Click($script:RemotePro.EventHandlers.FlowDirectionToggleButton_Click) $script:ReportIssueCommand.Add_Click($script:RemotePro.EventHandlers.ReportIssueButton_Click) $script:LicenseInformationCommand.Add_Click($script:RemotePro.EventHandlers.LicenseInformationButton_Click) $script:AboutCommand.Add_Click($script:RemotePro.EventHandlers.AboutButton_Click) $script:ExitApplicationCommand.Add_Click($script:RemotePro.EventHandlers.Window_AddClosed_Click) # Runspace log TAB $script:Runspace_Mutex_Log.Add_TextChanged($script:RemotePro.EventHandlers.RunspaceMutexLog_TextChanged) # Configuration > Main Settings TAB $script:RefreshSettings.Add_Click($script:RemotePro.EventHandlers.RefreshSettings_Click) $script:ResetSettings.Add_Click($script:RemotePro.EventHandlers.ResetSettings_Click) $script:DeleteSetting.Add_Click($script:RemotePro.EventHandlers.DeleteSetting_Click) $script:AddSetting.Add_Click($script:RemotePro.EventHandlers.AddSetting_Click) $script:EditSetting.Add_Click($script:RemotePro.EventHandlers.EditSetting_Click) # Configuration > ConfigCommands TAB $script:RefreshConfigCommands.Add_Click($script:RemotePro.EventHandlers.RefreshConfigCommands_Click) $script:ResetConfigCommands.Add_Click($script:RemotePro.EventHandlers.ResetConfigCommands_Click) $script:DeleteConfigCommands.Add_Click($script:RemotePro.EventHandlers.DeleteConfigCommands_Click) $script:AddConfigCommand.Add_Click($script:RemotePro.EventHandlers.AddConfigCommand_Click) $script:EditConfigCommand.Add_Click($script:RemotePro.EventHandlers.EditConfigCommand_Click) $script:TxtBox_FilterByName.Add_TextChanged($script:RemotePro.EventHandlers.TxtBox_FilterByName_TextChanged) $script:Commands_HeaderChkBox.Add_Click($script:RemotePro.EventHandlers.Commands_HeaderChkBox_Click) $script:Commands_DataGrid.AddHandler( [System.Windows.Controls.Primitives.ButtonBase]::ClickEvent, [System.Windows.RoutedEventHandler]$script:RemotePro.EventHandlers.Commands_DataGrid_CheckBox_Click ) $script:AllItemsChecked = ($script:Commands | Where-Object { -not $_.CheckboxSelect }).Count -eq 0 $script:Commands_HeaderChkBox.IsChecked = $script:AllItemsChecked $script:Commands_ScrollViewer.Add_PreviewMouseWheel($script:RemotePro.EventHandlers.Commands_ScrollViewer_PreviewMouseWheel) # Configuration > ConfigCommands TAB $script:ConfigurationTabs.Add_SelectionChanged($script:RemotePro.EventHandlers.ConfigurationTabs_SelectionChanged) # Attach runspace events to UI elements from RemotePro.RunspaceEvents # Syntax: $xamlVariableName.event($script:RemotePro.EventType.RunspaceEventName) $script:CamReport.Add_Click($script:RemotePro.RunspaceEvents.CamReport_Click) $script:ShowCameras.Add_Click($script:RemotePro.RunspaceEvents.ShowCameras_Click) $script:TicketBlock.Add_Click($script:RemotePro.RunspaceEvents.TicketBlock_Click) $script:ShowVideoOSItems.Add_Click($script:RemotePro.RunspaceEvents.ShowVideoOSItems_Click) $script:Hardware.Add_Click($script:RemotePro.RunspaceEvents.Hardware_Click) $script:ItemState.Add_Click($script:RemotePro.RunspaceEvents.ItemState_Click) #endegion #region clean up main window environment # Handle closing the main window $script:window.Add_Closed($script:RemotePro.EventHandlers.Window_AddClosed_Click) # Handle closing from the system tray icon $script:NotifyIcon = New-Object System.Windows.Forms.NotifyIcon $script:NotifyIcon.Icon = [System.Drawing.SystemIcons]::Application $script:NotifyIcon.Visible = $true $script:NotifyIcon.ContextMenu = New-Object System.Windows.Forms.ContextMenu $script:NotifyIcon.ContextMenu.MenuItems.Add("Exit", { $script:window.Close() }) $script:window.Add_Closed({ $script:NotifyIcon.Dispose() }) #endregion #region static runspace logic $jobsToRun = @( @{ JobName = "runspaceJob2" JobDetailsVariableName = "jobDetails2" Description = "Get VmsCameraReport" ScriptBlock = {} } ) $script:RpOpenRunspaces = Start-RpRunspaceJobStatic -Jobs $jobsToRun -uiElement $script:Runspace_Mutex_Log -OpenRunspaces $script:RpOpenRunspaces -RunspaceJobs $script:RunspaceJobs -Verbose #$script:openRunspaces.Jobs | ? Runspace -eq $runspaceJob2 | select Runspace #$script:openRunspaces.Jobs | ogv #endregion #region runspace job timer, collection, and maintenance $script:RunspaceTimerParams = @{ LogPath = $script:logPath uiElement = $script:Runspace_Mutex_Log RunspaceJobs = $script:RunspaceJobs RunspaceResults = $script:RunspaceResults OpenRunspaces = $script:RpOpenRunspaces } # Initialize the timer $script:RunspaceCleanupTimer = New-Object Windows.Threading.DispatcherTimer $script:RunspaceCleanupTimer.Interval = [TimeSpan]::FromSeconds(2) $script:RunspaceCleanupTimer.Add_Tick({ Watch-RpRunspaces @script:RunspaceTimerParams }) $script:RunspaceCleanupTimer.Start() #endregion if ($script:window) { if ($splashWindow) { $splashWindow.Close() } # Show the window modally (blocks until window is closed) $script:window.ShowDialog() | Out-Null } else { Write-Warning "Window is null - cannot display UI." } if ($script:window){ # Dispose of the window when closed $script:window.Close() } } catch { Write-Error "Start-RpRemotePro failed: $_" } } end { try { if ($script:RunspaceCleanupTimer -and $script:RunspaceCleanupTimer.Enabled) { $script:RunspaceCleanupTimer.Stop() $script:RunspaceCleanupTimer.Dispose() } if ($script:window -and $script:window.IsLoaded) { $script:window.Close() } if ($app -and ($app -is [System.Windows.Application])) { $app.Shutdown() } } catch { Write-Verbose "Safe shutdown skipped: $($_.Exception.Message)" } } } |