GuiMyPS.psm1

<#
.SYNOPSIS
    Adds a Click event handler to every button within a given element.
 
.DESCRIPTION
    The Add-ClickToEveryButton function traverses the visual tree of a given element and adds a Click event handler to every button found. Specific buttons can be ignored based on their names.
 
.PARAMETER Element
    The root element to start the search.
 
.PARAMETER ClickHandler
    The Click event handler to be added to each button.
 
.EXAMPLE
    Add-ClickToEveryButton -Element $window -ClickHandler $ClickHandler
 
    Traverses the visual tree of the $window element and adds the $ClickHandler Click event handler to every button found.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-18 14:22:52
#>

Function Add-ClickToEveryButton {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        $Element,
        [Parameter(Mandatory = $false)]
        [System.Windows.RoutedEventHandler]$ClickHandler
    )

    # Dump Parameter Values
    # $PSBoundParameters.GetEnumerator() | ForEach-Object {
    # Write-Verbose "$($_.Key) = $($_.Value)"
    # }

    If ($Element -is 'System.Windows.Controls.Button') {
        Write-Verbose ("Add-ClickToEveryButton() Button Object: [{0}] - {1}" -f $Element.GetType().ToString(), $Element.Name)
        If (-not [String]::IsNullOrEmpty($ClickHandler)) {
            Write-Verbose (" Adding Click Event For: {0}" -f $Element.Content)
            $Element.Add_Click($ClickHandler)
        }
    } Else {
        Write-Verbose ("Add-ClickToEveryButton() Object: [{0}] - {1}" -f $Element.GetType().ToString(), $Element.Name)
    }

    If ($Element.HasItems -or $Element.HasContent -or $Element.Child.Count -gt 0 -or $Element.Children.Count -gt 0 -or $Element.Items.Count -gt 0) {
        # The logical tree can contain any type of object, not just
        # instances of DependencyObject subclasses. LogicalTreeHelper
        # only works with DependencyObject subclasses, so we must be
        # sure that we do not pass it an object of the wrong type.
        $depObj = $Element
        If ($null -ne $depObj) {
            ForEach ($logicalChild in ([System.Windows.LogicalTreeHelper]::GetChildren($depObj))) {
                Add-ClickToEveryButton -Element $logicalChild -ClickHandler:$ClickHandler
            }
        }
    }
}


<#
.SYNOPSIS
    Adds a Click event handler to every menu item within a given menu object.
 
.DESCRIPTION
    The Add-ClickToEveryMenuItem function traverses the visual tree of a given menu object and adds a Click event handler to every menu item found. Specific menu items can be ignored based on their properties.
 
.PARAMETER MenuObj
    The root menu object to start the search.
 
.PARAMETER Handler
    The Click event handler to be added to each menu item.
 
.EXAMPLE
    Add-ClickToEveryMenuItem -MenuObj $menu -Handler $handler
 
    Traverses the visual tree of the $menu object and adds the $handler Click event handler to every menu item found.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-18 14:22:52
#>

Function Add-ClickToEveryMenuItem {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        $MenuObj,
        [Parameter(Mandatory = $true)]
        [ScriptBlock]$Handler
    )

    # Dump Parameter Values
    $PSBoundParameters.GetEnumerator() | ForEach-Object {
        Write-Verbose "$($_.Key) = $($_.Value)"
    }

    If ([String]::IsNullOrEmpty($Handler)) {
        Write-Warning "Parameter -Handler `$Handler cannot be blank"
        Return
    }
    If ([String]::IsNullOrEmpty($MenuObj)) {
        Write-Warning "Parameter -MenuObj `$MenuObj cannot be blank"
        Return
    }

    Write-Verbose ("Menu Object: [{0}] - {1}" -f $MenuObj.Name, $MenuObj.GetType().ToString())
    ForEach ($child in $MenuObj.Items) {
        Write-Verbose (" {0} - [{1}]" -f $child.Header, $child.GetType().ToString())
        If ($child.HasItems) {
            Add-ClickToEveryMenuItem -MenuObj $child -Handler:$Handler
        } Else {
            If ($child -is 'System.Windows.Controls.MenuItem') {
                If (-not [String]::IsNullOrEmpty($Handler)) {
                    Write-Verbose (" Adding Click Event For: {0}" -f $child.Content)
                    $child.Add_Click($Handler)
                } Else {
                    $child.Add_Click($null)
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Adds event handlers to every checkbox within a given element.
 
.DESCRIPTION
    The Add-EventsToEveryCheckBox function traverses the visual tree of a given element and adds event handlers to every checkbox found. Specific event handlers can be added based on the properties of the checkboxes.
 
.PARAMETER Element
    The root element to start the search.
 
.PARAMETER ClickHandler
    The Click event handler to be added to each checkbox.
 
.PARAMETER CheckedHandler
    The Checked event handler to be added to each checkbox.
 
.PARAMETER UncheckedHandler
    The Unchecked event handler to be added to each checkbox.
 
.PARAMETER PreviewMouseUpHandler
    The PreviewMouseUp event handler to be added to each TreeViewItem.
 
.PARAMETER PreventSelectionScrolling
    A switch to prevent horizontal content scrolling when an item is clicked.
 
.EXAMPLE
    Add-EventsToEveryCheckBox -Element $window -ClickHandler $clickHandler
 
    Traverses the visual tree of the $window element and adds the $clickHandler Click event handler to every checkbox found.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-18 14:22:52
#>

Function Add-EventsToEveryCheckBox {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        $Element,
        [Parameter(Mandatory = $false)]
        [System.Windows.RoutedEventHandler]$ClickHandler = $null,
        [Parameter(Mandatory = $false)]
        [System.Windows.RoutedEventHandler]$CheckedHandler = $null,
        [Parameter(Mandatory = $false)]
        [System.Windows.RoutedEventHandler]$UncheckedHandler = $null,
        [Parameter(Mandatory = $false)]
        [System.Windows.Input.MouseButtonEventHandler]$PreviewMouseUpHandler = $null,
        [Switch]$PreventSelectionScrolling
    )

    # Dump Parameter Values
    # $PSBoundParameters.GetEnumerator() | ForEach-Object {
    # Write-Verbose "$($_.Key) = $($_.Value)"
    # }

    Write-Verbose ("Add-EventsToEveryCheckBox(): [{0}] : ({1})" -f $($Element.GetType().ToString()), $($Element.Name))

    If ($Element -is 'System.Windows.Controls.TreeViewItem') {
        $Element.IsExpanded = $true # "True"
        If ($null -ne $PreviewMouseUpHandler) {
            $Element.Add_PreviewMouseUp($PreviewMouseUpHandler)
        }
        If ($PreventSelectionScrolling) {
            # This prevents horizontal content scrolling when an item is clicked
            $Element.Add_RequestBringIntoView({
                Param(
                    [object]$theSender, 
                    [System.Windows.RequestBringIntoViewEventArgs]$e
                )
                Write-Verbose ("`$Element.Add_RequestBringIntoView {0}: {1}({2})" -f $theSender.Name, $e.Source.Name, $e.ToString())
                # Mark the event as handled
                $e.Handled = $true
            })
        }
        <#
        $Count = [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($Element)
        For ($i=0; $i -lt $Count; $I++) {
            $current = [System.Windows.Media.VisualTreeHelper]::GetChild($Element, $i);
            Add-EventsToEveryCheckBox -Element $current -ClickHandler:$ClickHandler -CheckedHandler:$CheckedHandler -UncheckedHandler:$UncheckedHandler
        }
        #>

        Write-Verbose (" Header Name: {0}, Type: {1}" -f $Element.Header.Type, $Element.Header.Name)
        <#
        If ($Element.Header.Type -eq 'Server') {
            $current = [System.Windows.Media.VisualTreeHelper]::GetChild($Element, 0);
            Add-EventsToEveryCheckBox -Element $current -ClickHandler:$ClickHandler -CheckedHandler:$CheckedHandler -UncheckedHandler:$UncheckedHandler -PreviewMouseUpHandler:$PreviewMouseUpHandler
        }
        #>

    } ElseIf ($Element -is 'System.Windows.Controls.TextBlock') {
        Write-Verbose (" TextBlock: {0}" -f $($Element.Inlines.Text | Out-String))
    } ElseIf ($Element -is 'System.Windows.Controls.CheckBox') {
        Write-Verbose (" Adding CheckBox Event Handlers For: " + $Element.Content.Inlines.text -join "")
        If (-not [Sting]::IsNullOrEmpty($ClickHandler)) {
            $Element.Add_Click($ClickHandler)
        }
        If (-not [Sting]::IsNullOrEmpty($CheckedHandler)) {
            $Element.Add_Checked($CheckedHandler)
        }
        If (-not [Sting]::IsNullOrEmpty($UncheckedHandler)) {
            $Element.Add_Unchecked($UncheckedHandler)
        }
    }

    $Count = [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($Element)
    For ($i=0; $i -lt $Count; $i++) {
        $current = [System.Windows.Media.VisualTreeHelper]::GetChild($Element, $i)
        Add-EventsToEveryCheckBox -Element $current -ClickHandler:$ClickHandler -CheckedHandler:$CheckedHandler -UncheckedHandler:$UncheckedHandler -PreviewMouseUpHandler:$PreviewMouseUpHandler
    }
}

<#
.SYNOPSIS
    Generates Event Handler Code Snippet for the desired controlType.
 
.DESCRIPTION
    The Build-HandlerCode function generates an event handler code snippet for the specified control type.
    $Elements is an array of [PSCustomObject] created by the Find-EveryControl function.
    $ControlType is the type of elements in $Elements and is used mostly for naming the handler.
 
.PARAMETER Elements
    An array of [PSCustomObject] representing the elements created by Find-EveryControl() function.
    [PSCustomObject]@{
        Name = $Element.Name
        Type = $Element.GetType().ToString()
        Text = (Get-ControlContent -Element $Element)
        Element = $Element
    }
 
.PARAMETER ControlType
    Is the type of elements in $Elements and is used mostly for naming of the handler
 
.EXAMPLE
    $elements = @()
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
    Build-HandlerCode -Elements $elements -ControlType System.Windows.Controls.MenuItem
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-19 18:12:04
#>

Function Build-HandlerCode {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [System.Object[]]$Elements,
        [Parameter(Mandatory = $true)]
        [String]$ControlType
    )

    If ($null -eq $Elements) {
        Write-Warning "Parameter -Elements is empty or missing"
        Return
    }

    If ([String]::IsNullOrEmpty($ControlType)) {
        Write-Host "Parameter -ControlType is empty or missing" -ForegroundColor Red
        Return
    }

    $eol = [System.Environment]::NewLine
    $sb = [System.Text.StringBuilder]::new()
    $handlerName = "`$handler_$($ControlType.Split('.')[-1])_Click"

    # Starting Code
    [void]$sb.AppendLine(@"
$handlerName = {
    Param ([object]`$theSender, [System.EventArgs]`$e)
    Write-Host ("``$handlerName() Item clicked: {0}" -f `$theSender.Name)
    Switch -Regex (`$theSender.Name) {
"@
 + $eol)

    # Body Code consists of Regex patterns of the Item Element Name it finds of matching ControlTypes
    ForEach ($element in $Elements) {
        If (-not [String]::IsNullOrEmpty($element.Name)) {
            [void]$sb.AppendLine(@"
            '^$($element.Name)$' {
                Break
            }
"@
)
        }
    }

    # Ending Code
    [void]$sb.AppendLine(@"
        default {
            Write-Host ("{0}: {1}({2})" -f `$theSender.Name, `$e.OriginalSource.Name, `$e.OriginalSource.ToString())
        }
    }
}
"@
)

    $sb.ToString()
}

<# Usage Example # >
$elements = @()
$elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
$elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
Build-HandlerCode -Elements $elements -ControlType 'System.Windows.Controls.MenuItem'
# How to use the same $elements list to Add_Click() Events
$elements.ForEach({$_.Element.Add_Click($handler_MenuItem_Click)})
#>


<#
.SYNOPSIS
    Finds every control of a specified type within a given element.
 
.DESCRIPTION
    The Find-EveryControl function traverses the visual or logical tree of a given element and finds every control of a specified type. Specific controls can be excluded based on their properties.
 
.PARAMETER ControlType
    The type of control to Gather
 
.PARAMETER Element
    The root element to start searching from
 
.PARAMETER ExcludeElement
    A switch to exclude attaching the element to the results.
 
.PARAMETER UseVisualTreeHelper
    A switch to use VisualTreeHelper for the search.
 
.PARAMETER IncludeAll
    A switch to include all controls in the search results.
 
.EXAMPLE
    Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
 
    Finds every ToggleButton control within the $form element.
 
.EXAMPLE
    Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -ExcludeElement -UseVisualTreeHelper -IncludeAll
     
    Finds all controls (-IncludeAll) using VisualTreeHelper. Best use after exiting ShowDialog(). Excludes attaching Element (-ExcludeElement) to result
     
.EXAMPLE
    $elements = @()
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
    $elements.ForEach({$_.Element.Add_Click($handler_MenuItem_Click)})
 
    Find all MenuItem and ToggleButton elements and Add Click Event Handler
 
.EXAMPLE
    $elements = @()
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton' -ExcludeElement
    $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -ExcludeElement
    Build-HandlerCode -Elements $elements -ControlType System.Windows.Controls.MenuItem
     
    Find all MenuItem and ToggleButton elements and Generate the code for the Click Event Handler
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-19 14:29:36
#>

Function Find-EveryControl {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        $ControlType,
        [Parameter(Mandatory = $true)]
        $Element,
        [Parameter(Mandatory = $false)]
        [Switch]$ExcludeElement,
        [Parameter(Mandatory = $false)]
        [Switch]$UseVisualTreeHelper,
        [Parameter(Mandatory = $false)]
        [Switch]$IncludeAll
    )

    If (-not [String]::IsNullOrEmpty($IncludeAll) -and $IncludeAll) {
        $includeElement = $null
        If (-not $ExcludeElement -or [String]::IsNullOrEmpty($ExcludeElement)) {
            $includeElement = $Element
        }
        [PSCustomObject]@{
            Name = $Element.Name
            Type = $Element.GetType().ToString()
            Text = (Get-ControlContent -Element $Element)
            Element = $includeElement
        }        
    } Else {
        If ($Element -is $ControlType) {
            $includeElement = $null
            If (-not $ExcludeElement -or [String]::IsNullOrEmpty($ExcludeElement)) {
                $includeElement = $Element
            }
            [PSCustomObject]@{
                Name = $Element.Name
                Type = $Element.GetType().ToString()
                Text = (Get-ControlContent -Element $Element)
                Element = $includeElement
            }
        }
    }

    If ($UseVisualTreeHelper) {
        $Count = [System.Windows.Media.VisualTreeHelper]::GetChildrenCount($Element)
        If ($Count -gt 0) {
            For ($i=0; $i -lt $Count; $i++) {
                $current = [System.Windows.Media.VisualTreeHelper]::GetChild($Element, $i)
                Find-EveryControl -Element $current -ControlType:$ControlType -IncludeAll:$IncludeAll -ExcludeElement:$ExcludeElement -UseVisualTreeHelper:$UseVisualTreeHelper
            }
        }
    } Else {
        If ($Element.HasContent) {
            ForEach ($item in $Element.Content) {
                Find-EveryControl -Element $item -ControlType:$ControlType -IncludeAll:$IncludeAll -ExcludeElement:$ExcludeElement
            }
        }
        If ($Element.Children) {
            ForEach ($child in $Element.Children) {
                Find-EveryControl -Element $child -ControlType:$ControlType -IncludeAll:$IncludeAll -ExcludeElement:$ExcludeElement
            }
        }
        If ($Element.HasItems) {
            ForEach ($item in $Element.Items) {
                Find-EveryControl -Element $item -ControlType:$ControlType -IncludeAll:$IncludeAll -ExcludeElement:$ExcludeElement
            }
        }
    }
}

# Example usage:
# Find-EveryControl -Element $WPF_menuMasterDataGrid -ControlType 'System.Windows.Controls.MenuItem'
# Find-EveryControl -Element $WPF_menuDetailDataGrid -ControlType 'System.Windows.Controls.MenuItem'
# Find-EveryControl -Element $WPF_tabControl -ControlType 'System.Windows.Controls.Button'
# Find-EveryControl -Element $WPF_gridSqlQueryEditor -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
# Find-EveryControl -Element $WPF_tabControl -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.RadioButton'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Combobox'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Button'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -IncludeAll -ExcludeElement -UseVisualTreeHelper
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -IncludeAll -ExcludeElement

# How to identify all controls (-IncludeAll) from VisualTreeHelper. Use after exiting ShowDialog()
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -ExcludeElement -UseVisualTreeHelper -IncludeAll
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -ExcludeElement -UseVisualTreeHelper
# Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem' -ExcludeElement


<#
.SYNOPSIS
Locates the root element (main form) of a control.
 
.DESCRIPTION
This function traverses the visual tree to find the root element of a given control.
 
.PARAMETER Element
The control for which to find the root element.
 
.EXAMPLE
    $form = Find-RootElement -Element $Button
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-19 14:29:36
#>


Function Find-RootElement {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.Windows.DependencyObject]
        $Element
    )
    While ($Parent = [System.Windows.Media.VisualTreeHelper]::GetParent($Element)) {
        $Element = $Parent
    }
    Return $Element
}

<#
.SYNOPSIS
    Formats an XML string with indentation and optional attribute formatting and XML declaration.
 
.DESCRIPTION
    The Format-XML function formats an XML string with a specified indentation level. It provides options to format attributes with new lines and to include or omit the XML declaration.
 
.PARAMETER xml
    The XML string to be formatted.
 
.PARAMETER indent
    The number of spaces to use for indentation. Default is 4.
 
.PARAMETER FormatAttributes
    A switch to format attributes with new lines.
 
.PARAMETER IncludeXmlDeclaration
    A switch to include the XML declaration.
 
.EXAMPLE
    Format-XML -xml $inputXML -indent 4
 
    Formats the XML string $inputXML with an indentation level of 4 spaces.
 
.EXAMPLE
    Format-XML -xml $inputXML -indent 4 -FormatAttributes
 
    Formats the XML string $inputXML with an indentation level of 4 spaces and formats attributes with new lines.
 
.EXAMPLE
    Format-XML -xml $inputXML -indent 4 -IncludeXmlDeclaration
 
    Formats the XML string $inputXML with an indentation level of 4 spaces and includes the XML declaration.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-18 14:22:52
#>

function Format-XML {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory=$true,ValueFromPipeLine=$true)]
        [xml]$xml,
        [parameter(Mandatory=$false,ValueFromPipeLine=$false)]
        $indent = 4,
        [switch]$FormatAttributes,
        [switch]$IncludeXmlDeclaration
    )
    Process {
        $StringWriter = New-Object System.IO.StringWriter
        $XmlWriterSettings = New-Object System.Xml.XmlWriterSettings
        $XmlWriterSettings.Indent = $true
        $XmlWriterSettings.IndentChars = " " * $indent
        If (-not [Sting]::IsNullOrEmpty($IncludeXmlDeclaration)) {
            $XmlWriterSettings.OmitXmlDeclaration = $true
        }
        If (-not [Sting]::IsNullOrEmpty($FormatAttributes)) {
            $XmlWriterSettings.NewLineOnAttributes = $true
        }
        $XmlWriter = [System.Xml.XmlWriter]::Create($StringWriter, $XmlWriterSettings)
        $xml.WriteTo($XmlWriter)
        $XmlWriter.Flush()
        $StringWriter.Flush()
        $XmlWriter.Close()
        $Result = $StringWriter.ToString()
        $StringWriter.Dispose()
        $Result
    }
}

<#
.SYNOPSIS
    Retrieves the content of a given control element.
 
.DESCRIPTION
    The Get-ControlContent function retrieves the content of a given control element based on its properties, such as Content, Header, Text, or SelectedItem.
 
.PARAMETER Element
    The control element from which to retrieve the content.
 
.EXAMPLE
    Get-ControlContent -Element $button
 
    Retrieves the content of the $button element.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-19 18:16:11
#>

Function Get-ControlContent {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        $Element 
    )

    Switch ($Element) {
        { $_.Content }        { Return $_.Content }
        { $_.Header }         { Return $_.Header }
        { $_.Text }           { Return $_.Text }
        { $_.SelectedItem }   { Return $_.SelectedItem }
        Default               { Return $_.Name }
    }
}

<#
.SYNOPSIS
Opens a Windows OpenFileDialog to select a file.
 
.DESCRIPTION
This function opens a Windows OpenFileDialog to allow the user to select a file.
It supports options for initial directory, file filter, title dialog, and multi-select.
 
.PARAMETER InitialDirectory
The initial directory to open the file dialog.
 
.PARAMETER FileFilter
The file filter to apply in the dialog.
 
.PARAMETER TitleDialog
The title of the file dialog.
 
.PARAMETER AllowMultiSelect
Specifies if multiple files can be selected.
 
.PARAMETER SaveAs
Specifies if the dialog should be a SaveFileDialog.
 
.EXAMPLE
$fileName = Get-FileFromDialog -fileFilter 'CSV file (*.csv)|*.csv' -titleDialog "Select A CSV File:"
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2025"
#>


Function Get-FileFromDialog {
    [CmdletBinding()] 
    Param (
        [Parameter(Position=0)]
        [string]$InitialDirectory = './',
        
        [Parameter(Position=0)]
        [string]$InitialFileName,
        
        [Parameter(Position=1)]
        [string]$FileFilter = 'All files (*.*)| *.*',
        
        [Parameter(Position=2)] 
        [string]$TitleDialog = '',
        
        [Parameter(Position=3)] 
        [switch]$AllowMultiSelect = $false,
        
        [switch]$SaveAs
    )

    [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null

    If (-Not $SaveAs) {
        $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    } Else {
        $OpenFileDialog = New-Object System.Windows.Forms.SaveFileDialog
        # $OpenFileDialog.ValidateNames = $false
        $OpenFileDialog.OverwritePrompt = $false
    }

    $OpenFileDialog.InitialDirectory = $InitialDirectory
    $OpenFileDialog.FileName = $InitialFileName
    $OpenFileDialog.Filter = $FileFilter
    $OpenFileDialog.Title = $TitleDialog
    $OpenFileDialog.ShowHelp = If ($Host.Name -eq 'ConsoleHost') { $true } Else { $false }
    
    If ($AllowMultiSelect) { $OpenFileDialog.MultiSelect = $true } 
    
    $OpenFileDialog.ShowDialog() | Out-Null
    
    If ($AllowMultiSelect) { 
        Return $OpenFileDialog.Filenames 
    } Else { 
        Return $OpenFileDialog.Filename 
    }
}

<#
.SYNOPSIS
    Displays WPF_* form variables which are XAML elements whose x:Name begins with an _ underscore character.
 
.DESCRIPTION
    This function displays a list of WPF_* variables of XAML Elements which can accessed and referenced directly,
 
.EXAMPLE
    Get-FormVariable
-----------------------------------------------------------------
Found the following intractable elements from our form
-----------------------------------------------------------------
Name Value
---- -----
WPF_dataGrid System.Windows.Controls.DataGrid Items.Count:0
WPF_saveButton System.Windows.Controls.Button: Save
-----------------------------------------------------------------
 
.NOTES
    Author: Brooks Vaughn
    Date: Wednesday, 19 February 2025 19:26
#>


Function Get-FormVariable {
    [CmdletBinding()]
    param ()

    if ($null -eq $ReadmeDisplay -or $ReadmeDisplay -eq $false) {
        "If you need to reference this display again, run Get-FormVariables"
    }
    ("`n$("-" * 65)`nGet-FormVariable() Found the following intractable elements from the XAML form`n$("-" * 65)")
    ((Get-Variable WPF*) | Select-Object -Property Name, Value | Out-String).Trim()
    ("$("-" * 65)`n")
}


<#
.SYNOPSIS
    Returns a PSObject containing details about an object's properties.
 
.DESCRIPTION
    The Get-ObjectPropertyDetail function returns a PSObject that lists the properties of the input object along with their names, values, and types.
 
.PARAMETER InputObject
    The object whose properties are to be detailed.
 
.EXAMPLE
    $details = Get-ObjectPropertyDetail -InputObject $form1
    $details | Format-Table -AutoSize
    $details | Format-Table -AutoSize -Force -Wrap -Property Property, Value, Type
 
    Retrieves the properties of the object $form1 and displays them in a formatted table.
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-18 15:41:05
#>

Function Get-ObjectPropertyDetail {
    [CmdletBinding()]
    Param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]$InputObject
    )
    Begin {
        $ReturnObject = [System.Collections.Generic.List[PSCustomObject]]::new()
    }
    Process {
        ForEach ($property in $InputObject.PSObject.Properties) {
            $typeName = [Microsoft.PowerShell.ToStringCodeMethods]::Type([type]$property.TypeNameOfValue)
            $value = $property.Value

            If ($typeName -like '*IScriptExtent*') {
                $file = if ($null -eq $value.File) { "" } else { Split-Path -Leaf $value.File }
                $value = "{0} ({1},{2})-({3},{4})" -f $file, $value.StartLineNumber, $value.StartColumnNumber, $value.EndLineNumber, $value.EndColumnNumber
            }

            [void]$ReturnObject.Add([PSCustomObject]@{
                Property = $property.Name
                Value    = $value
                Type     = $typeName
            })
        }
    }
    End {
        $ReturnObject
    }
}


<#
.SYNOPSIS
Creates a unique filename by adding a sequence number or timestamp to ensure the filename is unique.
 
.DESCRIPTION
This function creates a unique filename based on an input filename used as a template.
It adds a date timestamp and/or a sequence number (.01, .02,...,.99) if the new file name already exists.
 
.PARAMETER Name
The existing filename to make Unique
 
.PARAMETER NoDate
Specifies not to add a date to the filename.
 
.PARAMETER AddTime
Specifies to add a time to the filename.
 
.PARAMETER AddSeq
Specifies to add a sequence number to the filename.
 
.EXAMPLE
Get-UniqueFileName -Name "FileNameTemplate.ext"
 
.EXAMPLE
Get-UniqueFileName -Name "C:\Temp\Prod"
 
.EXAMPLE
Get-UniqueFileName -Name "C:\Temp\Prod" -AddSeq
 
.EXAMPLE
Get-UniqueFileName -Name "C:\Temp\Prod" -NoDate
 
.EXAMPLE
Get-UniqueFileName -Name "C:\Temp\Prod" -NoDate -AddSeq
 
Get-UniqueFileName -Name C:\Git\GuiMyPS\src\public\Get-UniqueFileName.ps1 -NoDate -AddSeq
 
Get-UniqueFileName -Name FileNameTemplate_2025-0223.ext -NoDate -AddSeq
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2012"
#>



Function Get-UniqueFileName {
    [CmdletBinding()]
    Param (
        [Parameter(HelpMessage="Existing FileName to Copy", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [switch]$NoDate,
        [switch]$AddTime,
        [switch]$AddSeq
    )
    Begin {
        $NewFileName = [String]::Empty
        # Save File Ext and Get Root of File
        $PathToFile = [System.IO.Path]::GetDirectoryName($Name)
        $Ext = [System.IO.Path]::GetExtension($Name)
        $FileName = [System.IO.Path]::GetFileNameWithoutExtension($Name)

        # Strip Off Date / Time
        $FileName = ($FileName -replace "_\d{4}-\d{4}\D{1}?\d{4,6}|_\d{4}-\d{4}", "")

        # Strip off any Sequence such as .000 to .999 extension
        If ([System.IO.Path]::GetExtension($FileName) -match "\.\d{3}") {
            $FileName = [System.IO.Path]::GetFileNameWithoutExtension($FileName)
        }
    }
    Process {
        Try {
            If (-not $NoDate) {
                # Add Date Stamp
                $FileName += "_$(Get-Date -Format yyyy-MMdd)"

                # Add Time Stamp
                If ($AddTime) {
                    # Add Time Stamp to FileName
                    $FileName += "-$(Get-Date -Format HHmm)"
                }
            }

            if ([String]::IsNullOrEmpty($newFileName)) {
                $newFileName = [System.IO.Path]::Combine($pathToFile, "$FileName$($ext)")
            }            

            # Verify that New FileName does not already exist and Add Sequence # when it does
            $i = 0
            While ((Test-Path -Path $NewFileName) -or $AddSeq) {
                $AddSeq = $false
                $i++
                $Seq = "{0:D3}" -f ([int]$i)
                $NewFileName = [System.IO.Path]::Combine($PathToFile, "$FileName.$Seq$($Ext)")
            }
            $Host.UI.WriteDebugLine("Get-UniqueFileName() New FileName: ($NewFileNameItem)")
        } Catch {
            Write-Error $_
        }
    }

    End {
        $NewFileName
    }
}

<#
.SYNOPSIS
Verifies that the required modules are installed and imported.
 
.DESCRIPTION
This function checks if a specified module is installed.
If not, it installs the module from the PSGallery repository and imports it.
If the module is already installed, it simply imports the module.
 
.PARAMETER ModuleName
The name of the module to verify, install, and import.
 
.EXAMPLE
Initialize-Module -ModuleName "SqlQueryClass"
 
.EXAMPLE
Initialize-Module -ModuleName "GuiMyPS"
 
.EXAMPLE
Initialize-Module -ModuleName "ImportExcel"
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2025"
#>


Function Initialize-Module {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$ModuleName
    )

    # Check if the module is already installed
    $module = Get-Module -ListAvailable -Name $ModuleName
    If ($null -eq $module) {
        # Module is not installed, install it
        Write-Host "Module '$ModuleName' is not installed. Installing..."
        Install-Module -Name $ModuleName -Repository PSGallery -Scope CurrentUser
    
        # Import the newly installed module
        Write-Host "Importing module '$ModuleName'..."
        Import-Module -Name $ModuleName
    } Else {
        # Module is already installed, import it
        Write-Host "Module '$ModuleName' is already installed. Importing..."
        Import-Module -Name $ModuleName
    }

    # Verify the module is imported
    If (Get-Module -Name $ModuleName) {
        Write-Host "Module '$ModuleName' has been successfully imported."
    } Else {
        Write-Host "Failed to import module '$ModuleName'."
    }
}

<#
.SYNOPSIS
New-Popup -- Display a Popup Message
 
.DESCRIPTION
This command uses the Wscript.Shell PopUp method to display a graphical message
box. You can customize its appearance of icons and buttons. By default the user
must click a button to dismiss but you can set a timeout value in seconds to
automatically dismiss the popup.
 
The command will write the return value of the clicked button to the pipeline:
  OK = 1
  Cancel = 2
  Abort = 3
  Retry = 4
  Ignore = 5
  Yes = 6
  No = 7
 
  If no button is clicked, the return value is -1.
 
  .OUTPUTS
    Null = -1
    OK = 1
    Cancel = 2
    Abort = 3
    Retry = 4
    Ignore = 5
    Yes = 6
    No = 7
 
.EXAMPLE
PS C:\> New-Popup -Message "The update script has completed" -Title "Finished" -Time 5
 
This will display a popup message using the default OK button and default
Information icon. The popup will automatically dismiss after 5 seconds.
 
.NOTES
Original Source: http://powershell.org/wp/2013/04/29/powershell-popup/
Original Source: The Lonely Administrator's https://jdhitsolutions.com/blog/powershell/2976/powershell-popup/
Last Updated: April 8, 2013
Version : 1.0
Similar to: Bill Riedy's https://www.powershellgallery.com/packages/PoshFunctions/2.2.1.6/Content/Functions%5CNew-Popup.ps1
#>


#------------------------------------------------------------------
# Display a Popup Message
#------------------------------------------------------------------
Function New-Popup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Position=0, Mandatory=$true, HelpMessage="Enter a message for the popup")]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Position=1, Mandatory=$true, HelpMessage="Enter a title for the popup")]
        [ValidateNotNullOrEmpty()]
        [string]$Title,

        [Parameter(Position=2, HelpMessage="How many seconds to display? Use 0 to require a button click.")]
        [ValidateScript({$_ -ge 0})]
        [int]$Time = 0,

        [Parameter(Position=3, HelpMessage="Enter a button group")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("OK", "OKCancel", "AbortRetryIgnore", "YesNo", "YesNoCancel", "RetryCancel")]
        [string]$Buttons = "OK",

        [Parameter(Position=4, HelpMessage="Enter an icon set")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("Stop", "Question", "Exclamation", "Information")]
        [string]$Icon = "Information"
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Display Popup Message")) {
            # Convert buttons to their integer equivalents
            Switch ($Buttons) {
                "OK"               {$ButtonValue = 0}
                "OKCancel"         {$ButtonValue = 1}
                "AbortRetryIgnore" {$ButtonValue = 2}
                "YesNo"            {$ButtonValue = 4}
                "YesNoCancel"      {$ButtonValue = 3}
                "RetryCancel"      {$ButtonValue = 5}
            }
        }
        
        # Set an integer value for icon type
        Switch ($Icon) {
            "Stop"        {$IconValue = 16}
            "Question"    {$IconValue = 32}
            "Exclamation" {$IconValue = 48}
            "Information" {$IconValue = 64}
        }
        
        # Create the COM Object
        Try {
            $Wshell = New-Object -ComObject Wscript.Shell -ErrorAction Stop
            # Button and icon type values are added together to create an integer value
            $Wshell.Popup($Message, $Time, $Title, $ButtonValue + $IconValue)
        } Catch {
            Write-Warning "Failed to create Wscript.Shell COM object"
            Write-Warning $_.Exception.Message
        }
    }
}

<#
.SYNOPSIS
    Helper function that processes and loads XAML (as string, filename, or as [XML] object) into a WPF Form Object.
 
.DESCRIPTION
    The New-XamlWindow function processes and loads XAML (as string, filename, or as [XML] object) into a WPF Form Object.
 
.PARAMETER xaml
    The XAML content to process and load.
 
.PARAMETER NoXRemoval
    A switch to prevent the removal of x:Name attributes.
 
.EXAMPLE
    # Test for XAML String
    try {
        $form1 = New-XamlWindow -xaml $inputXML
        Add-ClickToEveryButton -Element $form1 -ClickHandler $handler_button_Click
        $form1.ShowDialog()
    } catch {
        Write-Warning ($_ | Format-List | Out-String)
    }
 
.EXAMPLE
    # Test for File Path to XAML file with two approaches for Adding Click Events
    # Note: Code for $handler_MenuItem_Click can be generated using Build-HandlerCode()
    try {
        $form = New-XamlWindow -xaml 'C:\Git\SqlQueryEditor\src\resources\SqlQueryEditor.xaml'
        # Add-ClickToEveryMenuItem -MenuObj $WPF_menuMasterDataGrid -Handler $handler_MenuItem_Click
        # Add-ClickToEveryMenuItem -MenuObj $WPF_menuDetailDataGrid -Handler $handler_MenuItem_Click
        $elements = @()
        $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.Primitives.ToggleButton'
        $elements += Find-EveryControl -Element $form -ControlType 'System.Windows.Controls.MenuItem'
        $elements.ForEach({$_.Element.Add_Click($handler_MenuItem_Click)})
        $form.ShowDialog()
    } catch {
        Write-Warning ($_ | Format-List | Out-String)
    }
 
.EXAMPLE
    # Test for [System.Xml.XmlDocument]
    try {
        $xaml = [xml]$inputXML
        $form2 = New-XamlWindow -xaml $xaml
        Add-ClickToEveryButton -Element $form2 -ClickHandler $handler_button_Click
        $form2.ShowDialog()
    } catch {
        Write-Warning ($_ | Format-List | Out-String)
    }
 
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-19 18:28:36
#>

Function New-XamlWindow {
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true)]
        [Alias("InputObject")]
        [System.Object]$xaml,

        [Parameter(Mandatory = $false)]
        [switch]$NoXRemoval
    )

    # Load the necessary assemblies
    Add-Type -AssemblyName PresentationFramework

    <#
    XAML Elements might use either x:Name="" or Name=""
    The x: refers to the namespace xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" which is not automatically defined in PowerShell.
    To use XPath queries during preprocessing of the XAML Text, you have to create a NamespaceManager and add the missing namespaces.
    For x:Name, the XPath query would be $xaml.SelectNodes("//*[@x:Name]", $nsManager).
    For Name, the XPath query would be $xaml.SelectNodes("//*[@Name]", $nsManager).
    By removing x:Name from the XAML string before converting to [System.Xml.XmlDocument], there is no need for the NamespaceManager and XPath needs only $xaml.SelectNodes("//*[@Name]").
    #>

    Function Remove-XName {
        [CmdletBinding(SupportsShouldProcess = $true)]
        Param (
            [Parameter(Mandatory = $true)]
            [string]$xamlString,

            [Parameter(Mandatory = $false)]
            [switch]$NoXRemoval
        )

        If ($NoXRemoval) {
            $xamlString
        } Else {
            If ($PSCmdlet.ShouldProcess("XAML String", "Remove x:Name attributes")) {
                ((($xamlString -replace 'mc:Ignorable="d"', '') -replace "x:Na", 'Na') -replace '^<Win.*', '<Window')
            } Else {
                $xamlString
            }
        }
    }

    # Define return and working variables
    $uiForm = $null
    $xamlReader = $null

    Try {
        # Process the $xaml input object into an XmlDocument and create reader
        If ($xaml -is [System.Xml.XmlDocument]) {
            $newXaml = $xaml
        } ElseIf ($xaml -is [System.String]) {
            If (Test-Path -Path $xaml -PathType Leaf -ErrorAction SilentlyContinue) {
                $xamlString = Remove-XName -NoXRemoval:$NoXRemoval -xamlString (Get-Content -Path $xaml -Raw)
                If ($xamlString -match '<(Window|Grid|StackPanel|Button|DataGrid|TextBox)') {
                    $newXaml = [xml]$xamlString
                } Else {
                    Throw "-xaml parameter as file path was valid but its content is not XAML"
                }
            } ElseIf ($xaml -match '<(Window|Grid|StackPanel|Button|DataGrid|TextBox)') {
                $xamlString = Remove-XName -NoXRemoval:$NoXRemoval -xamlString $xaml
                $newXaml = [xml]$xamlString
            } Else {
                Throw "-xaml parameter as XAML String has no valid XAML content"
            }
        } Else {
            Throw "-xaml is not a valid [System.Xml.XmlDocument], XAML string, or filepath to a XAML file"
        }

        # Perform XML data cleanup and create PowerShell $WPF_* Variables for XAML names that begin with "_"
        # Remove unsupported namespaces created by XAML editors like Blend / Visual Studio
        $newXaml.Window.RemoveAttribute('x:Class')
        $newXaml.Window.RemoveAttribute('d')
        $newXaml.Window.RemoveAttribute('mc:Ignorable')

        $problemObjects = $newXaml.SelectNodes("//*[@Name]").Where({
            $_.Click -or $_.TextChanged -or $_.SelectionChanged -or $_.Checked -or $_.Unchecked -or $_.ValueChanged}) | 
                Select-Object -Property Name, LocalName, Click, TextChanged, SelectionChanged, Checked, Unchecked, ValueChanged

        $errorHeading = "XAML String has unsupported events defined and cannot be converted by PowerShell WPF" + [System.Environment]::NewLine
        $errorText = ForEach ($obj in $problemObjects) {
            Switch ($obj) {
                { $_.Click } { "$($_.LocalName) named ($($_.Name)) has Click event and needs to be removed"; break }
                { $_.TextChanged } { "$($_.LocalName) named ($($_.Name)) has TextChanged event and needs to be removed"; break }
                { $_.SelectionChanged } { "$($_.LocalName) named ($($_.Name)) has SelectionChanged event and needs to be removed"; break }
                { $_.Checked } { "$($_.LocalName) named ($($_.Name)) has Checked event and needs to be removed"; break }
                { $_.Unchecked } { "$($_.LocalName) named ($($_.Name)) has Unchecked event and needs to be removed"; break }
                { $_.ValueChanged } { "$($_.LocalName) named ($($_.Name)) has ValueChanged event and needs to be removed"; break }
                Default { "Skipping: $($_.LocalName) named ($($_.Name))" }
            }
        }
        If ($problemObjects.Count -gt 0) {
            Write-Host ($errorHeading) -ForegroundColor Red
            Write-Host ($errorText | Out-String) -ForegroundColor Magenta
            Write-Host "Please remove the event definitions from the XAML and try again" -ForegroundColor Red
            Exit
        }

        # Create and Load the XAML Reader to create the WPF Form
        $xamlReader = (New-Object System.Xml.XmlNodeReader $newXaml)
        $uiForm = [System.Windows.Markup.XamlReader]::Load($xamlReader)

        # Create a PowerShell Variable $WPF_ for each Form Object defined in the XAML where the
        # element name begin with "_" as in Name="_MyObj" or x:Name="_MyObj"
        # For the most part, this allows the Code-Behind to directly access the element without needing to find it first
        $FormObjects = $newXaml.SelectNodes("//*[@Name]").Where({ $_.Name -like "_*" }).ForEach({
            $element = $uiForm.FindName($_.Name)
            If ($element) {
                Write-Output ("WPF$($_.Name)")
                Set-Variable -Name "WPF$($_.Name)" -Value $element -Force -Scope Global -Visibility Public
            } Else {
                Write-Warning "Unable to locate $($_.Name) element name"
            }
        })
        Write-Verbose "`nList of `$WPF_ named Variables of XAML Elements`n$($FormObjects | Out-String)`n"
        $uiForm
    } Catch {
        Write-Error ($_ | Format-List | Out-String)
    }
}

<#
.SYNOPSIS
Saves the content of a DataGrid to various formats such as Clipboard, CSV, or Excel.
 
.DESCRIPTION
This function converts the content of a DataGrid and saves it to the specified format.
It supports saving to Clipboard, CSV files, and Excel files.
 
.PARAMETER InputObject
The object to convert and save. This can be a DataGrid or DataRowView.
 
.PARAMETER Path
The path and filename of the SaveAs file.
 
.PARAMETER SaveAs
The target output format. This can be Clipboard, CSV, or Excel.
 
.EXAMPLE
Save-DataGridContent -InputObject $WPF_dataGridSqlQuery -SaveAs CVS -Verbose
Save-DataGridContent -InputObject $WPF_dataGridSqlQuery -SaveAS CSV -Path C:\Temp\dataGridSqlQuery.csv
$WPF_dataGridSqlQuery | Save-DataGridContent -SaveAs Excel -Verbose
Save-DataGridContent -InputObject $WPF_dataGridSqlQuery -SaveAs Clipboard -Verbose
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2025"
#>


Function Save-DataGridContent {
    [CmdletBinding()] 
    Param (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, HelpMessage='Object to convert and save')]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSObject]$InputObject,

        [Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, HelpMessage='Path and Filename of SaveAs File')]
        [ValidateScript({
            $Parent = Split-Path $_ -Parent -ErrorAction SilentlyContinue
            If (-Not (Test-Path -Path $Parent -PathType Container -ErrorAction SilentlyContinue)) {
                Throw "Specify a valid path. Parent '$Parent' does not exist: $_"
            }
            $true
        })]        
        [string]$Path,

        [Parameter(Position=2, Mandatory=$false, HelpMessage='Target output format')]
        [ValidateSet('Clipboard', 'CSV', 'Excel')]
        [string]$SaveAs = 'Clipboard'
    )
    Begin {
        Add-Type -AssemblyName System.Windows.Forms
        $RowCount = 0
        # $PipelineInput = -Not $PSBoundParameters.ContainsKey('InputObject')
        # $Index = 1
        $OutObject = @()
    }
    Process {
        If ($SaveAs -eq 'clipboard' -and $InputObject -is 'System.Windows.Controls.DataGrid') {
            ForEach ($Row in $InputObject.Items) {
                If ($Row.ToString() -eq '{NewItemPlaceholder}') {
                    Continue
                }
                $RowCount++
                If ($RowCount -ge $InputObject.Items.Count) {
                    $Row
                    Break
                }
                $OutObject += "Record # {0}{1}" -f $RowCount, [System.Environment]::NewLine
                ForEach ($Column in $InputObject.Columns.Header) {
                    $OutObject += "{0} : {1}{2}" -f $Column, ($Row."$Column"), [System.Environment]::NewLine
                }
                $OutObject += [System.Environment]::NewLine
            }
            $OutObject += "Total Record Count: {0}{1}" -f $RowCount, [System.Environment]::NewLine
        } ElseIf ($SaveAs -eq 'clipboard' -and $InputObject -is 'System.Data.DataRowView') {
            ForEach ($Row in $InputObject) {
                $RowCount++
                $OutObject += "Record # {0}{1}" -f $RowCount, [System.Environment]::NewLine
                ForEach ($Column in (($Row."Row").PSObject.Properties.Name | Where-Object {$_ -notin @('RowError','RowState','Table','ItemArray','HasErrors')})) {
                    $OutObject += "{0} : {1}{2}" -f $Column, ($Row."$Column"), [System.Environment]::NewLine
                }
                $OutObject += [System.Environment]::NewLine
            }
            $OutObject += "Total Record Count: {0}{1}" -f $RowCount, [System.Environment]::NewLine
        }
    }
    End {
        Switch ($SaveAs) {
            'clipboard' {
                [System.Windows.Forms.Clipboard]::SetText($OutObject)
                Write-Host ("Record Copied to {0}" -f $SaveAs)
            }
            'CSV' {
                $SaveFile = Get-FileFromDialog -InitialDirectory $Parent -InitialFileName $Path -TitleDialog "Specify SaveAs $SaveAs path and filename" -FileFilter 'CSV files (*.csv)|*.csv|CSV files (*.txt)|*.txt|All files (*.*)|*.*' -SaveAs
                If (-Not [string]::IsNullOrEmpty($SaveFile)) {
                    $SaveFile = Get-UniqueFileName -Name $SaveFile
                    $InputObject.Items.Where({$_.ToString() -ne '{NewItemPlaceholder}'}) | Export-Csv -Path $SaveFile -Force -NoTypeInformation
                    Write-Host ("{0} Saved to: {1}" -f $SaveAs, $SaveFile)
                } Else {
                    Write-Host ("SaveAs {0} aborted by user" -f $SaveAs)
                }
            }
            'Excel' {
                $SaveFile = Get-FileFromDialog -InitialDirectory $Path -TitleDialog "Specify SaveAs $SaveAs path and filename" -FileFilter 'Excel files (*.xlsx)|*.xlsx|All files (*.*)|*.*' -SaveAs
                If (-Not [string]::IsNullOrEmpty($SaveFile)) {
                    $XLFile = Get-UniqueFileName -Name $SaveFile
                    Export-XLSX -InputObject $InputObject.Items -Path $XLFile -Table -Autofit -WorksheetName $rsObject.QueryDB
                    Write-Host ("{0} Saved to: {1}" -f $SaveAs, $SaveFile)
                } Else {
                    Write-Host ("SaveAs {0} aborted by user" -f $SaveAs)
                }
            }
            Default {}
        }
    }
}

<#
.SYNOPSIS
Saves a dataset to an Excel file.
 
.DESCRIPTION
This function converts a dataset to an Excel file and saves it to the specified path.
 
.PARAMETER InputObject
The dataset to convert and save.
 
.PARAMETER Path
The path and filename of the SaveAs file.
 
.EXAMPLE
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2025"
#>


Function Save-DatasetToExcel {
    [CmdletBinding()] 
    Param (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, HelpMessage='Object to convert and save')]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSObject]$InputObject,

        [Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, HelpMessage='Path and Filename of SaveAs File')]
        [ValidateScript({
            $Parent = Split-Path $_ -Parent -ErrorAction SilentlyContinue
            If (-not (Test-Path -Path $Parent -PathType Container -ErrorAction SilentlyContinue)) {
                Throw "Specify a valid path. Parent '$Parent' does not exist: $_"
            }
            $true
        })]        
        [string]$Path
    )

    Begin {
        Initialize-Module -ModuleName "ImportExcel"

        # $RowCount = 0
        # $PipelineInput = -not $PSBoundParameters.ContainsKey('InputObject')
        # $Index = 1
        # $OutObject = @()

        If (-not [string]::IsNullOrEmpty($Path)) {
            # Create unique TimeStamped File pathname for saving output to
            $XLFile = Get-UniqueFileName -Name "$Parent\$Path"
            Write-Host ("Exporting DataSet To: {0}" -f $XLFile)
        } Else {
            Write-Host ("Error: -Path is missing or empty")
            $XLFile = Get-FileFromDialog -initialDirectory $Path -titleDialog "Specify SaveAs $Parent path and filename" -fileFilter 'Excel files (*.xlsx)|*.xlsx|All files (*.*)|*.*' -SaveAs
        }

        # ------------------------------------------------------------------
        # Begin Excel Creation
        # ------------------------------------------------------------------
        Write-Host ("Exporting Query Results to Excel File ($XLFile)")
        $xlApp = New-Object OfficeOpenXml.ExcelPackage $XLFile
        [void]$xlApp.Workbook.Worksheets.Add($WorkSheetName)
        $wbSource = $xlApp.Workbook

        # Update workBook properties
        $wbSource.Properties.Author = $env:USERNAME
        $wbSource.Properties.Title = "Query Results Export"
        $wbSource.Properties.Subject = "Query Results Export"
        $wbSource.Properties.Company = ""

        # -----------------------------------------------------
        # Create Queries Worksheet and Populate
        # -----------------------------------------------------
        Add-WorkSheet -Workbook $wbSource -Name 'Query' -Headers @{ 'Query Number' = 1; 'SQL Query' = 2 } -HeaderBorderStyle ([OfficeOpenXml.Style.ExcelBorderStyle]::Thin)
        $xlApp.Save()
        Close-ExcelPackage -ExcelPackage $xlApp
        $xlApp.Dispose()
        $xlApp = $null        
    }

    Process {
        # Add each Result Set ($InputObject.Value) as a new worksheet named as ($InputObject.Name)
        Export-XLSX -InputObject $InputObject.Value -Path $XLFile -Table -Autofit -WorksheetName $InputObject.Name
    }

    End {
        Write-Host ("Export Completed")
    }
}

<#
.SYNOPSIS
Sets a message in a WPF String control using DispatcherTimer to Set Message Box via WPF String control.
 
.DESCRIPTION
This function sets a message in a WPF String control and allows for various options such as clearing the message, adding new lines before or after, and avoiding new lines.
 
Requires Defining an XAML: Element named Messages which gets updated by a DispatchTimer
    <Window.Resources>
        <system:String x:Key="Messages">Messages:</system:String>
    <Window.Resources>
 
    <ScrollViewer x:Name="_svMessages" Padding="0,0,0,0" ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
        <TextBox x:Name="_tbxMessages" Margin="5,2,5,5" Padding="0,0,0,0"
            Text="{DynamicResource ResourceKey=Messages}"
            ScrollViewer.CanContentScroll="False"
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
            VerticalContentAlignment="Top"
            TextWrapping="WrapWithOverflow" AcceptsReturn="True" Cursor="Arrow"
        />
    </ScrollViewer>
 
.PARAMETER Message
The message to set in the WPF String control.
 
.PARAMETER Clear
Clears the current message.
 
.PARAMETER NoNewline
Specifies not to add a newline after the message.
 
.PARAMETER NewlineBefore
Adds a newline before the message.
 
.PARAMETER NewlineAfter
Adds a newline after the message.
 
.EXAMPLE
Set-Message -Resources $syncHash.Form.Resources -Message 'This is a test message'
Set-Message -Resources $syncHash.Form.Resources -Message 'This is a test message' -Clear
 
.NOTES
Author: Brooks Vaughn
Date: "22 February 2025"
#>


Function Set-Message {
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Object]$Resources = $SyncHash.Form.Resources,
        [string]$Message = [string]::Empty,
        [switch]$Clear,
        [switch]$NoNewline,
        [switch]$NewlineBefore,
        [switch]$NewlineAfter
    )

    If ($PSCmdlet.ShouldProcess("Setting message")) {
        If ($Clear) {
            $Message = [string]::Empty
            $Resources["Messages"] = [string]::Empty
        }
    }

    If ($NewlineBefore) {
        $Resources["Messages"] += [System.Environment]::NewLine
    }

    If (-not [string]::IsNullOrEmpty($Message)) {
        If ($NoNewline) {
            $Resources["Messages"] += $Message
        } Else {
            $Resources["Messages"] += $Message + [System.Environment]::NewLine
        }
        Write-Host $Message -NoNewline:$NoNewline
    }

    If ($NewlineAfter) {
        $Resources["Messages"] += [System.Environment]::NewLine
    }
}