private/Select-ObjectFromGrid.ps1

function Select-ObjectFromGrid {
    <#
    .SYNOPSIS
        Displays a WPF-based data grid for selecting objects from a collection.
 
    .DESCRIPTION
        The Select-ObjectFromGrid function presents a XAML-based WPF window containing
        a DataGrid that displays objects from an input collection. Users can select
        one or more items using checkboxes and confirm their selection with OK or
        cancel the operation with Cancel.
 
        This function is similar to Out-GridView but provides additional features
        such as Select All/Deselect All buttons and minimum/maximum selection
        validation.
 
        With the ViewOnly switch the checkbox column and the selection controls are
        hidden, which turns the window into a read-only viewer. This is a usable
        alternative to Out-GridView on hosts where Out-GridView is unavailable, such
        as PowerShell 7 without the Microsoft.PowerShell.ConsoleGuiTools module, or
        Windows installations without the ISE components.
 
    .PARAMETER InputObject
        The collection of objects to display in the grid.
        Accepts pipeline input.
 
    .PARAMETER Title
        The title to display in the window title bar.
        Defaults to "Select Items".
 
    .PARAMETER Description
        The subtitle shown under the header inside the window.
        Defaults to a text matching the current mode.
 
    .PARAMETER ColumnsToShow
        An optional array of property names to display as columns.
        If not specified, all properties from the first object are displayed.
 
    .PARAMETER DetailsProperty
        The name of a property whose (usually long or multi-line) text is shown in an
        expandable details panel underneath the row instead of, or in addition to, a
        column. The panel appears when a row is selected. Excluded from the visible
        columns unless the same name is also explicitly listed in -ColumnsToShow.
        Its text is always searched by the global filter box.
 
    .PARAMETER MinSelection
        The minimum number of items that must be selected before OK can be clicked.
        Defaults to 0 (no minimum). Ignored when ViewOnly is used.
 
    .PARAMETER MaxSelection
        The maximum number of items that can be selected before OK can be clicked.
        Defaults to 0 (no maximum / unlimited). Ignored when ViewOnly is used.
 
        Note that this validates rather than blocks: the user can tick more items than
        MaxSelection, but OK stays disabled until the count is back within the limit.
 
    .PARAMETER ViewOnly
        Hides the checkbox column, the Select All / Deselect All buttons and the Cancel
        button, and turns the window into a read-only viewer with a single Close button.
        No objects are returned in this mode.
 
        NoSelection is an alias for this parameter.
 
    .EXAMPLE
        $selected = Get-Process | Select-Object -Property Name, Id, CPU | Select-ObjectFromGrid
 
        Displays all processes in a grid and returns the selected items.
 
    .EXAMPLE
        $users = Get-ADUser -Filter * -Properties DisplayName, EmailAddress
        $selected = $users | Select-ObjectFromGrid -Title "Select Users" -ColumnsToShow DisplayName, EmailAddress
 
        Displays AD users with only DisplayName and EmailAddress columns.
 
    .EXAMPLE
        $items | Select-ObjectFromGrid -MinSelection 1 -MaxSelection 5
 
        Requires at least 1 selection and allows maximum 5 selections.
 
    .EXAMPLE
        $entries | Select-ObjectFromGrid -Title "Log" -ColumnsToShow Timestamp, Message -DetailsProperty Details -ViewOnly
 
        Shows Timestamp and Message as columns; selecting a row expands a panel
        showing the entry's Details text underneath it.
 
    .EXAMPLE
        Get-Service | Select-Object -Property Name, Status, StartType | Select-ObjectFromGrid -Title "Services" -ViewOnly
 
        Displays the services in a read-only viewer without checkboxes, as an
        alternative to Out-GridView. Nothing is returned.
 
    .OUTPUTS
        PSCustomObject[]
        Returns an array of the selected input objects, or $null if cancelled.
        Returns nothing when ViewOnly is used.
 
    .NOTES
        Function : Select-ObjectFromGrid
        Author : John Billekens
        Copyright : Copyright (c) John Billekens Consultancy
        Version : 2026.720.1245
        Requires : Windows PowerShell 5.1 or PowerShell 7+ with Windows Desktop support
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [PSObject[]]$InputObject,

        [Parameter(Mandatory = $false)]
        [string]$Title = "Select Items",

        [Parameter(Mandatory = $false)]
        [string]$Description,

        [Parameter(Mandatory = $false)]
        [string[]]$ColumnsToShow,

        [Parameter(Mandatory = $false)]
        [string]$DetailsProperty,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$MinSelection = 0,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$MaxSelection = 0,

        [Parameter(Mandatory = $false)]
        [Alias('NoSelection')]
        [switch]$ViewOnly
    )

    begin {
        $allItems = [System.Collections.Generic.List[object]]::new()
    }

    process {
        foreach ($item in $InputObject) {
            $allItems.Add($item)
        }
    }

    end {
        if ($allItems.Count -eq 0) {
            Write-Warning "No items to display."
            return $null
        }

        if ($MinSelection -gt 0 -and $MaxSelection -gt 0 -and $MinSelection -gt $MaxSelection) {
            throw "MinSelection ($MinSelection) cannot be greater than MaxSelection ($MaxSelection)."
        }

        # ShowDialog() requires STA; MTA throws unhandled on the dispatcher and kills the process.
        if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne [System.Threading.ApartmentState]::STA) {
            throw "Select-ObjectFromGrid requires a single-threaded apartment (STA) thread to show its window. " + `
                "Restart PowerShell with 'powershell.exe -STA' or 'pwsh -STA', or run this from a host that already uses STA (e.g. the default Windows PowerShell 5.1 console)."
        }

        # Load WPF assemblies
        Add-Type -AssemblyName PresentationFramework
        Add-Type -AssemblyName PresentationCore
        Add-Type -AssemblyName WindowsBase

        # Determine columns to display
        $firstItem = $allItems[0]
        if ($ColumnsToShow -and $ColumnsToShow.Count -gt 0) {
            $columns = @($ColumnsToShow)
        } else {
            $columns = @($firstItem.PSObject.Properties.Name)
        }

        # Details property gets its own panel unless the caller explicitly kept it in ColumnsToShow.
        if ($DetailsProperty -and -not ($ColumnsToShow -and $ColumnsToShow -contains $DetailsProperty)) {
            $columns = @($columns | Where-Object { $_ -ne $DetailsProperty })
        }

        if ($columns.Count -eq 0) {
            Write-Warning "No columns to display."
            return $null
        }

        if (-not $PSBoundParameters.ContainsKey('Description')) {
            $Description = if ($ViewOnly) {
                "Review the items below and close the window when you are done."
            } else {
                "Manage your selection and confirm your choice."
            }
        }

        # Column names are built in code, not via XAML string interpolation.
        $escapeXml = {
            param([string]$Text)
            [System.Security.SecurityElement]::Escape($Text)
        }

        # The checkbox column and the selection controls only exist in selection mode.
        if ($ViewOnly) {
            $checkBoxColumnXaml = ""
            $selectionPanelVisibility = "Collapsed"
            $cancelButtonVisibility = "Collapsed"
            $okButtonText = "Close"
            $headerText = & $escapeXml $Title
        } else {
            $checkBoxColumnXaml = @"
                        <DataGridTemplateColumn Header="" Width="50" CanUserResize="False">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <CheckBox Name="chkSelect"
                                             IsChecked="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                             HorizontalAlignment="Center"
                                             VerticalAlignment="Center"
                                             Margin="4,0"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
"@

            $selectionPanelVisibility = "Visible"
            $cancelButtonVisibility = "Visible"
            $okButtonText = "OK"
            $headerText = & $escapeXml $Title
        }

        $titleXml = & $escapeXml $Title
        $descriptionXml = & $escapeXml $Description

        # Create the XAML for the window
        $xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="$titleXml"
       Width="1440"
       Height="900"
       WindowStartupLocation="CenterScreen"
       ResizeMode="CanResize"
       MinWidth="600"
       MinHeight="400">
    <Window.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
            <GradientStop Color="#0F172A" Offset="0"/>
            <GradientStop Color="#1E293B" Offset="0.6"/>
            <GradientStop Color="#111827" Offset="1"/>
        </LinearGradientBrush>
    </Window.Background>
 
    <Window.Resources>
        <!-- Palette -->
        <Color x:Key="AccentColor">#3B82F6</Color>
        <Color x:Key="CardColor">#0B1221</Color>
        <SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
        <SolidColorBrush x:Key="AccentBrushLight" Color="#1F6FEB"/>
        <SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}"/>
        <SolidColorBrush x:Key="CardBorderBrush" Color="#24324D"/>
        <SolidColorBrush x:Key="TextBrush" Color="#F8FAFC"/>
        <SolidColorBrush x:Key="TextSubtleBrush" Color="#CBD5E1"/>
        <SolidColorBrush x:Key="BorderBrushLight" Color="#1F2937"/>
 
        <!-- Buttons -->
        <Style TargetType="Button">
            <Setter Property="Foreground" Value="#FFFFFF"/>
            <Setter Property="Background" Value="{StaticResource AccentBrush}"/>
            <Setter Property="Padding" Value="18,11"/>
            <Setter Property="MinWidth" Value="120"/>
            <Setter Property="MinHeight" Value="38"/>
            <Setter Property="Margin" Value="6,0,0,0"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="FontWeight" Value="SemiBold"/>
            <Setter Property="FontSize" Value="13"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border x:Name="ButtonBg"
                               Background="{TemplateBinding Background}"
                               BorderBrush="Transparent"
                               BorderThickness="1"
                               CornerRadius="8">
                            <ContentPresenter HorizontalAlignment="Center"
                                             VerticalAlignment="Center"
                                             RecognizesAccessKey="True"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="#2F6FE3"/>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter Property="Background" Value="#2257B8"/>
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="True">
                                <Setter TargetName="ButtonBg" Property="BorderBrush" Value="#93C5FD"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="Background" Value="#232E45"/>
                                <Setter Property="Foreground" Value="#5B6B85"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <!-- Secondary (ghost) button -->
        <Style x:Key="GhostButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
            <Setter Property="Background" Value="#182033"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Setter Property="BorderBrush" Value="#2D3B54"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Border x:Name="GhostBg"
                               Background="{TemplateBinding Background}"
                               BorderBrush="{TemplateBinding BorderBrush}"
                               BorderThickness="{TemplateBinding BorderThickness}"
                               CornerRadius="8">
                            <ContentPresenter HorizontalAlignment="Center"
                                             VerticalAlignment="Center"
                                             RecognizesAccessKey="True"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="#22304C"/>
                                <Setter TargetName="GhostBg" Property="BorderBrush" Value="#3A4C6E"/>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">
                                <Setter Property="Background" Value="#151D31"/>
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="True">
                                <Setter TargetName="GhostBg" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <!-- DataGrid -->
        <Style TargetType="DataGrid">
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="RowBackground" Value="#0F172A"/>
            <Setter Property="AlternatingRowBackground" Value="#111B2F"/>
            <Setter Property="Background" Value="#0B1221"/>
            <Setter Property="GridLinesVisibility" Value="None"/>
            <Setter Property="RowHeaderWidth" Value="0"/>
            <Setter Property="HeadersVisibility" Value="Column"/>
            <Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BorderBrushLight}"/>
            <Setter Property="VerticalGridLinesBrush" Value="{StaticResource BorderBrushLight}"/>
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Margin" Value="0"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
        </Style>
 
        <Style TargetType="DataGridColumnHeader">
            <Setter Property="Background" Value="#142038"/>
            <Setter Property="Foreground" Value="{StaticResource TextSubtleBrush}"/>
            <Setter Property="FontWeight" Value="SemiBold"/>
            <Setter Property="BorderBrush" Value="{StaticResource BorderBrushLight}"/>
            <Setter Property="BorderThickness" Value="0,0,0,1"/>
            <Setter Property="Padding" Value="12,10"/>
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="Cursor" Value="Hand"/>
        </Style>
 
        <!-- Per-column filter TextBox, rounded to match the global filter box -->
        <Style x:Key="ColumnFilterBox" TargetType="TextBox">
            <Setter Property="Background" Value="#0F172A"/>
            <Setter Property="BorderBrush" Value="#2D3B54"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Setter Property="CaretBrush" Value="{StaticResource TextBrush}"/>
            <Setter Property="FontSize" Value="11"/>
            <Setter Property="FontWeight" Value="Normal"/>
            <Setter Property="Padding" Value="8,4"/>
            <Setter Property="Margin" Value="0,6,0,0"/>
            <Setter Property="MinWidth" Value="80"/>
            <Setter Property="Cursor" Value="IBeam"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="TextBox">
                        <Border x:Name="FilterBorder"
                               Background="{TemplateBinding Background}"
                               BorderBrush="{TemplateBinding BorderBrush}"
                               BorderThickness="{TemplateBinding BorderThickness}"
                               CornerRadius="6">
                            <Grid>
                                <TextBlock x:Name="FilterHint"
                                          Text="Filter"
                                          Foreground="#4B5A75"
                                          Margin="{TemplateBinding Padding}"
                                          VerticalAlignment="Center"
                                          IsHitTestVisible="False"
                                          Visibility="Collapsed"/>
                                <ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsKeyboardFocused" Value="True">
                                <Setter TargetName="FilterBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
                            </Trigger>
                            <Trigger Property="Text" Value="">
                                <Setter TargetName="FilterHint" Property="Visibility" Value="Visible"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <!-- Thin dark scrollbars -->
        <Style x:Key="GridThumb" TargetType="Thumb">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Thumb">
                        <Border x:Name="ThumbBorder" Background="#3B4E75" CornerRadius="5"/>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="ThumbBorder" Property="Background" Value="#4A5F8C"/>
                            </Trigger>
                            <Trigger Property="IsDragging" Value="True">
                                <Setter TargetName="ThumbBorder" Property="Background" Value="#5A6F9E"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style x:Key="GridRepeatButton" TargetType="RepeatButton">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="RepeatButton">
                        <Rectangle Fill="Transparent"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style TargetType="ScrollBar">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Width" Value="12"/>
            <Setter Property="MinWidth" Value="12"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ScrollBar">
                        <Grid Background="Transparent">
                            <Border Background="#131D33" CornerRadius="5" Margin="2,0"/>
                            <Track Name="PART_Track" IsDirectionReversed="True">
                                <Track.DecreaseRepeatButton>
                                    <RepeatButton Style="{StaticResource GridRepeatButton}" Command="ScrollBar.PageUpCommand"/>
                                </Track.DecreaseRepeatButton>
                                <Track.IncreaseRepeatButton>
                                    <RepeatButton Style="{StaticResource GridRepeatButton}" Command="ScrollBar.PageDownCommand"/>
                                </Track.IncreaseRepeatButton>
                                <Track.Thumb>
                                    <Thumb Style="{StaticResource GridThumb}" Width="8" Margin="2,0"/>
                                </Track.Thumb>
                            </Track>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Orientation" Value="Horizontal">
                    <Setter Property="Height" Value="12"/>
                    <Setter Property="MinHeight" Value="12"/>
                    <Setter Property="Width" Value="Auto"/>
                    <Setter Property="MinWidth" Value="0"/>
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ScrollBar">
                                <Grid Background="Transparent">
                                    <Border Background="#131D33" CornerRadius="5" Margin="0,2"/>
                                    <Track Name="PART_Track" IsDirectionReversed="False">
                                        <Track.DecreaseRepeatButton>
                                            <RepeatButton Style="{StaticResource GridRepeatButton}" Command="ScrollBar.PageLeftCommand"/>
                                        </Track.DecreaseRepeatButton>
                                        <Track.IncreaseRepeatButton>
                                            <RepeatButton Style="{StaticResource GridRepeatButton}" Command="ScrollBar.PageRightCommand"/>
                                        </Track.IncreaseRepeatButton>
                                        <Track.Thumb>
                                            <Thumb Style="{StaticResource GridThumb}" Height="8" Margin="0,2"/>
                                        </Track.Thumb>
                                    </Track>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="DataGridRow">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Margin" Value="0,0,0,2"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Background" Value="#16233C"/>
                </Trigger>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background" Value="#1E3A8A"/>
                    <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
                </Trigger>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsSelected" Value="True"/>
                        <Condition Property="IsKeyboardFocusWithin" Value="False"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="Background" Value="#243451"/>
                    <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
                </MultiTrigger>
                <!-- Right-click gives the row keyboard focus without selecting it; default
                     WPF chrome for a focused-but-unselected row is a light system color. -->
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="Background" Value="Transparent"/>
                    <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="DataGridCell">
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="Padding" Value="14,12"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
            <Style.Triggers>
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="BorderThickness" Value="1"/>
                    <Setter Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
                    <Setter Property="Background" Value="Transparent"/>
                    <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="CheckBox">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Setter Property="BorderBrush" Value="#9CA3AF"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="CheckBox">
                        <Grid VerticalAlignment="Center" HorizontalAlignment="Center">
                            <Border x:Name="Border" Width="16" Height="16" CornerRadius="3" BorderThickness="1" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"/>
                            <Path x:Name="CheckMark" Data="M2,6 L6,10 L14,2" StrokeThickness="2" Stroke="#F8FAFC" StrokeEndLineCap="Round" StrokeStartLineCap="Round" StrokeLineJoin="Round" Visibility="Collapsed"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="Border" Property="Background" Value="#3B82F6"/>
                                <Setter TargetName="Border" Property="BorderBrush" Value="#3B82F6"/>
                                <Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Border" Property="BorderBrush" Value="#5B8EF6"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter TargetName="Border" Property="Opacity" Value="0.5"/>
                                <Setter TargetName="CheckMark" Property="Opacity" Value="0.5"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style x:Key="ContextMenuItemDarkStyle" TargetType="MenuItem">
            <Setter Property="Foreground" Value="{StaticResource TextBrush}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Padding" Value="10,6"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="MenuItem">
                        <Border x:Name="ItemBorder" Background="{TemplateBinding Background}" CornerRadius="5">
                            <ContentPresenter ContentSource="Header" Margin="{TemplateBinding Padding}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsHighlighted" Value="True">
                                <Setter TargetName="ItemBorder" Property="Background" Value="{StaticResource AccentBrush}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style x:Key="ContextMenuDarkStyle" TargetType="ContextMenu">
            <Setter Property="Background" Value="{StaticResource CardBrush}"/>
            <Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Padding" Value="4"/>
            <Setter Property="HasDropShadow" Value="False"/>
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="ItemContainerStyle" Value="{StaticResource ContextMenuItemDarkStyle}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ContextMenu">
                        <Border Background="{StaticResource CardBrush}"
                               BorderBrush="{TemplateBinding BorderBrush}"
                               BorderThickness="{TemplateBinding BorderThickness}"
                               CornerRadius="8"
                               SnapsToDevicePixels="True">
                            <Border.Effect>
                                <DropShadowEffect Color="#33000000" BlurRadius="14" ShadowDepth="0" Opacity="0.35"/>
                            </Border.Effect>
                            <StackPanel IsItemsHost="True" Margin="{TemplateBinding Padding}"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
 
    <Grid Margin="24">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
 
        <!-- Header -->
        <StackPanel Grid.Row="0" Margin="0,0,0,16">
            <TextBlock Text="$headerText"
                      FontSize="24"
                      FontWeight="Bold"
                      Foreground="{StaticResource TextBrush}"/>
            <TextBlock Text="$descriptionXml"
                      FontSize="13"
                      Foreground="{StaticResource TextSubtleBrush}"
                      Margin="0,4,0,0"/>
        </StackPanel>
 
        <!-- Card -->
        <Border Grid.Row="1"
               Background="{StaticResource CardBrush}"
               CornerRadius="12"
               Padding="20"
               BorderBrush="{StaticResource CardBorderBrush}"
               BorderThickness="1">
            <Border.Effect>
                <DropShadowEffect Color="#33000000" BlurRadius="18" ShadowDepth="0" Opacity="0.28"/>
            </Border.Effect>
 
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
 
                <!-- Filter and selection actions -->
                <Grid Grid.Row="0" Margin="0,0,0,12">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
 
                    <StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Left">
                        <Border Name="borderFilter"
                               Background="#0F172A"
                               BorderBrush="#2D3B54"
                               BorderThickness="1"
                               CornerRadius="8"
                               Padding="10,6"
                               MinWidth="280">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Grid Grid.Column="0" Width="15" Height="15" Margin="2,0,8,0" VerticalAlignment="Center">
                                    <Ellipse Width="10" Height="10"
                                            Stroke="#64748B"
                                            StrokeThickness="1.6"
                                            HorizontalAlignment="Left"
                                            VerticalAlignment="Top"/>
                                    <Line X1="9.5" Y1="9.5" X2="13.5" Y2="13.5"
                                         Stroke="#64748B"
                                         StrokeThickness="1.6"
                                         StrokeEndLineCap="Round"/>
                                </Grid>
                                <TextBlock Grid.Column="1"
                                          Name="txtFilterHint"
                                          Text="Search all columns..."
                                          Foreground="#64748B"
                                          VerticalAlignment="Center"
                                          IsHitTestVisible="False"
                                          Margin="2,0,0,0"/>
                                <TextBox Grid.Column="1"
                                        Name="txtFilter"
                                        Background="Transparent"
                                        BorderThickness="0"
                                        Foreground="{StaticResource TextBrush}"
                                        CaretBrush="{StaticResource TextBrush}"
                                        VerticalAlignment="Center"/>
                            </Grid>
                        </Border>
                        <Button Name="btnClearFilters"
                               Content="Clear filters"
                               Style="{StaticResource GhostButton}"/>
                    </StackPanel>
 
                    <StackPanel Grid.Column="1" Orientation="Horizontal">
                        <StackPanel Name="pnlSelectionActions"
                                   Orientation="Horizontal"
                                   Visibility="$selectionPanelVisibility">
                            <Button Name="btnSelectAll" Content="Select All" Style="{StaticResource GhostButton}" Margin="0,0,10,0"/>
                            <Button Name="btnDeselectAll" Content="Deselect All" Style="{StaticResource GhostButton}"/>
                        </StackPanel>
                        <TextBlock Name="txtSelectionInfo"
                                  VerticalAlignment="Center"
                                  Margin="16,0,0,0"
                                  FontSize="12"
                                  Foreground="{StaticResource TextSubtleBrush}"/>
                    </StackPanel>
                </Grid>
 
                <!-- Data Grid -->
                <DataGrid Grid.Row="1"
                         Name="dataGrid"
                         AutoGenerateColumns="False"
                         CanUserAddRows="False"
                         CanUserDeleteRows="False"
                         CanUserSortColumns="False"
                         SelectionMode="Extended"
                         SelectionUnit="FullRow"
                         IsReadOnly="True"
                         AlternationCount="2"
                         EnableRowVirtualization="True"
                         EnableColumnVirtualization="False"
                         VirtualizingPanel.IsVirtualizing="True"
                         VirtualizingPanel.VirtualizationMode="Recycling"
                         VirtualizingPanel.ScrollUnit="Pixel"
                         VerticalScrollBarVisibility="Auto"
                         HorizontalScrollBarVisibility="Auto">
                    <DataGrid.RowDetailsTemplate>
                        <DataTemplate>
                            <Border x:Name="DetailsBorder"
                                   Background="#0D1526"
                                   BorderBrush="#24324D"
                                   BorderThickness="0,1,0,1"
                                   Padding="18,10">
                                <TextBox Text="{Binding _DetailsText, Mode=OneWay}"
                                        IsReadOnly="True"
                                        BorderThickness="0"
                                        Background="Transparent"
                                        Foreground="#CBD5E1"
                                        FontFamily="Consolas"
                                        FontSize="12"
                                        TextWrapping="Wrap"/>
                            </Border>
                            <DataTemplate.Triggers>
                                <DataTrigger Binding="{Binding _DetailsText}" Value="">
                                    <Setter TargetName="DetailsBorder" Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </DataTemplate.Triggers>
                        </DataTemplate>
                    </DataGrid.RowDetailsTemplate>
                    <DataGrid.Columns>
$checkBoxColumnXaml
                    </DataGrid.Columns>
                </DataGrid>
 
                <!-- Status -->
                <TextBlock Grid.Row="2"
                           Name="txtStatus"
                           Margin="0,12,0,0"
                           FontSize="12"
                           Foreground="{StaticResource TextSubtleBrush}"/>
            </Grid>
        </Border>
 
        <!-- Footer buttons -->
        <StackPanel Grid.Row="2"
                    Orientation="Horizontal"
                    HorizontalAlignment="Right"
                    Margin="0,18,0,0">
            <Button Name="btnCancel"
                   Content="Cancel"
                   Style="{StaticResource GhostButton}"
                   IsCancel="True"
                   Margin="0,0,10,0"
                   Visibility="$cancelButtonVisibility"/>
            <Button Name="btnOK" Content="$okButtonText" IsDefault="True"/>
        </StackPanel>
    </Grid>
</Window>
"@


        # Ordered hashtable; Add-Member per property is far slower.
        $wrappedItems = [System.Collections.Generic.List[object]]::new($allItems.Count)
        foreach ($item in $allItems) {
            $properties = [ordered]@{}
            if (-not $ViewOnly) {
                $properties['IsSelected'] = $false
            }
            foreach ($col in $columns) {
                $value = $item.$col
                if ($null -ne $value) {
                    # Unwrap PSObject; WPF's sort comparer can't compare the wrapper.
                    $value = $value.PSObject.BaseObject
                }
                $properties[$col] = $value
            }
            if ($DetailsProperty) {
                $detailsValue = $item.$DetailsProperty
                $properties['_DetailsText'] = if ($null -eq $detailsValue) { '' } else { [string]$detailsValue }
            }
            $properties['_OriginalItem'] = $item
            $wrappedItems.Add([PSCustomObject]$properties)
        }

        # Parse XAML
        try {
            $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
            try {
                $window = [System.Windows.Markup.XamlReader]::Load($reader)
            } finally {
                $reader.Close()
            }
        } catch [System.Windows.Markup.XamlParseException] {
            throw "Failed to build the grid window: $($_.Exception.Message)"
        }

        # Get controls
        $dataGrid = $window.FindName("dataGrid")
        $dataGrid.RowDetailsVisibilityMode = if ($DetailsProperty) {
            [System.Windows.Controls.DataGridRowDetailsVisibilityMode]::VisibleWhenSelected
        } else {
            [System.Windows.Controls.DataGridRowDetailsVisibilityMode]::Collapsed
        }
        $btnSelectAll = $window.FindName("btnSelectAll")
        $btnDeselectAll = $window.FindName("btnDeselectAll")
        $btnOK = $window.FindName("btnOK")
        $btnCancel = $window.FindName("btnCancel")
        $txtSelectionInfo = $window.FindName("txtSelectionInfo")
        $txtStatus = $window.FindName("txtStatus")
        $txtFilter = $window.FindName("txtFilter")
        $txtFilterHint = $window.FindName("txtFilterHint")
        $btnClearFilters = $window.FindName("btnClearFilters")
        $borderFilter = $window.FindName("borderFilter")
        $columnFilterBoxStyle = $window.FindResource("ColumnFilterBox")
        $accentBrush = $window.FindResource("AccentBrush")
        $defaultFilterBorderBrush = $borderFilter.BorderBrush

        # Focus highlight, matches per-column filter box style.
        $txtFilter.Add_GotFocus({
            $borderFilter.BorderBrush = $accentBrush
        }.GetNewClosure())
        $txtFilter.Add_LostFocus({
            $borderFilter.BorderBrush = $defaultFilterBorderBrush
        }.GetNewClosure())

        # Columns built in code; arbitrary property names could break XAML binding paths.
        $cellStyle = [System.Windows.Style]::new([System.Windows.Controls.TextBlock])
        $cellStyle.Setters.Add([System.Windows.Setter]::new(
                [System.Windows.Controls.TextBlock]::VerticalAlignmentProperty,
                [System.Windows.VerticalAlignment]::Center))
        $cellStyle.Setters.Add([System.Windows.Setter]::new(
                [System.Windows.FrameworkElement]::MarginProperty,
                [System.Windows.Thickness]::new(10, 6, 10, 6)))
        $cellStyle.Setters.Add([System.Windows.Setter]::new(
                [System.Windows.Controls.TextBlock]::ForegroundProperty,
                $window.FindResource("TextBrush")))

        # A collection view gives filtering/sorting without touching the backing list.
        $view = [System.Windows.Data.CollectionViewSource]::GetDefaultView($wrappedItems)

        $state = @{
            DialogResult     = $null
            SelectedItems    = @()
            WrappedItems     = $wrappedItems
            Columns          = $columns
            View             = $view
            DataGrid         = $dataGrid
            Window           = $window
            MinSelection     = $MinSelection
            MaxSelection     = $MaxSelection
            ViewOnly         = [bool]$ViewOnly
            TxtSelectionInfo = $txtSelectionInfo
            TxtStatus        = $txtStatus
            TxtFilterHint    = $txtFilterHint
            BtnOK            = $btnOK
            # Tracked incrementally, no full rescan per click.
            SelectedCount    = 0
            ColumnFilters    = @{}
            ColumnFilterBoxes = @{}
            SortArrows       = @{}
            PopupCloseTicks  = @{}
            DetailsKey       = if ($DetailsProperty) { '_DetailsText' } else { $null }
            SortColumn       = $null
            SortDirection    = 0
        }

        $state.UpdateSelection = {
            $selectedCount = $state.SelectedCount
            $totalCount = $state.WrappedItems.Count

            $infoText = "$selectedCount of $totalCount selected"
            if ($state.MinSelection -gt 0 -or $state.MaxSelection -gt 0) {
                $constraints = @()
                if ($state.MinSelection -gt 0) { $constraints += "min: $($state.MinSelection)" }
                if ($state.MaxSelection -gt 0) { $constraints += "max: $($state.MaxSelection)" }
                $infoText += " (" + ($constraints -join ", ") + ")"
            }
            $visibleCount = $state.View.Count
            if ($visibleCount -lt $totalCount) {
                $infoText += " - showing $visibleCount of $totalCount"
            }
            $state.TxtSelectionInfo.Text = $infoText

            $valid = $true
            $statusText = ""

            if ($state.MinSelection -gt 0 -and $selectedCount -lt $state.MinSelection) {
                $valid = $false
                $statusText = "Please select at least $($state.MinSelection) item(s)."
            } elseif ($state.MaxSelection -gt 0 -and $selectedCount -gt $state.MaxSelection) {
                $valid = $false
                $statusText = "Please select no more than $($state.MaxSelection) item(s)."
            }

            $state.TxtStatus.Text = $statusText
            $state.TxtStatus.Foreground = if ($valid) {
                [System.Windows.Media.Brushes]::Gray
            } else {
                [System.Windows.Media.Brushes]::Red
            }
            $state.BtnOK.IsEnabled = $valid
        }.GetNewClosure()

        # ViewOnly status text (no selection counter).
        $state.UpdateFilterStatus = {
            $totalCount = $state.WrappedItems.Count
            $visibleCount = $state.View.Count
            if ($visibleCount -lt $totalCount) {
                $state.TxtSelectionInfo.Text = "showing $visibleCount of $totalCount item(s)"
            } else {
                $state.TxtSelectionInfo.Text = "$totalCount item(s)"
            }
        }.GetNewClosure()

        # Reapplies filter and refreshes status text.
        $state.RefreshFilterState = {
            $anyActive = (-not [string]::IsNullOrEmpty($state.Filter))
            if (-not $anyActive) {
                foreach ($needle in $state.ColumnFilters.Values) {
                    if (-not [string]::IsNullOrEmpty($needle)) { $anyActive = $true; break }
                }
            }

            if ($anyActive) {
                $state.View.Filter = $state.FilterPredicate
            } else {
                $state.View.Filter = $null
            }
            $state.View.Refresh()

            if ($state.ViewOnly) {
                & $state.UpdateFilterStatus
            } else {
                & $state.UpdateSelection
            }
        }.GetNewClosure()

        # 3-state: unsorted -> ascending -> descending -> unsorted. Single column only.
        $state.ApplySort = {
            param([string]$ColumnName)

            if ($state.SortColumn -eq $ColumnName) {
                $state.SortDirection = $state.SortDirection + 1
                if ($state.SortDirection -gt 2) { $state.SortDirection = 0 }
            } else {
                $state.SortColumn = $ColumnName
                $state.SortDirection = 1
            }
            if ($state.SortDirection -eq 0) { $state.SortColumn = $null }

            $state.View.SortDescriptions.Clear()
            if ($state.SortColumn) {
                $direction = if ($state.SortDirection -eq 1) {
                    [System.ComponentModel.ListSortDirection]::Ascending
                } else {
                    [System.ComponentModel.ListSortDirection]::Descending
                }
                try {
                    [void]$state.View.SortDescriptions.Add([System.ComponentModel.SortDescription]::new($state.SortColumn, $direction))
                } catch {
                    # Non-comparable values.
                    $state.View.SortDescriptions.Clear()
                    $state.SortColumn = $null
                    $state.SortDirection = 0
                    $state.TxtStatus.Text = "Column '$ColumnName' contains values that cannot be sorted."
                }
            }

            foreach ($name in $state.SortArrows.Keys) {
                $arrow = $state.SortArrows[$name]
                if ($name -eq $state.SortColumn -and $state.SortDirection -eq 1) {
                    $arrow.Text = [char]0x25B2
                } elseif ($name -eq $state.SortColumn -and $state.SortDirection -eq 2) {
                    $arrow.Text = [char]0x25BC
                } else {
                    $arrow.Text = ""
                }
            }
        }.GetNewClosure()

        $funnelIdleBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Color]::FromRgb(0x64, 0x74, 0x8B))
        $funnelIdleBrush.Freeze()
        $popupBorderBrush = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.Color]::FromRgb(0x2D, 0x3B, 0x54))
        $popupBorderBrush.Freeze()

        # Header: title (sort) + funnel icon (filter popup).
        foreach ($col in $columns) {
            $colName = $col

            $binding = [System.Windows.Data.Binding]::new()
            $binding.Path = [System.Windows.PropertyPath]::new($colName)
            $binding.Mode = [System.Windows.Data.BindingMode]::OneWay

            $column = [System.Windows.Controls.DataGridTextColumn]::new()
            $column.Binding = $binding
            $column.IsReadOnly = $true
            $column.CanUserSort = $false
            # CanUserSort is off; SortMemberPath just carries the column name to the Click handler.
            $column.SortMemberPath = $colName
            $column.Width = [System.Windows.Controls.DataGridLength]::Auto
            $column.ElementStyle = $cellStyle

            # Title + sort arrow.
            $titleBlock = [System.Windows.Controls.TextBlock]::new()
            $titleBlock.Text = $colName
            $titleBlock.VerticalAlignment = [System.Windows.VerticalAlignment]::Center

            $arrowBlock = [System.Windows.Controls.TextBlock]::new()
            $arrowBlock.Text = ""
            $arrowBlock.Margin = [System.Windows.Thickness]::new(4, 0, 0, 0)
            $arrowBlock.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
            $arrowBlock.FontSize = 10
            $arrowBlock.Foreground = $window.FindResource("AccentBrush")
            $state.SortArrows[$colName] = $arrowBlock

            $titleRow = [System.Windows.Controls.StackPanel]::new()
            $titleRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
            [void]$titleRow.Children.Add($titleBlock)
            [void]$titleRow.Children.Add($arrowBlock)

            # Filter popup trigger.
            $funnel = [System.Windows.Shapes.Path]::new()
            $funnel.Data = [System.Windows.Media.Geometry]::Parse("M0,0 H12 L7,5.5 V10.5 L5,9 V5.5 Z")
            $funnel.Fill = $funnelIdleBrush
            $funnel.VerticalAlignment = [System.Windows.VerticalAlignment]::Center

            $funnelHost = [System.Windows.Controls.Border]::new()
            $funnelHost.Background = [System.Windows.Media.Brushes]::Transparent
            $funnelHost.Padding = [System.Windows.Thickness]::new(5, 3, 5, 3)
            $funnelHost.Margin = [System.Windows.Thickness]::new(2, 0, 0, 0)
            $funnelHost.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
            $funnelHost.Cursor = [System.Windows.Input.Cursors]::Hand
            $funnelHost.ToolTip = "Filter this column"
            $funnelHost.Child = $funnel

            $filterBox = [System.Windows.Controls.TextBox]::new()
            $filterBox.Style = $columnFilterBoxStyle
            $filterBox.Width = 180
            $filterBox.Margin = [System.Windows.Thickness]::new(0)
            $state.ColumnFilters[$colName] = $null
            $state.ColumnFilterBoxes[$colName] = $filterBox
            $state.PopupCloseTicks[$colName] = 0L

            $popupBorder = [System.Windows.Controls.Border]::new()
            $popupBorder.Background = $window.FindResource("CardBrush")
            $popupBorder.BorderBrush = $popupBorderBrush
            $popupBorder.BorderThickness = [System.Windows.Thickness]::new(1)
            $popupBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
            $popupBorder.Padding = [System.Windows.Thickness]::new(10)
            $popupBorder.Child = $filterBox

            $popup = [System.Windows.Controls.Primitives.Popup]::new()
            $popup.AllowsTransparency = $true
            $popup.StaysOpen = $false
            $popup.Placement = [System.Windows.Controls.Primitives.PlacementMode]::Bottom
            $popup.PlacementTarget = $funnelHost
            $popup.VerticalOffset = 4
            $popup.Child = $popupBorder

            $popup.Add_Closed({
                $state.PopupCloseTicks[$colName] = [DateTime]::UtcNow.Ticks
            }.GetNewClosure())

            # Header captures mouse on down; only Preview reaches here. Handled=true blocks sort.
            # Time guard stops StaysOpen=False from reopening on the same click.
            $funnelHost.Add_PreviewMouseLeftButtonDown({
                $_.Handled = $true
                if ($popup.IsOpen) {
                    $popup.IsOpen = $false
                } elseif (([DateTime]::UtcNow.Ticks - $state.PopupCloseTicks[$colName]) -gt 3000000) {
                    $popup.IsOpen = $true
                    [void]$state.Window.Dispatcher.BeginInvoke(
                        [System.Windows.Threading.DispatcherPriority]::Input,
                        [System.Action]{ [void]$filterBox.Focus() }.GetNewClosure())
                }
            }.GetNewClosure())

            $filterBox.Add_KeyDown({
                if ($_.Key -eq [System.Windows.Input.Key]::Escape -or $_.Key -eq [System.Windows.Input.Key]::Enter) {
                    $popup.IsOpen = $false
                }
            }.GetNewClosure())

            $filterBox.Add_TextChanged({
                $text = $filterBox.Text
                if ([string]::IsNullOrWhiteSpace($text)) {
                    $state.ColumnFilters[$colName] = $null
                    $funnel.Fill = $funnelIdleBrush
                } else {
                    $state.ColumnFilters[$colName] = $text
                    $funnel.Fill = $accentBrush
                }
                & $state.RefreshFilterState
            }.GetNewClosure())

            $headerPanel = [System.Windows.Controls.StackPanel]::new()
            $headerPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
            [void]$headerPanel.Children.Add($titleRow)
            [void]$headerPanel.Children.Add($funnelHost)
            [void]$headerPanel.Children.Add($popup)

            $column.Header = $headerPanel

            $dataGrid.Columns.Add($column)
        }

        $btnClearFilters.Add_Click({
            $txtFilter.Text = ""
            foreach ($box in $state.ColumnFilterBoxes.Values) {
                $box.Text = ""
            }
        }.GetNewClosure())

        # Header captures mouse; child MouseUp handlers never fire. Use header's own Click.
        $dataGrid.AddHandler(
            [System.Windows.Controls.Primitives.DataGridColumnHeader]::ClickEvent,
            [System.Windows.RoutedEventHandler] {
                $header = $_.OriginalSource
                if ($header -is [System.Windows.Controls.Primitives.DataGridColumnHeader] -and
                    $null -ne $header.Column -and
                    -not [string]::IsNullOrEmpty($header.Column.SortMemberPath)) {
                    & $state.ApplySort $header.Column.SortMemberPath
                }
            }.GetNewClosure()
        )

        # Ctrl+C copies the focused cell; context menu adds explicit Copy Cell / Copy Row.
        $copyCellMenuItem = [System.Windows.Controls.MenuItem]::new()
        $copyCellMenuItem.Header = "Copy Cell"
        $copyRowMenuItem = [System.Windows.Controls.MenuItem]::new()
        $copyRowMenuItem.Header = "Copy Row"
        $gridContextMenu = [System.Windows.Controls.ContextMenu]::new()
        # Code-created ContextMenu is reparented under a Popup adorner; implicit
        # Window.Resources styling by TargetType does not reliably apply there.
        $gridContextMenu.Style = $window.FindResource("ContextMenuDarkStyle")
        [void]$gridContextMenu.Items.Add($copyCellMenuItem)
        [void]$gridContextMenu.Items.Add($copyRowMenuItem)
        $dataGrid.ContextMenu = $gridContextMenu

        # Right-click must not change row selection (it would toggle the details panel).
        # Move CurrentCell to the cell under the pointer so Copy Cell/Row still target it.
        $dataGrid.AddHandler(
            [System.Windows.Controls.DataGridCell]::PreviewMouseRightButtonDownEvent,
            [System.Windows.Input.MouseButtonEventHandler] {
                $cellElement = $_.OriginalSource
                while ($cellElement -and -not ($cellElement -is [System.Windows.Controls.DataGridCell])) {
                    $cellElement = [System.Windows.Media.VisualTreeHelper]::GetParent($cellElement)
                }
                if ($cellElement -and $cellElement.Column) {
                    $state.DataGrid.CurrentCell = [System.Windows.Controls.DataGridCellInfo]::new($cellElement.DataContext, $cellElement.Column)
                }
                $_.Handled = $true
            }.GetNewClosure()
        )

        $state.CopyCurrentCell = {
            $cell = $state.DataGrid.CurrentCell
            if ($null -eq $cell.Column -or $null -eq $cell.Item -or [string]::IsNullOrEmpty($cell.Column.SortMemberPath)) { return }
            $value = $cell.Item.($cell.Column.SortMemberPath)
            [System.Windows.Clipboard]::SetText($(if ($null -eq $value) { '' } else { $value.ToString() }))
        }.GetNewClosure()

        $state.CopyCurrentRow = {
            $item = $state.DataGrid.CurrentCell.Item
            if ($null -eq $item) { $item = $state.DataGrid.SelectedItem }
            if ($null -eq $item) { return }
            $values = foreach ($col in $state.Columns) { $item.$col }
            [System.Windows.Clipboard]::SetText($values -join "`t")
        }.GetNewClosure()

        $copyCellMenuItem.Add_Click({ & $state.CopyCurrentCell }.GetNewClosure())
        $copyRowMenuItem.Add_Click({ & $state.CopyCurrentRow }.GetNewClosure())

        $dataGrid.Add_PreviewKeyDown({
            if ($_.Key -eq [System.Windows.Input.Key]::C -and
                ($_.KeyboardDevice.Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) {
                & $state.CopyCurrentCell
                $_.Handled = $true
            }
        }.GetNewClosure())

        $dataGrid.ItemsSource = $view

        # Case-insensitive substring. Global: any column. Per-column: AND, own column only.
        $state.Filter = $null
        $state.FilterPredicate = [Predicate[object]] {
            $row = $args[0]

            $needle = $state.Filter
            if (-not [string]::IsNullOrEmpty($needle)) {
                $globalMatch = $false
                foreach ($col in $state.Columns) {
                    $value = $row.$col
                    if ($null -ne $value -and $value.ToString().IndexOf($needle, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
                        $globalMatch = $true
                        break
                    }
                }
                if (-not $globalMatch -and $state.DetailsKey) {
                    $value = $row.($state.DetailsKey)
                    if ($null -ne $value -and $value.ToString().IndexOf($needle, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
                        $globalMatch = $true
                    }
                }
                if (-not $globalMatch) { return $false }
            }

            foreach ($col in $state.Columns) {
                $colNeedle = $state.ColumnFilters[$col]
                if ([string]::IsNullOrEmpty($colNeedle)) { continue }
                $value = $row.$col
                if ($null -eq $value -or $value.ToString().IndexOf($colNeedle, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) {
                    return $false
                }
            }

            return $true
        }.GetNewClosure()

        $txtFilter.Add_TextChanged({
            $text = $txtFilter.Text
            $state.TxtFilterHint.Visibility = if ([string]::IsNullOrEmpty($text)) {
                [System.Windows.Visibility]::Visible
            } else {
                [System.Windows.Visibility]::Collapsed
            }

            if ([string]::IsNullOrWhiteSpace($text)) {
                $state.Filter = $null
            } else {
                $state.Filter = $text
            }
            & $state.RefreshFilterState
        }.GetNewClosure())

        if (-not $ViewOnly) {
            & $state.UpdateSelection

            # Visible rows only; count updates via Checked/Unchecked handlers.
            $state.SetAllVisible = {
                param([bool]$Value)
                foreach ($item in $state.View) {
                    $item.IsSelected = $Value
                }
                $state.DataGrid.Items.Refresh()
            }.GetNewClosure()

            $btnSelectAll.Add_Click({
                & $state.SetAllVisible $true
            }.GetNewClosure())

            $btnDeselectAll.Add_Click({
                & $state.SetAllVisible $false
            }.GetNewClosure())

            # O(1) count update per checkbox toggle.
            $dataGrid.AddHandler(
                [System.Windows.Controls.Primitives.ToggleButton]::CheckedEvent,
                [System.Windows.RoutedEventHandler] {
                    $state.SelectedCount++
                    & $state.UpdateSelection
                }.GetNewClosure()
            )

            $dataGrid.AddHandler(
                [System.Windows.Controls.Primitives.ToggleButton]::UncheckedEvent,
                [System.Windows.RoutedEventHandler] {
                    $state.SelectedCount--
                    & $state.UpdateSelection
                }.GetNewClosure()
            )

            $btnOK.Add_Click({
                $selected = [System.Collections.Generic.List[object]]::new()
                foreach ($item in $state.WrappedItems) {
                    if ($item.IsSelected) {
                        $selected.Add($item._OriginalItem)
                    }
                }
                $state.DialogResult = $true
                $state.SelectedItems = $selected.ToArray()
                $state.Window.Close()
            }.GetNewClosure())

            $btnCancel.Add_Click({
                $state.DialogResult = $false
                $state.Window.Close()
            }.GetNewClosure())
        } else {
            $txtSelectionInfo.Text = "$($wrappedItems.Count) item(s)"

            $btnOK.Add_Click({
                $state.Window.Close()
            }.GetNewClosure())
        }

        [void]$window.ShowDialog()

        if ($ViewOnly) {
            return
        }

        if ($state.DialogResult -eq $true) {
            return $state.SelectedItems
        } else {
            return $null
        }
    }
}

# SIG # Begin signature block
# MIInigYJKoZIhvcNAQcCoIInezCCJ3cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBnpsgO96QI7yZF
# TeNlFBl5Ku/Fq6eS8p5HeZOmzyz1M6CCIR0wggZFMIIELaADAgECAhAIMk+dt9qR
# b2Pk8qM8Xl1RMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQK
# ExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2Rl
# IFNpZ25pbmcgMjAyMSBDQTAeFw0yNDA0MDQxNDA0MjRaFw0yNzA0MDQxNDA0MjNa
# MGsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlTY2hpam5kZWwxIzAhBgNVBAoMGkpv
# aG4gQmlsbGVrZW5zIENvbnN1bHRhbmN5MSMwIQYDVQQDDBpKb2huIEJpbGxla2Vu
# cyBDb25zdWx0YW5jeTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAMsl
# ntDbSQwHZXwFhmibivbnd0Qfn6sqe/6fos3pKzKxEsR907RkDMet2x6RRg3eJkiI
# r3TFPwqBooyXXgK3zxxpyhGOcuIqyM9J28DVf4kUyZHsjGO/8HFjrr3K1hABNUsz
# P0o7H3o6J31eqV1UmCXYhQlNoW9FOmRC1amlquBmh7w4EKYEytqdmdOBavAD5Xq4
# vLPxNP6kyA+B2YTtk/xM27TghtbwFGKnu9Vwnm7dFcpLxans4ONt2OxDQOMA5Nwg
# cUv/YTpjhq9qoz6ivG55NRJGNvUXsM3w2o7dR6Xh4MuEGrTSrOWGg2A5EcLH1XqQ
# tkF5cZnAPM8W/9HUp8ggornWnFVQ9/6Mga+ermy5wy5XrmQpN+x3u6tit7xlHk1H
# c+4XY4a4ie3BPXG2PhJhmZAn4ebNSBwNHh8z7WTT9X9OFERepGSytZVeEP7hgypt
# SLcuhpwWeR4QdBb7dV++4p3PsAUQVHFpwkSbrRTv4EiJ0Lcz9P1HPGFoHiFAQQID
# AQABo4IBeDCCAXQwDAYDVR0TAQH/BAIwADA9BgNVHR8ENjA0MDKgMKAuhixodHRw
# Oi8vY2NzY2EyMDIxLmNybC5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNybDBzBggrBgEF
# BQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0
# dW0uY29tMDUGCCsGAQUFBzAChilodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y2NzY2EyMDIxLmNlcjAfBgNVHSMEGDAWgBTddF1MANt7n6B0yrFu9zzAMsBwzTAd
# BgNVHQ4EFgQUO6KtBpOBgmrlANVAnyiQC6W6lJwwSwYDVR0gBEQwQjAIBgZngQwB
# BAEwNgYLKoRoAYb2dwIFAQQwJzAlBggrBgEFBQcCARYZaHR0cHM6Ly93d3cuY2Vy
# dHVtLnBsL0NQUzATBgNVHSUEDDAKBggrBgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4Aw
# DQYJKoZIhvcNAQELBQADggIBAEQsN8wgPMdWVkwHPPTN+jKpdns5AKVFjcn00psf
# 2NGVVgWWNQBIQc9lEuTBWb54IK6Ga3hxQRZfnPNo5HGl73YLmFgdFQrFzZ1lnaMd
# Icyh8LTWv6+XNWfoyCM9wCp4zMIDPOs8LKSMQqA/wRgqiACWnOS4a6fyd5GUIAm4
# CuaptpFYr90l4Dn/wAdXOdY32UhgzmSuxpUbhD8gVJUaBNVmQaRqeU8y49MxiVrU
# KJXde1BCrtR9awXbqembc7Nqvmi60tYKlD27hlpKtj6eGPjkht0hHEsgzU0Fxw7Z
# JghYG2wXfpF2ziN893ak9Mi/1dmCNmorGOnybKYfT6ff6YTCDDNkod4egcMZdOSv
# +/Qv+HAeIgEvrxE9QsGlzTwbRtbm6gwYYcVBs/SsVUdBn/TSB35MMxRhHE5iC3aU
# TkDbceo/XP3uFhVL4g2JZHpFfCSu2TQrrzRn2sn07jfMvzeHArCOJgBW1gPqR3Wr
# J4hUxL06Rbg1gs9tU5HGGz9KNQMfQFQ70Wz7UIhezGcFcRfkIfSkMmQYYpsc7rfz
# j+z0ThfDVzzJr2dMOFsMlfj1T6l22GBq9XQx0A4lcc5Fl9pRxbOuHHWFqIBD/BCE
# hwniOCySzqENd2N+oz8znKooSISStnkNaYXt6xblJF2dx9Dn89FK7d1IquNxOwt0
# tI5dMIIGgjCCBGqgAwIBAgIQNsKwvXwbOuejs902y8l1aDANBgkqhkiG9w0BAQwF
# ADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcT
# C0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAs
# BgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcN
# MjEwMzIyMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjBXMQswCQYDVQQGEwJHQjEYMBYG
# A1UEChMPU2VjdGlnbyBMaW1pdGVkMS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBU
# aW1lIFN0YW1waW5nIFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAiJ3YuUVnnR3d6LkmgZpUVMB8SQWbzFoVD9mUEES0QUCBdxSZqdTkdizI
# CFNeINCSJS+lV1ipnW5ihkQyC0cRLWXUJzodqpnMRs46npiJPHrfLBOifjfhpdXJ
# 2aHHsPHggGsCi7uE0awqKggE/LkYw3sqaBia67h/3awoqNvGqiFRJ+OTWYmUCO2G
# AXsePHi+/JUNAax3kpqstbl3vcTdOGhtKShvZIvjwulRH87rbukNyHGWX5tNK/WA
# BKf+Gnoi4cmisS7oSimgHUI0Wn/4elNd40BFdSZ1EwpuddZ+Wr7+Dfo0lcHflm/F
# DDrOJ3rWqauUP8hsokDoI7D/yUVI9DAE/WK3Jl3C4LKwIpn1mNzMyptRwsXKrop0
# 6m7NUNHdlTDEMovXAIDGAvYynPt5lutv8lZeI5w3MOlCybAZDpK3Dy1MKo+6aEtE
# 9vtiTMzz/o2dYfdP0KWZwZIXbYsTIlg1YIetCpi5s14qiXOpRsKqFKqav9R1R5vj
# 3NgevsAsvxsAnI8Oa5s2oy25qhsoBIGo/zi6GpxFj+mOdh35Xn91y72J4RGOJEoq
# zEIbW3q0b2iPuWLA911cRxgY5SJYubvjay3nSMbBPPFsyl6mY4/WYucmyS9lo3l7
# jk27MAe145GWxK4O3m3gEFEIkv7kRmefDR7Oe2T1HxAnICQvr9sCAwEAAaOCARYw
# ggESMB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBT2
# d2rdP/0BE/8WoWyCAi/QCj0UJTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw
# AwEB/zATBgNVHSUEDDAKBggrBgEFBQcDCDARBgNVHSAECjAIMAYGBFUdIAAwUAYD
# VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz
# dFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMDUGCCsGAQUFBwEBBCkwJzAl
# BggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0B
# AQwFAAOCAgEADr5lQe1oRLjlocXUEYfktzsljOt+2sgXke3Y8UPEooU5y39rAARa
# AdAxUeiX1ktLJ3+lgxtoLQhn5cFb3GF2SSZRX8ptQ6IvuD3wz/LNHKpQ5nX8hjsD
# LRhsyeIiJsms9yAWnvdYOdEMq1W61KE9JlBkB20XBee6JaXx4UBErc+YuoSb1SxV
# f7nkNtUjPfcxuFtrQdRMRi/fInV/AobE8Gw/8yBMQKKaHt5eia8ybT8Y/Ffa6HAJ
# yz9gvEOcF1VWXG8OMeM7Vy7Bs6mSIkYeYtddU1ux1dQLbEGur18ut97wgGwDiGin
# CwKPyFO7ApcmVJOtlw9FVJxw/mL1TbyBns4zOgkaXFnnfzg4qbSvnrwyj1NiurMp
# 4pmAWjR+Pb/SIduPnmFzbSN/G8reZCL4fvGlvPFk4Uab/JVCSmj59+/mB2Gn6G/U
# YOy8k60mKcmaAZsEVkhOFuoj4we8CYyaR9vd9PGZKSinaZIkvVjbH/3nlLb0a7SB
# IkiRzfPfS9T+JesylbHa1LtRV9U/7m0q7Ma2CQ/t392ioOssXW7oKLdOmMBl14su
# VFBmbzrt5V5cQPnwtd3UOTpS9oCG+ZZheiIvPgkDmA8FzPsnfXW5qHELB43ET7HH
# FHeRPRYrMBKjkb8/IN7Po0d0hQoF4TeMM+zYAJzoKQnVKOLg8pZVPT8wgganMIIE
# j6ADAgECAhEAkKwIciD9xafEa1zHDfc9BjANBgkqhkiG9w0BAQwFADBXMQswCQYD
# VQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS4wLAYDVQQDEyVTZWN0
# aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFJvb3QgUjQ2MB4XDTI2MDMyNTAwMDAw
# MFoXDTQxMDMyNDIzNTk1OVowVTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJsaWMgVGltZSBTdGFtcGlu
# ZyBDQSBSNDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCu5EqiAa2C
# HGL5Zi1bmgPM8NUXwYZJ+BtQqHps43GLTC+sjVLypsBh+8uv+TLkgtVGD//vSmA0
# qrzELf9YRCh2MTAA/aGaQZKGg0BRCmziR3pbCnvgWjtGXBDUyn3j3K2lZAO8KxgF
# tlxwOYEAkL+CCqK4v9zzTl8ZwzDpPMiDIFa5THk8an1ieF5I09cXNrPQw+1ER1li
# ThaG0z6FrOpqwxZWmPRZQBw2E32878UB1bL0Zp91vuWZgsMpNNiPCoBj0/1F+LE8
# +NRokfqacFI0F2tftrRB2W7HQClLR9zjxFbWb5be2rceIfNyHUUfKGIvMI2NzoxS
# lxXnFqUG887D8W1Cj8DFok688JKxWvHR/9aQykSbd+9Vutj36ij2sgq/125wTpUZ
# /AgC0ph50bRs7gFrUyaXE9wSsOqMvCCC+sEm7vd/BemSG0TSHNXSmyCba+FCzeke
# WX03TRIcF3Laqd0Rw24OH7jpei4zaGhcI7nfdhBA4c8RScxNY6jeHLHHmSMMTk9W
# qn7H4dLhUBP5YEwbgbN4uv1i9ltTnHli8t1xHV0StX9BFgrnmunTX19kUXY1H5OR
# JbRZyZDdvm1oZyteDj0SnMozr+YSmdIleDUTXdfoY7b2taz8s2+QbOxLxcahEIYG
# Wzqu6h955tKwcANHcZ4gTmAhT3btuOiQsQIDAQABo4IBbjCCAWowHwYDVR0jBBgw
# FoAU9ndq3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFDp0pQxnxkJQwv21/Me7
# KTSC9Hq5MA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMCMGA1UdIAQcMBowCAYGZ4EMAQQCMA4GDCsGAQQBsjEB
# AgEDCDBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLnNlY3RpZ28uY29tL1Nl
# Y3RpZ29QdWJsaWNUaW1lU3RhbXBpbmdSb290UjQ2LmNybDB8BggrBgEFBQcBAQRw
# MG4wRwYIKwYBBQUHMAKGO2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1
# YmxpY1RpbWVTdGFtcGluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8v
# b2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMt5SR2bxngNm+N8o
# c6Gq76Gx1c235fkX7jw8Ho9MAkJGADerHE7dhsBXttqmzgr/7ZZahZSykGRPhPY1
# crj028kB8KzO0dKC2qQBAwtfgqMLKkkX/6bYq2uT33eD6ByAp2/XKD0LcmZh0kKe
# cvSBr6ln9ajX6u1dnx2fA7xEKy1M3qBhfQSUWLtjs2nFt0ELVLptzTlX9ID0cL+i
# OPfdboZ3CelT+JXKVKR2Sge0d4YiFAtPZkfSo8z1Z1x7y/Z9mwMIlBAnyuWXs4Ys
# NuxdrYIt/QxE31PDOJ9DesS4Bc7H9OTORlEV/AvfiF/VepKZpira1MzLYuCw+uoL
# Zn/pkpvd+CvNTS+mEHjBJNa6WK1j8qXFu+jIq+sG9QILHiyB6p/xpHrkJu8zkw39
# 3+VqF9eKlTY2VjRxdycZLrVemZ4Yp3wi33b+W58CllH3HqjmowlZ7SOrgmx8YwYO
# kgrHsXOQHyBp6O4FRb8In0+FzjT7ElGie9V7CfhL3IlVFZ4zjuKsZtH1iU3fGu4z
# /JnOGT6sCb0BbTqe/uhvpFCQBdH5xPGIA/LrbQUXjU2tWJgHhTIqnN/HvHyOHi5t
# M4zP3nhgh2rJ6Kqq2xsHBeNYs/R18xQ8DeIg+c90Eoaeh0YlN1KU8AyYol3K9M+q
# Y5ez8syd/7ZlrRnoVewgH3P1pcswgga5MIIEoaADAgECAhEAmaOACiZVO2Wr3G6E
# prPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVu
# aXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZp
# Y2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y
# ayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjELMAkGA1UE
# BhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UE
# AxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34vuTmflN4
# mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSoMcbvIKck
# 6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+njvMk2xq
# bNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHIohzuC8KN
# qfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRdaCtVD2bSl
# bfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837Q4QOZgYq
# VWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8mFSkcSkAE
# xzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI5XDYO962
# WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5boufUnq1UiY
# PIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplKShBSND36
# E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n858JSqPEC
# AwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10XUwA23uf
# oHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbROg79MA4G
# A1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8EKTAnMCWg
# I6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsGAQUFBwEB
# BGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVtLmNvbTAy
# BggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0bmNhMi5j
# ZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93d3cu
# Y2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCiaU58Q7EP8
# 9DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L94C9LGF3v
# jzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rGDxLUUAsO
# 0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f8pXdd3x2
# mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0CdY9rNLqyA
# 3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B7eMcZNhp
# n8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloMU+vUBfSo
# uCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1tVxTpV2Na
# 3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2DaGgK2W1
# eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNjt2ywzOr+
# tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCrW602NCmv
# O1nm+/80nLy5r0AZvCQxaQ4wggbiMIIEyqADAgECAhEA507yVbBQT/rbpt/3/Iuj
# FTANBgkqhkiG9w0BAQwFADBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMSwwKgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5n
# IENBIFI0MTAeFw0yNjAzMjUwMDAwMDBaFw0zNzA2MjQyMzU5NTlaMHIxCzAJBgNV
# BAYTAkdCMRcwFQYDVQQIEw5HcmVhdGVyIExvbmRvbjEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMTAwLgYDVQQDEydTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5n
# IFNpZ25lciBSMzcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCy/8Nt
# S9xQ2UUtBRF32bj7VK3n4m50Uqjk/zTciSziYV40H1LKah0/oEklYG42E4VCP3Dv
# sBUB6DmpCkDZ0jCnZBPIEevaH15ZJOQwFWP2ZXr5YjlJpb68Nlbs+ElNvKx32/1Y
# Hde3qqUSLybjulxPLz6T85+HOIqK7M1Bep8LspyhEP/q6nw5kGxTSrGvufmeH+JF
# 8CnVBcVMFA40FlIYh0cDJVFhhfTfdWgLy/vWuLMQoKkf3s/FvByf16r0rtbyHm/i
# emwxSioJL9zyZDDKUNAbHXl0dhXo2VxUV2NcPXWXuoKsjL+6cfk6Vm2DHnxAlFdF
# saBDIF1JOkSnC6PeLlBznZn2buF3vIIYJcq6N/zeFRCk4/HXDz7zgRsRRMdUB+rh
# yk5FoZaBjw0nLq3GZ3fClLUx5es5pUAxzNODMBn7JkFYip2BAGBPER5eV0ROhk6t
# GTG+fUiMiV+vgjg1YnP5FvnYWyEtWeQD/B2hp3vz0RvtdkM0p3igyadzrfpOBq5p
# pVk/YsuhTQkP99ivneHAGfi5e7lmxJ+meoBPrRLuzMmb81rzzbESjJHMsn5RVtc6
# Ucs7rcMqQC13PUIO7BbGBETV2ufCmV6lPTp3P7XJOvmnUCRTPbVvMTpxP/z+SOHg
# 4/OCBhiqs4FA9+4oQvlkk9w32NGASli9GWrm5wIDAQABo4IBjjCCAYowHwYDVR0j
# BBgwFoAUOnSlDGfGQlDC/bX8x7spNIL0erkwHQYDVR0OBBYEFGEQ6XoSr1HEhdTy
# z6R0D1DNIK/4MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEoGA1UdIARDMEEwCAYGZ4EMAQQCMDUGDCsGAQQBsjEB
# AgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzBKBgNV
# HR8EQzBBMD+gPaA7hjlodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJs
# aWNUaW1lU3RhbXBpbmdDQVI0MS5jcmwwegYIKwYBBQUHAQEEbjBsMEUGCCsGAQUF
# BzAChjlodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3Rh
# bXBpbmdDQVI0MS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28u
# Y29tMA0GCSqGSIb3DQEBDAUAA4ICAQAD6j2N0azN+hl6k6bKB5/U6VuSOs93ZBb3
# Pczy9VtBIKu4947Z5GwL0aFngIxl+GSuLFrJgPruBCRvKJEJsm7kv+LQ1COVCEG9
# tZ+IRtr4ocUoa53lgdFaENlS0N4wgkZkbQEPv+x+1lSjYh+T4JeL9mUznT7Erc6S
# p5dWLka5sMP/m3GZi6oJPdPcsCKWagH7m2H2xDGIyHJC5PdH9phvi/KmhkktiSVT
# NNqVeV5bWdX2zhRE6UTfz0IcMoCL996lFIydXxOCE4MNDHDM0as4lnTiT/KHMccO
# 6l8c9TnUVgmpci9ar1IABZ2U1XUkYjGGSn9MC3EHDP9V39VuBVvZ33/BEV/EWSRr
# f07T7jFplKX+gQr/UOqPGMlE7ZJ72UaUkNJy7bVl3bcLKzdpjIHzLkf/4MVa1V7w
# 8wqCv5W4gOnRGTlud5UMARbRM8BPxR/CXYXoMmIOD8pmTk2axgRL4LG8XtuchISd
# CHRmtacAmLGq5XSYSVTHTXADlO48iDKh3HM2r98LSF6f0sG12d8V9Jn7C3wDUieO
# xuKj4MdWrW+hiJU2kF87v6eH00HgCFFc2V0+CvfOCMn7juzS41jLaINcBlKWQ/fK
# b/uDLfWOW73z1I2lFY7Xj8tQ1XYtK5eREjWItM8jpl1cbQOc88btR+0XS2TmboE/
# 141+va2PWzGCBcMwggW/AgEBMGowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFz
# c2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUgU2ln
# bmluZyAyMDIxIENBAhAIMk+dt9qRb2Pk8qM8Xl1RMA0GCWCGSAFlAwQCAQUAoIGE
# MBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEID+OgPBsYML35zRIPgn3n77fDmnBb1OwUoj1mvGe0dALMA0GCSqGSIb3DQEB
# AQUABIIBgHsY8K0RZW0+HkzO94q+MI1lyNj06HR5iiaW0XlBus6DSntksBO76OmA
# wEC1063e1WP9FI/jLAJ6J8vFkmOGTjUPHUwQxVN2m6wGAjlRXL0emlIXK43GwIsZ
# WL676+M45yUU/wdPh0ejuM5/Fdt+xawVC8njZF79gemgeuFWQ/dJnxRH8/rjnidA
# oZS6JDoFhs1RKWjngQmy2So8qbEytuwi9QHwMjpe2jQxS9bseeBZwMHiB5Cqpf28
# IOXkDnP/pTlwxWpJRP+pX77upfGIrbNNtm+S9DjKjebxl9g9r3ahNkqOnLcpf7lE
# Icd1s03R5zxSvevF+hXOqfz9ZVcYg/dwfijwZyAB9194Cj6bPd7cILvRRU8nEokw
# ILmWJyl0lAsgUgioiefV7NaFK01wpdnoxq+fEjQJve2RsPEG3O49KlkzRnP+XnzP
# 456Fi8eKol9J5qz802fSNC9a+7HSf/P5zXt7qS1yTDl5pUwp4mpiKnysIgRSSxs2
# 6PODH9H7KaGCAyMwggMfBgkqhkiG9w0BCQYxggMQMIIDDAIBATBqMFUxCzAJBgNV
# BAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLDAqBgNVBAMTI1NlY3Rp
# Z28gUHVibGljIFRpbWUgU3RhbXBpbmcgQ0EgUjQxAhEA507yVbBQT/rbpt/3/Iuj
# FTANBglghkgBZQMEAgIFAKB5MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJ
# KoZIhvcNAQkFMQ8XDTI2MDcyMDA4NDE1MFowPwYJKoZIhvcNAQkEMTIEMNWUOdVT
# aCjVIUGVheJ4L1/sDmcSdGeQr3lXKH/s235OVjhQP3mfcZoMdwYk31U5lDANBgkq
# hkiG9w0BAQEFAASCAgAQHJr9lh6c6ijwC8U45R0PMR4BC+1gVN79g7PH97PLbewY
# ljyZ8GWmKMeOuZGErLdNTJN3TgK/TugcONga5fJ26LPm3I4meJXu4E+4a1gXk9Us
# RJ1/B5Tsp9XJFPBBrIirqNWPTgEI0RrUHsCM+1Y0cgaT2GlFL3OCMXu06X+Xj5Uf
# z4rjY7VBaKna0111PN43PoD271H4HJfFbx9C5yCdtmPD65cHaAqlFG/zYflzzOQ0
# 4820OKfOCdfvqyPSSVwQ3nzGdXaZCjNyc2xzInEYTkJ+Omoi2rEcZ5PovCFCjqtV
# 4PybJqQXjjBNshM3c517V/bV5B01bBtmoVbslZ95kCU/gfoUx0H0WRHb7/qAv36b
# bbU6RnNDdyyFTdttUEXVieyHf6O4Cw14U3zaQmuA0HfWhFSWkxwrPEN4GXjRGtki
# cLU4O39IXDxr1bkUw5NQnEOXCVzLUuWLkD03lB9wgi5qwzGVyoVJ7x01mYdF08Mb
# ezzd5GdlVIFZhCVLW02IB41CDpajRImOO44zWj4NWPEM4RW0DVNOiDN2aREQ8uaR
# EfBungoHd6ALuYSqqlz+FGzsvth/IN/1zqNSdm3XrRnY0NJD9F/cA0HRvu+iNNYO
# ZMr5i13o/xkrZIwoF8OeLvLQfx9ibcJ90x/YjXMREyCmrYtXwAEfQOzkZDPKEQ==
# SIG # End signature block