GUI/MainForm.ps1
|
# GUI MainForm - Event Handlers # Wires up all UI events. Business logic calls into Public cmdlets. function Initialize-DATMainForm { <# .SYNOPSIS Initializes event handlers and populates controls for the main form. .PARAMETER Controls Hashtable of form controls from New-DATMainForm. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Controls ) $Form = $Controls['MainForm'] $script:SyncCancellation = $null $script:Initializing = $true # --- Populate OS dropdown from OEMSources.json --- try { $Builds = Get-DATWindowsBuilds foreach ($BuildName in ($Builds.Keys | Sort-Object -Descending)) { $Controls['OsCombo'].Items.Add($BuildName) } if ($Controls['OsCombo'].Items.Count -gt 0) { $Controls['OsCombo'].SelectedIndex = 0 } } catch { $Controls['OsCombo'].Items.Add('Windows 11 24H2') $Controls['OsCombo'].SelectedIndex = 0 } # --- OS Selection Change: enable/disable manufacturer checkboxes --- # Dell drivers don't need build versions (plain "Windows 11" / "Windows 10") # Lenovo drivers need build versions ("Windows 11 23H2" etc.) $Controls['OsCombo'].Add_SelectedIndexChanged({ if ($script:Initializing) { return } $SelectedOS = $Controls['OsCombo'].Text $IsPlainOS = ($SelectedOS -match '^Windows 1[01]$') if ($IsPlainOS) { # Plain OS: Dell enabled, Lenovo disabled $Controls['DellCheckBox'].Enabled = $true $Controls['LenovoCheckBox'].Enabled = $false $Controls['LenovoCheckBox'].Checked = $false # Enable individual drivers option if Dell is checked $Controls['UpdateIndividualCheckBox'].Enabled = $Controls['DellCheckBox'].Checked } else { # Versioned OS: Dell disabled, Lenovo enabled $Controls['DellCheckBox'].Enabled = $false $Controls['DellCheckBox'].Checked = $false $Controls['LenovoCheckBox'].Enabled = $true # Disable individual drivers option (Dell-only feature) $Controls['UpdateIndividualCheckBox'].Enabled = $false $Controls['UpdateIndividualCheckBox'].Checked = $false } # Microsoft Surface works with any OS version $Controls['MicrosoftCheckBox'].Enabled = $true }) # Set initial manufacturer checkbox state based on default OS selection $InitOS = $Controls['OsCombo'].Text if ($InitOS -match '^Windows 1[01]$') { $Controls['DellCheckBox'].Enabled = $true $Controls['LenovoCheckBox'].Enabled = $false $Controls['LenovoCheckBox'].Checked = $false } elseif ($InitOS) { $Controls['DellCheckBox'].Enabled = $false $Controls['DellCheckBox'].Checked = $false $Controls['LenovoCheckBox'].Enabled = $true } # --- Dell Checkbox Change: enable/disable individual drivers option --- $Controls['DellCheckBox'].Add_CheckedChanged({ if ($script:Initializing) { return } $Controls['UpdateIndividualCheckBox'].Enabled = $Controls['DellCheckBox'].Checked if (-not $Controls['DellCheckBox'].Checked) { $Controls['UpdateIndividualCheckBox'].Checked = $false } }) # --- Register log subscriber for GUI --- $LogListBox = $Controls['LogListBox'] Register-DATLogSubscriber -Action { param($Event) if ($LogListBox -and -not $LogListBox.IsDisposed -and $LogListBox.IsHandleCreated) { try { $entry = "[{0}] {1}" -f $Event.Timestamp.ToString('HH:mm:ss'), $Event.Message $lb = $LogListBox $lb.Invoke([System.Windows.Forms.MethodInvoker]{ $lb.Items.Add($entry) | Out-Null $lb.TopIndex = [math]::Max(0, $lb.Items.Count - 1) }) } catch { } } } # --- Refresh Models Button --- $Controls['RefreshButton'].Add_Click({ $Controls['ModelGrid'].Rows.Clear() $Controls['StatusStripLabel'].Text = 'Loading models...' $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor try { $Manufacturers = @() if ($Controls['DellCheckBox'].Checked) { $Manufacturers += 'Dell' } if ($Controls['LenovoCheckBox'].Checked) { $Manufacturers += 'Lenovo' } if ($Controls['MicrosoftCheckBox'].Checked) { $Manufacturers += 'Microsoft' } foreach ($Make in $Manufacturers) { $Models = switch ($Make) { 'Dell' { Get-DellModelList } 'Lenovo' { Get-LenovoModelList } 'Microsoft' { Get-SurfaceModelList } } foreach ($M in $Models) { $ID = if ($M.SystemID) { $M.SystemID } elseif ($M.MachineType) { $M.MachineType } elseif ($M.DownloadID) { $M.DownloadID } else { '' } $Plat = if ($M.Platform) { $M.Platform } else { '' } $Controls['ModelGrid'].Rows.Add($false, $M.Manufacturer, $M.Model, $ID, $Plat) } } $ModelCount = $Controls['ModelGrid'].Rows.Count $Controls['StatusStripLabel'].Text = "Loaded $ModelCount models" # Auto-select known models if checkbox is checked and SCCM is connected if ($Controls['KnownModelsCheckBox'].Checked -and $script:CMConnected -and $ModelCount -gt 0) { $Controls['StatusStripLabel'].Text = "Loaded $ModelCount models - querying SCCM for known models..." try { $Manufacturers = @() if ($Controls['DellCheckBox'].Checked) { $Manufacturers += 'Dell' } if ($Controls['LenovoCheckBox'].Checked) { $Manufacturers += 'Lenovo' } if ($Controls['MicrosoftCheckBox'].Checked) { $Manufacturers += 'Microsoft' } $KnownModels = Get-DATKnownModels -Manufacturers $Manufacturers $MatchCount = Select-DATKnownModelsInGrid -Grid $Controls['ModelGrid'] -KnownModels $KnownModels $Controls['StatusStripLabel'].Text = "Loaded $ModelCount models - $MatchCount known model(s) selected" } catch { Write-DATLog -Message "Known models auto-select failed: $($_.Exception.Message)" -Severity 2 $Controls['StatusStripLabel'].Text = "Loaded $ModelCount models (known models query failed)" } } } catch { Show-DATFormMessage -Message "Error loading models: $($_.Exception.Message)" -Type Error $Controls['StatusStripLabel'].Text = 'Error loading models' } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } }) # --- Search Box - filter models --- $Controls['SearchBox'].Add_TextChanged({ $SearchText = $Controls['SearchBox'].Text foreach ($Row in $Controls['ModelGrid'].Rows) { if ([string]::IsNullOrEmpty($SearchText)) { $Row.Visible = $true } else { $ModelName = $Row.Cells['Model'].Value $Row.Visible = ($ModelName -and $ModelName -like "*$SearchText*") } } }) # --- Select All / None --- $Controls['SelectAllButton'].Add_Click({ foreach ($Row in $Controls['ModelGrid'].Rows) { if ($Row.Visible) { $Row.Cells[0].Value = $true } } }) $Controls['SelectNoneButton'].Add_Click({ foreach ($Row in $Controls['ModelGrid'].Rows) { $Row.Cells[0].Value = $false } }) # --- Known Models Checkbox --- $Controls['KnownModelsCheckBox'].Add_CheckedChanged({ if ($Controls['KnownModelsCheckBox'].Checked -and $Controls['ModelGrid'].Rows.Count -gt 0 -and $script:CMConnected) { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor $Controls['StatusStripLabel'].Text = 'Querying SCCM for known models...' try { $Manufacturers = @() if ($Controls['DellCheckBox'].Checked) { $Manufacturers += 'Dell' } if ($Controls['LenovoCheckBox'].Checked) { $Manufacturers += 'Lenovo' } if ($Controls['MicrosoftCheckBox'].Checked) { $Manufacturers += 'Microsoft' } $KnownModels = Get-DATKnownModels -Manufacturers $Manufacturers $MatchCount = Select-DATKnownModelsInGrid -Grid $Controls['ModelGrid'] -KnownModels $KnownModels $Controls['StatusStripLabel'].Text = "Selected $MatchCount known model(s) from SCCM inventory" } catch { Show-DATFormMessage -Message "Error querying known models: $($_.Exception.Message)" -Type Error $Controls['StatusStripLabel'].Text = 'Error querying known models' } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } } }) # --- Connect Button --- $Controls['ConnectButton'].Add_Click({ $Server = $Controls['SiteServerInput'].Text $Code = $Controls['SiteCodeInput'].Text $SSL = $Controls['UseSSLCheckBox'].Checked if ([string]::IsNullOrWhiteSpace($Server)) { Show-DATFormMessage -Message 'Please enter a site server name.' -Type Warning return } $Controls['ConnStatusLabel'].Text = 'Connecting...' $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Orange $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor try { $Params = @{ SiteServer = $Server } if ($Code) { $Params['SiteCode'] = $Code } if ($SSL) { $Params['UseSSL'] = $true } Connect-DATConfigMgr @Params $Controls['ConnStatusLabel'].Text = "Connected (Site: $($script:CMSiteCode))" $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Green $Controls['SiteCodeInput'].Text = $script:CMSiteCode # Enable Known Models checkbox now that SCCM is connected $Controls['KnownModelsCheckBox'].Enabled = $true # Populate DPs and DPGs $DPs = Get-DATDistributionPoints $Controls['DPGrid'].Rows.Clear() foreach ($DP in $DPs) { $Controls['DPGrid'].Rows.Add($false, $DP) } $DPGs = Get-DATDistributionPointGroups $Controls['DPGGrid'].Rows.Clear() foreach ($DPG in $DPGs) { $Controls['DPGGrid'].Rows.Add($false, $DPG) } $Controls['StatusStripLabel'].Text = "Connected to $Server - $($DPs.Count) DPs, $($DPGs.Count) DPGs" } catch { $Controls['ConnStatusLabel'].Text = 'Connection Failed' $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Red Show-DATFormMessage -Message "Connection failed: $($_.Exception.Message)" -Type Error } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } }) # --- Browse Buttons --- $Controls['DLBrowseButton'].Add_Click({ $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog $Dialog.Description = 'Select download path' if ($Dialog.ShowDialog() -eq 'OK') { $Controls['DownloadPathInput'].Text = $Dialog.SelectedPath } }) $Controls['PkgBrowseButton'].Add_Click({ $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog $Dialog.Description = 'Select package source path' if ($Dialog.ShowDialog() -eq 'OK') { $Controls['PackagePathInput'].Text = $Dialog.SelectedPath } }) # --- Compress Package Checkbox --- $Controls['CompressPackageCheckBox'].Add_CheckedChanged({ if ($script:Initializing) { return } $Controls['CompressionTypeCombo'].Enabled = $Controls['CompressPackageCheckBox'].Checked }) # --- Deployment Platform Change: enable/disable Clean Unused Drivers --- $Controls['DeployPlatformCombo'].Add_SelectedIndexChanged({ if ($script:Initializing) { return } $IsDriverPkg = $Controls['DeployPlatformCombo'].Text -in @('ConfigMgr - Driver Pkg', 'ConfigMgr - Driver Pkg (Test)') $Controls['CleanUnusedCheckBox'].Enabled = $IsDriverPkg if (-not $IsDriverPkg) { $Controls['CleanUnusedCheckBox'].Checked = $false } }) # --- Start Sync Button --- $Controls['StartButton'].Add_Click({ # Validate $SelectedModels = Get-DATFormSelectedModels -Grid $Controls['ModelGrid'] if ($SelectedModels.Count -eq 0) { Show-DATFormMessage -Message 'Please select at least one model.' -Type Warning return } if ([string]::IsNullOrWhiteSpace($Controls['DownloadPathInput'].Text) -or [string]::IsNullOrWhiteSpace($Controls['PackagePathInput'].Text)) { Show-DATFormMessage -Message 'Please configure download and package paths on the SCCM Settings tab.' -Type Warning return } if (-not $script:CMConnected) { Show-DATFormMessage -Message 'Please connect to ConfigMgr on the SCCM Settings tab.' -Type Warning return } # Switch to progress tab $Controls['TabControl'].SelectedIndex = 2 $Controls['LogListBox'].Items.Clear() # Disable controls during sync $Controls['StartButton'].Enabled = $false $Controls['StopButton'].Enabled = $true # Gather parameters $Manufacturers = @() if ($Controls['DellCheckBox'].Checked) { $Manufacturers += 'Dell' } if ($Controls['LenovoCheckBox'].Checked) { $Manufacturers += 'Lenovo' } if ($Controls['MicrosoftCheckBox'].Checked) { $Manufacturers += 'Microsoft' } $ModelNames = $SelectedModels | ForEach-Object { $_.Model } $TypeSelection = $Controls['TypeCombo'].Text $IncludeDrivers = $TypeSelection -in @('Drivers', 'Drivers + BIOS') $IncludeBIOS = $TypeSelection -in @('BIOS Updates', 'Drivers + BIOS') $DPs = Get-DATFormSelectedDPs -Grid $Controls['DPGrid'] $DPGs = Get-DATFormSelectedDPs -Grid $Controls['DPGGrid'] $SyncParams = @{ Manufacturer = $Manufacturers Models = $ModelNames OperatingSystem = $Controls['OsCombo'].Text Architecture = $Controls['ArchCombo'].Text SiteServer = $Controls['SiteServerInput'].Text SiteCode = $Controls['SiteCodeInput'].Text DownloadPath = $Controls['DownloadPathInput'].Text PackagePath = $Controls['PackagePathInput'].Text IncludeDrivers = $IncludeDrivers IncludeBIOS = $IncludeBIOS RemoveLegacy = $Controls['RemoveLegacyCheckBox'].Checked CleanSource = $Controls['CleanSourceCheckBox'].Checked EnableBDR = $Controls['EnableBDRCheckBox'].Checked CleanUnusedDrivers = $Controls['CleanUnusedCheckBox'].Checked CleanDownloads = $Controls['CleanDownloadsCheckBox'].Checked DeploymentPlatform = $Controls['DeployPlatformCombo'].Text } if ($DPs.Count -gt 0) { $SyncParams['DistributionPoints'] = $DPs } if ($DPGs.Count -gt 0) { $SyncParams['DistributionPointGroups'] = $DPGs } if ($Controls['UseSSLCheckBox'].Checked) { $SyncParams['UseSSL'] = $true } if ($Controls['CompressPackageCheckBox'].Checked) { $SyncParams['CompressPackage'] = $true $SyncParams['CompressionType'] = $Controls['CompressionTypeCombo'].Text } if ($Controls['UpdateIndividualCheckBox'].Checked) { $SyncParams['UpdateIndividualDrivers'] = $true } if ($Controls['VerifyHashCheckBox'].Checked) { $SyncParams['VerifyDownloadHash'] = $true } # Run sync in a background runspace so the GUI stays responsive $Controls['StatusLabel'].Text = 'Sync in progress...' $Controls['ProgressBar'].Style = 'Marquee' # Get module path so the runspace can import it $ModulePath = (Get-Module DriverAutomationTool).ModuleBase # Create a thread-safe queue so the runspace can send log messages to the UI $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() # Script block that runs in the background runspace $SyncScript = { param($ModulePath, $SyncParams, $LogQueue) Import-Module (Join-Path $ModulePath 'DriverAutomationTool.psd1') -Force # Register a log subscriber that enqueues to the shared queue # Uses exported wrapper function (private functions aren't accessible from runspace scope) Register-DATQueueLogSubscriber -LogQueue $LogQueue # Run sync (Invoke-DATSync handles Connect-DATConfigMgr internally # using the SiteServer/SiteCode/UseSSL params already in $SyncParams) Invoke-DATSync @SyncParams } # Create and start background runspace $script:SyncRunspace = [System.Management.Automation.PowerShell]::Create() $script:SyncRunspace.AddScript($SyncScript).AddArgument($ModulePath).AddArgument($SyncParams).AddArgument($script:LogQueue) | Out-Null $script:SyncHandle = $script:SyncRunspace.BeginInvoke() # Start polling timer to check for completion and drain log queue if ($script:SyncTimer) { $script:SyncTimer.Stop() $script:SyncTimer.Dispose() } # Capture controls in script scope so the timer tick can access them # ($Controls is a function parameter and won't be in scope when the timer fires) $script:TimerControls = $Controls $script:SyncTimer = New-Object System.Windows.Forms.Timer $script:SyncTimer.Interval = 500 $script:SyncTimer.Add_Tick({ # Drain log queue and update the Progress tab if ($script:LogQueue) { $msg = $null while ($script:LogQueue.TryDequeue([ref]$msg)) { $script:TimerControls['LogListBox'].Items.Add($msg) } if ($script:TimerControls['LogListBox'].Items.Count -gt 0) { $script:TimerControls['LogListBox'].TopIndex = $script:TimerControls['LogListBox'].Items.Count - 1 } } if ($null -eq $script:SyncHandle) { return } if (-not $script:SyncHandle.IsCompleted) { return } $script:SyncTimer.Stop() # Final drain of any remaining log messages if ($script:LogQueue) { $msg = $null while ($script:LogQueue.TryDequeue([ref]$msg)) { $script:TimerControls['LogListBox'].Items.Add($msg) } if ($script:TimerControls['LogListBox'].Items.Count -gt 0) { $script:TimerControls['LogListBox'].TopIndex = $script:TimerControls['LogListBox'].Items.Count - 1 } } try { $Results = $script:SyncRunspace.EndInvoke($script:SyncHandle) # Use actual sync results to determine success/failure. # The error stream may contain non-fatal warnings (e.g. extraction # timeouts that still produced files) so don't use it as the primary # success indicator. if ($Results -and @($Results).Count -gt 0) { $SuccessCount = @($Results | Where-Object { $_.Status -eq 'Success' }).Count $SkipCount = @($Results | Where-Object { $_.Status -eq 'Skipped' }).Count $ErrorCount = @($Results | Where-Object { $_.Status -eq 'Error' }).Count $script:TimerControls['ProgressBar'].Style = 'Continuous' $script:TimerControls['ProgressBar'].Value = $script:TimerControls['ProgressBar'].Maximum if ($ErrorCount -gt 0 -and $SuccessCount -eq 0) { $script:TimerControls['StatusLabel'].Text = "Sync failed - $ErrorCount error(s)" Show-DATFormMessage -Message "Sync failed!`n`nErrors: $ErrorCount`nSkipped: $SkipCount" -Type Error } elseif ($ErrorCount -gt 0) { $script:TimerControls['StatusLabel'].Text = "Sync complete - $SuccessCount succeeded, $ErrorCount error(s)" Show-DATFormMessage -Message "Sync complete with warnings.`n`nSuccess: $SuccessCount`nSkipped: $SkipCount`nErrors: $ErrorCount" -Type Warning } else { $script:TimerControls['StatusLabel'].Text = "Sync complete - $SuccessCount succeeded, $SkipCount skipped" Show-DATFormMessage -Message "Sync complete!`n`nSuccess: $SuccessCount`nSkipped: $SkipCount" -Type Information } } else { # No results returned - check error stream for fatal errors $RunspaceErrors = $script:SyncRunspace.Streams.Error if ($RunspaceErrors -and $RunspaceErrors.Count -gt 0) { $ErrMsg = $RunspaceErrors[0].Exception.Message $script:TimerControls['StatusLabel'].Text = 'Sync failed' Show-DATFormMessage -Message "Sync failed: $ErrMsg" -Type Error } else { $script:TimerControls['StatusLabel'].Text = 'Sync complete - no packages to process' Show-DATFormMessage -Message "Sync complete - no packages were selected for processing." -Type Information } } } catch { $script:TimerControls['StatusLabel'].Text = 'Sync failed' Show-DATFormMessage -Message "Sync failed: $($_.Exception.Message)" -Type Error } finally { if ($script:SyncRunspace) { $script:SyncRunspace.Dispose() $script:SyncRunspace = $null } $script:SyncHandle = $null $script:LogQueue = $null $script:TimerControls['StartButton'].Enabled = $true $script:TimerControls['StopButton'].Enabled = $false $script:TimerControls['ProgressBar'].Style = 'Continuous' } }) $script:SyncTimer.Start() }) # --- Stop Sync Button --- $Controls['StopButton'].Add_Click({ if ($script:SyncRunspace -and $script:SyncHandle -and -not $script:SyncHandle.IsCompleted) { Write-DATLog -Message 'Sync operation cancelled by user' -Severity 2 $script:SyncRunspace.Stop() if ($script:SyncTimer) { $script:SyncTimer.Stop() } $script:SyncRunspace.Dispose() $script:SyncRunspace = $null $script:SyncHandle = $null $script:LogQueue = $null $Controls['LogListBox'].Items.Add('[Cancelled] Sync operation cancelled by user') $Controls['LogListBox'].TopIndex = $Controls['LogListBox'].Items.Count - 1 $Controls['StatusLabel'].Text = 'Sync cancelled' $Controls['ProgressBar'].Style = 'Continuous' $Controls['StartButton'].Enabled = $true $Controls['StopButton'].Enabled = $false } }) # --- Health Check Button --- $Controls['HealthCheckButton'].Add_Click({ $Controls['TabControl'].SelectedIndex = 2 $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor try { $Results = Test-DATCatalogHealth $Healthy = ($Results | Where-Object { $_.Reachable }).Count $Total = $Results.Count Show-DATFormMessage -Message "Health check: $Healthy/$Total endpoints reachable." ` -Type $(if ($Healthy -eq $Total) { 'Information' } else { 'Warning' }) } catch { Show-DATFormMessage -Message "Health check failed: $($_.Exception.Message)" -Type Error } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } }) # --- Save Settings Button --- $Controls['SaveSettingsButton'].Add_Click({ try { $Config = @{ manufacturers = @() operatingSystem = $Controls['OsCombo'].Text architecture = $Controls['ArchCombo'].Text sccm = @{ siteServer = $Controls['SiteServerInput'].Text siteCode = $Controls['SiteCodeInput'].Text useSSL = $Controls['UseSSLCheckBox'].Checked distributionPoints = @(Get-DATFormSelectedDPs -Grid $Controls['DPGrid']) distributionPointGroups = @(Get-DATFormSelectedDPs -Grid $Controls['DPGGrid']) } paths = @{ download = $Controls['DownloadPathInput'].Text package = $Controls['PackagePathInput'].Text } options = @{ removeLegacy = $Controls['RemoveLegacyCheckBox'].Checked enableBDR = $Controls['EnableBDRCheckBox'].Checked cleanSource = $Controls['CleanSourceCheckBox'].Checked cleanUnusedDrivers = $Controls['CleanUnusedCheckBox'].Checked cleanDownloads = $Controls['CleanDownloadsCheckBox'].Checked updateIndividualDrivers = $Controls['UpdateIndividualCheckBox'].Checked verifyDownloadHash = $Controls['VerifyHashCheckBox'].Checked deploymentPlatform = $Controls['DeployPlatformCombo'].Text compressPackage = $Controls['CompressPackageCheckBox'].Checked compressionType = $Controls['CompressionTypeCombo'].Text } } if ($Controls['DellCheckBox'].Checked) { $Config.manufacturers += 'Dell' } if ($Controls['LenovoCheckBox'].Checked) { $Config.manufacturers += 'Lenovo' } if ($Controls['MicrosoftCheckBox'].Checked) { $Config.manufacturers += 'Microsoft' } Save-DATConfig -Config $Config Show-DATFormMessage -Message 'Settings saved successfully.' -Type Information } catch { Show-DATFormMessage -Message "Failed to save settings: $($_.Exception.Message)" -Type Error } }) # --- Package Management - Refresh --- $Controls['PkgRefreshButton'].Add_Click({ if (-not $script:CMConnected) { Show-DATFormMessage -Message 'Connect to ConfigMgr first.' -Type Warning return } $Controls['PkgGrid'].Rows.Clear() $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor try { $TypeFilter = switch ($Controls['PkgFilterCombo'].Text) { 'Drivers Only' { 'Drivers' } 'BIOS Only' { 'BIOS' } default { 'All' } } $FindParams = @{ Type = $TypeFilter } if ($Controls['PkgIncludeDriverPkgsCheckBox'].Checked) { $FindParams['IncludeDriverPackages'] = $true } $Packages = Find-DATExistingPackages @FindParams | Sort-Object Name foreach ($Pkg in $Packages) { $PkgTypeLabel = if ($Pkg.PackageType -eq 'DriverPackage') { 'Driver Pkg' } else { 'Standard' } $Controls['PkgGrid'].Rows.Add( $false, $Pkg.PackageID, $Pkg.Name, $Pkg.Version, $Pkg.Manufacturer, $PkgTypeLabel, $Pkg.SourcePath ) } $Controls['StatusStripLabel'].Text = "Found $($Packages.Count) packages" } catch { Show-DATFormMessage -Message "Error loading packages: $($_.Exception.Message)" -Type Error } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } }) # --- Package Management - Select All / Select None --- $Controls['PkgSelectAllButton'].Add_Click({ foreach ($Row in $Controls['PkgGrid'].Rows) { $Row.Cells[0].Value = $true } }) $Controls['PkgSelectNoneButton'].Add_Click({ foreach ($Row in $Controls['PkgGrid'].Rows) { $Row.Cells[0].Value = $false } }) # --- Package Management - Search --- $Controls['PkgSearchBox'].Add_TextChanged({ $SearchText = $Controls['PkgSearchBox'].Text foreach ($Row in $Controls['PkgGrid'].Rows) { if ([string]::IsNullOrEmpty($SearchText)) { $Row.Visible = $true } else { $Name = $Row.Cells['Name'].Value $Manufacturer = $Row.Cells['Manufacturer'].Value $Row.Visible = (($Name -and $Name -like "*$SearchText*") -or ($Manufacturer -and $Manufacturer -like "*$SearchText*")) } } }) # --- Package Management - Delete --- $Controls['PkgDeleteButton'].Add_Click({ $SelectedRows = @($Controls['PkgGrid'].Rows | Where-Object { $_.Cells[0].Value -eq $true }) if ($SelectedRows.Count -eq 0) { Show-DATFormMessage -Message 'Select packages to remove.' -Type Warning return } $Confirm = Show-DATFormMessage ` -Message "Remove $($SelectedRows.Count) selected package(s)? This cannot be undone." ` -Type Question if ($Confirm -ne 'Yes') { return } # Capture everything needed by the runspace before going background $PackagesToRemove = @($SelectedRows | ForEach-Object { @{ ID = $_.Cells['PackageID'].Value; Name = $_.Cells['Name'].Value } }) $CleanSource = $Controls['CleanSourceCheckBox'].Checked $ConnParams = @{ SiteServer = $Controls['SiteServerInput'].Text; SiteCode = $Controls['SiteCodeInput'].Text } if ($Controls['UseSSLCheckBox'].Checked) { $ConnParams['UseSSL'] = $true } $ModulePath = (Get-Module DriverAutomationTool).ModuleBase # Disable controls and show progress $Controls['PkgDeleteButton'].Enabled = $false $Controls['PkgRefreshButton'].Enabled = $false $Controls['PkgApplyButton'].Enabled = $false $Controls['StatusStripLabel'].Text = "Removing $($PackagesToRemove.Count) package(s)..." $script:LogQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() $DeleteScript = { param($ModulePath, $ConnParams, $PackagesToRemove, $CleanSource, $LogQueue) Import-Module (Join-Path $ModulePath 'DriverAutomationTool.psd1') -Force Register-DATQueueLogSubscriber -LogQueue $LogQueue # Invoke-DATRemovePackages is a public exported function that handles # Connect-DATConfigMgr + Remove-DATLegacyPackage internally return Invoke-DATRemovePackages @ConnParams -Packages $PackagesToRemove -CleanSource:$CleanSource } $script:DeleteRunspace = [System.Management.Automation.PowerShell]::Create() $script:DeleteRunspace.AddScript($DeleteScript).AddArgument($ModulePath).AddArgument($ConnParams).AddArgument($PackagesToRemove).AddArgument($CleanSource).AddArgument($script:LogQueue) | Out-Null $script:DeleteHandle = $script:DeleteRunspace.BeginInvoke() $script:TimerControls = $Controls if ($script:DeleteTimer) { $script:DeleteTimer.Stop(); $script:DeleteTimer.Dispose() } $script:DeleteTimer = New-Object System.Windows.Forms.Timer $script:DeleteTimer.Interval = 500 $script:DeleteTimer.Add_Tick({ # Drain log queue to the Progress tab if ($script:LogQueue) { $LogMsg = $null while ($script:LogQueue.TryDequeue([ref]$LogMsg)) { $script:TimerControls['LogListBox'].Items.Add($LogMsg) } if ($script:TimerControls['LogListBox'].Items.Count -gt 0) { $script:TimerControls['LogListBox'].TopIndex = $script:TimerControls['LogListBox'].Items.Count - 1 } } if ($null -eq $script:DeleteHandle -or -not $script:DeleteHandle.IsCompleted) { return } $script:DeleteTimer.Stop() # Final log drain if ($script:LogQueue) { $LogMsg = $null while ($script:LogQueue.TryDequeue([ref]$LogMsg)) { $script:TimerControls['LogListBox'].Items.Add($LogMsg) } if ($script:TimerControls['LogListBox'].Items.Count -gt 0) { $script:TimerControls['LogListBox'].TopIndex = $script:TimerControls['LogListBox'].Items.Count - 1 } } # Re-enable controls $script:TimerControls['PkgDeleteButton'].Enabled = $true $script:TimerControls['PkgRefreshButton'].Enabled = $true $script:TimerControls['PkgApplyButton'].Enabled = $true try { $Results = $script:DeleteRunspace.EndInvoke($script:DeleteHandle) $Succeeded = @($Results | Where-Object { $_.Status -eq 'Success' }) $Failed = @($Results | Where-Object { $_.Status -eq 'Failed' }) $script:DeleteRunspace.Dispose() # Refresh grid and clear search so all packages are visible $script:TimerControls['PkgSearchBox'].Text = '' $script:TimerControls['PkgRefreshButton'].PerformClick() $script:TimerControls['StatusStripLabel'].Text = "Removed $($Succeeded.Count) package(s)" if ($Failed.Count -eq 0) { Show-DATFormMessage -Message "Removed $($Succeeded.Count) package(s) successfully." -Type Information } elseif ($Succeeded.Count -eq 0) { $FailList = ($Failed | ForEach-Object { "$($_.ID) ($($_.Name)): $($_.Error)" }) -join "`n" Show-DATFormMessage -Message "All $($Failed.Count) package removal(s) failed.`n`n$FailList`n`nCheck the DAT log for details." -Type Error } else { $FailList = ($Failed | ForEach-Object { "$($_.ID) ($($_.Name)): $($_.Error)" }) -join "`n" Show-DATFormMessage -Message "$($Succeeded.Count) removed, $($Failed.Count) failed.`n`nFailed:`n$FailList`n`nCheck the DAT log for details." -Type Warning } } catch { $script:TimerControls['StatusStripLabel'].Text = 'Package removal failed' Show-DATFormMessage -Message "Package removal failed: $($_.Exception.Message)" -Type Error } finally { $script:DeleteHandle = $null } }) $script:DeleteTimer.Start() }) # --- Package Management - Apply Action --- $Controls['PkgApplyButton'].Add_Click({ if (-not $script:CMConnected) { Show-DATFormMessage -Message 'Connect to ConfigMgr first.' -Type Warning return } $SelectedRows = @($Controls['PkgGrid'].Rows | Where-Object { $_.Cells[0].Value -eq $true }) if ($SelectedRows.Count -eq 0) { Show-DATFormMessage -Message 'Select at least one package to apply the action.' -Type Warning return } $Action = $Controls['PkgActionCombo'].Text # Handle "Patch Driver Package" separately (requires folder browser + single selection) if ($Action -eq 'Patch Driver Package') { if ($SelectedRows.Count -gt 1) { Show-DATFormMessage -Message 'Select only one package to patch.' -Type Warning return } $FolderDialog = New-Object System.Windows.Forms.FolderBrowserDialog $FolderDialog.Description = 'Select folder containing additional driver files (*.inf)' if ($FolderDialog.ShowDialog() -ne 'OK') { return } $PatchPath = $FolderDialog.SelectedPath $PkgID = $SelectedRows[0].Cells['PackageID'].Value $PkgName = $SelectedRows[0].Cells['Name'].Value $Confirm = Show-DATFormMessage ` -Message "Patch package '$PkgName' ($PkgID) with drivers from:`n$PatchPath`n`nThis will modify the package content and redistribute." ` -Type Question if ($Confirm -eq 'Yes') { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor try { Invoke-DATPatchPackage -PackageID $PkgID -PatchSourcePath $PatchPath Show-DATFormMessage -Message "Package '$PkgName' patched and redistribution initiated." -Type Information $Controls['PkgRefreshButton'].PerformClick() } catch { Show-DATFormMessage -Message "Failed to patch package: $($_.Exception.Message)" -Type Error } finally { $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default } } return } # Determine action description for confirmation dialog $ActionDesc = switch -Wildcard ($Action) { 'Move to Production' { "move $($SelectedRows.Count) package(s) to Production (remove Test/Pilot/Retired prefix, existing production packages will be retired)" } 'Move to Pilot' { "mark $($SelectedRows.Count) package(s) as Pilot" } 'Mark as Retired' { "mark $($SelectedRows.Count) package(s) as Retired" } 'Move to Windows *' { "change $($SelectedRows.Count) package(s) to target $($Action -replace 'Move to ', '')" } } $Confirm = Show-DATFormMessage -Message "Are you sure you want to $ActionDesc`?" -Type Question if ($Confirm -ne 'Yes') { return } $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::WaitCursor $SuccessCount = 0 $ErrorCount = 0 foreach ($Row in $SelectedRows) { $PkgID = $Row.Cells['PackageID'].Value try { switch -Wildcard ($Action) { 'Move to Production' { Rename-DATPackageState -PackageID $PkgID -State 'Production' } 'Move to Pilot' { Rename-DATPackageState -PackageID $PkgID -State 'Pilot' } 'Mark as Retired' { Rename-DATPackageState -PackageID $PkgID -State 'Retired' } 'Move to Windows *' { $TargetOS = $Action -replace '^Move to ', '' Move-DATPackageOSVersion -PackageID $PkgID -TargetOS $TargetOS } } $SuccessCount++ } catch { $ErrorCount++ Write-DATLog -Message "Action '$Action' failed for $PkgID`: $($_.Exception.Message)" -Severity 3 } } $Controls['MainForm'].Cursor = [System.Windows.Forms.Cursors]::Default Show-DATFormMessage -Message "Action complete: $SuccessCount succeeded, $ErrorCount failed." -Type Information $Controls['PkgRefreshButton'].PerformClick() }) # --- Load saved settings on form load --- $Form.Add_Load({ try { $Config = Get-DATConfig if ($Config) { if ($Config.sccm.siteServer) { $Controls['SiteServerInput'].Text = $Config.sccm.siteServer } if ($Config.sccm.siteCode) { $Controls['SiteCodeInput'].Text = $Config.sccm.siteCode } if ($Config.sccm.useSSL) { $Controls['UseSSLCheckBox'].Checked = $true } if ($Config.paths.download) { $Controls['DownloadPathInput'].Text = $Config.paths.download } if ($Config.paths.package) { $Controls['PackagePathInput'].Text = $Config.paths.package } if ($Config.options.removeLegacy) { $Controls['RemoveLegacyCheckBox'].Checked = $true } if ($Config.options.cleanSource) { $Controls['CleanSourceCheckBox'].Checked = $true } if ($Config.options.cleanDownloads) { $Controls['CleanDownloadsCheckBox'].Checked = $true } if ($Config.options.updateIndividualDrivers) { $Controls['UpdateIndividualCheckBox'].Checked = $true } if ($Config.options.verifyDownloadHash) { $Controls['VerifyHashCheckBox'].Checked = $true } if ($Config.options.deploymentPlatform) { $Idx = $Controls['DeployPlatformCombo'].Items.IndexOf($Config.options.deploymentPlatform) if ($Idx -ge 0) { $Controls['DeployPlatformCombo'].SelectedIndex = $Idx } } # Load CleanUnused AFTER platform selection (platform change handler enables/disables it) if ($Config.options.cleanUnusedDrivers -and $Controls['DeployPlatformCombo'].Text -in @('ConfigMgr - Driver Pkg', 'ConfigMgr - Driver Pkg (Test)')) { $Controls['CleanUnusedCheckBox'].Checked = $true } if ($Config.options.compressPackage) { $Controls['CompressPackageCheckBox'].Checked = $true $Controls['CompressionTypeCombo'].Enabled = $true } if ($Config.options.compressionType) { $Idx = $Controls['CompressionTypeCombo'].Items.IndexOf($Config.options.compressionType) if ($Idx -ge 0) { $Controls['CompressionTypeCombo'].SelectedIndex = $Idx } } $Controls['DellCheckBox'].Checked = $Config.manufacturers -contains 'Dell' $Controls['LenovoCheckBox'].Checked = $Config.manufacturers -contains 'Lenovo' $Controls['MicrosoftCheckBox'].Checked = $Config.manufacturers -contains 'Microsoft' if ($Config.operatingSystem) { $Idx = $Controls['OsCombo'].Items.IndexOf($Config.operatingSystem) if ($Idx -ge 0) { $Controls['OsCombo'].SelectedIndex = $Idx } } # Apply control states that event handlers would normally set # (handlers are suppressed during initialization) $LoadedOS = $Controls['OsCombo'].Text if ($LoadedOS -match '^Windows 1[01]$') { $Controls['DellCheckBox'].Enabled = $true $Controls['LenovoCheckBox'].Enabled = $false } else { $Controls['LenovoCheckBox'].Enabled = $true $Controls['DellCheckBox'].Enabled = $false } $IsDriverPkg = $Controls['DeployPlatformCombo'].Text -in @('ConfigMgr - Driver Pkg', 'ConfigMgr - Driver Pkg (Test)') $Controls['CleanUnusedCheckBox'].Enabled = $IsDriverPkg $Controls['CompressionTypeCombo'].Enabled = $Controls['CompressPackageCheckBox'].Checked # Enable individual drivers checkbox only when Dell is selected $Controls['UpdateIndividualCheckBox'].Enabled = $Controls['DellCheckBox'].Checked # Auto-connect to ConfigMgr if site server is configured if (-not [string]::IsNullOrWhiteSpace($Controls['SiteServerInput'].Text)) { $Controls['ConnStatusLabel'].Text = 'Auto-connecting...' $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Orange try { $AutoParams = @{ SiteServer = $Controls['SiteServerInput'].Text } $AutoCode = $Controls['SiteCodeInput'].Text if ($AutoCode) { $AutoParams['SiteCode'] = $AutoCode } if ($Controls['UseSSLCheckBox'].Checked) { $AutoParams['UseSSL'] = $true } Connect-DATConfigMgr @AutoParams $Controls['ConnStatusLabel'].Text = "Connected (Site: $($script:CMSiteCode))" $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Green $Controls['SiteCodeInput'].Text = $script:CMSiteCode $Controls['KnownModelsCheckBox'].Enabled = $true # Populate DPs and DPGs $DPs = Get-DATDistributionPoints $Controls['DPGrid'].Rows.Clear() foreach ($DP in $DPs) { $Controls['DPGrid'].Rows.Add($false, $DP) } $DPGs = Get-DATDistributionPointGroups $Controls['DPGGrid'].Rows.Clear() foreach ($DPG in $DPGs) { $Controls['DPGGrid'].Rows.Add($false, $DPG) } # Restore saved DP/DPG selections if ($Config.sccm.distributionPoints -and $Config.sccm.distributionPoints.Count -gt 0) { foreach ($Row in $Controls['DPGrid'].Rows) { if ($Config.sccm.distributionPoints -contains $Row.Cells['Name'].Value) { $Row.Cells['Selected'].Value = $true } } } if ($Config.sccm.distributionPointGroups -and $Config.sccm.distributionPointGroups.Count -gt 0) { foreach ($Row in $Controls['DPGGrid'].Rows) { if ($Config.sccm.distributionPointGroups -contains $Row.Cells['Name'].Value) { $Row.Cells['Selected'].Value = $true } } } $Controls['StatusStripLabel'].Text = "Auto-connected to $($Controls['SiteServerInput'].Text) - Select manufacturers and click Refresh Models" } catch { $Controls['ConnStatusLabel'].Text = 'Auto-connect failed' $Controls['ConnStatusLabel'].ForeColor = [System.Drawing.Color]::Red $Controls['StatusStripLabel'].Text = 'Ready - Auto-connect failed. Select manufacturers and click Refresh Models' } } } } catch { # Settings load failure is non-fatal } if (-not $Controls['StatusStripLabel'].Text -or $Controls['StatusStripLabel'].Text -eq 'Ready') { $Controls['StatusStripLabel'].Text = 'Ready - Select manufacturers and click Refresh Models' } # Always apply Dell-dependent checkbox state (handles both config-loaded and default scenarios) $Controls['UpdateIndividualCheckBox'].Enabled = $Controls['DellCheckBox'].Checked # Enable event handlers now that initialization is complete $script:Initializing = $false }) } |