0
votes

I'm trying to search a WPF DataGrid via PowerShell, select and highlight the cells, each one in turn, that contains the search keyword. The goal is to 1) search the DataGrid, 2) select and highlight the cell that contains search keyword while moving the focus/scrolling to the row that contains column/cell and 3) repeat the same for any other columns/cells that may contain the same keyword.

I put together a sample script (see below) to demonstrate what I have done so far. In the sample script shown below, I was able to search and find the row contains the keyword and scroll to that row. However, I couldn't figure out how to highlight the cell that contains the keyword on that row.

The sample data shown below has pre-defined columns for demonstration purpose. However, the actual data returned from backend is dynamic such that the number of columns, title of the columns would vary and any column may contain the keyword. How do we select the cell that contains the keyword? Is there any better approach to achieve this overall goal? Thanks in advance for the help.

[xml]$xaml=@"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Test"
    Title="MainWindow" Height="175" Width="550">
<Grid>
    <TextBox x:Name="tb_Search" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="149"/>
    <Button x:Name="bt_Search" Content="Search" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="165,10,0,0" />        
    <DataGrid x:Name="dg" Margin="10,45,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="100" Height="Auto" Width="Auto" ColumnWidth="Auto" AlternationCount="1" IsReadOnly="True" SelectionMode="Extended" SelectionUnit="Cell" Background="White" /> 
</Grid>
</Window>
"@

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load($reader)

#Turn XAML into PowerShell objects
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'x:Name')]]") | ForEach-Object{
Set-Variable -Name ($_.Name) -Value $Window.FindName($_.Name)
}

#sample data
$DataSet = New-Object System.Data.DataSet
$Table = $DataSet.Tables.Add("Table")
$Properties = @("Country","Capital","Population")
$Properties | foreach {
 $Column = New-Object System.Data.DataColumn($_)
 $Table.Columns.Add($Column)
 }
$Null=$Table.Rows.Add("China PR","Beijing","20,693,000")
$Null=$Table.Rows.Add("India","New Delhi","16,787,949") 
$Null=$Table.Rows.Add("Japan","Tokyo","13,189,000")
$Null=$Table.Rows.Add("Philippines","Manila","12,877,253")
$Null=$Table.Rows.Add("Russia","Moscow","11,541,000")
$Null=$Table.Rows.Add("Egypt","Cairo","10,230,350")
$Null=$Table.Rows.Add("USA","Washington, D.C","658,893")
$Null=$Table.Rows.Add("China PR","Beijing","20,693,000")
$Null=$Table.Rows.Add("India","New Delhi","16,787,949") 
$Null=$Table.Rows.Add("Japan","Tokyo","13,189,000")
$Null=$Table.Rows.Add("Philippines","Manila","12,877,253")
$Null=$Table.Rows.Add("Russia","Moscow","11,541,000")
$Null=$Table.Rows.Add("Egypt","Cairo","10,230,350")
$Null=$Table.Rows.Add("USA","Washington, D.C","658,893")

#populate datagrid
$DataView = New-Object System.Data.DataView($Table)
$array = New-Object System.Collections.ArrayList
[void] $array.AddRange($DataView)       
$dg.clear()
$dg.ItemsSource = $array
$dg.IsReadOnly = $true

$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
for ($i = 0; $i -lt $dg.Items.Count; $i++)
{
    if ($dg.Items[$i].Row[$dg.Columns.DisplayIndex] -eq "$SearchValue")
    {
        [System.Windows.Forms.MessageBox]::Show("Keyword Found")
        $dg.ScrollIntoView($dg.Items[$i]) #scroll to the row that contains the keyword searched
    }
}
})

#Display Form
$Window.ShowDialog() | Out-Null
1

1 Answers

1
votes

Using a very similar method to what you're doing I was able to get it to highlight all cells that contain the search phrase (since I misunderstood the question at first). What I had to do was iterate through columns instead of searching all columns at once for each row. That way we know what column the desired info is in, and then we can select the cell in that column after we scroll the correct row into view.

$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = 0; $i -lt $dg.Items.Count; $i++)
{
    0..($dg.Columns.Count-1)|?{$dg.Items[$i].Row[$_] -eq "$SearchValue"}|%{
        $dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$_])
        $DGCell = $dg.Columns[$_].GetCellContent($dg.Items[$i]).Parent
        $DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
        $dg.SelectedCells.add($DGCellInfo)
    }
}
})

This at least gets you the groundwork to select a cell, you'll just need to figure out how to track the current cell to be able to move on to the next one since it sounds like you want to be able to move cell to cell by clicking the Search button over and over.

Edit: In regards to setting it up to do a Find/Find Next, you could setup a global variable, set $i to that, and then set that global variable inside your loop each time. Then anytime you get a match run a break to kill the loop, and it should just pick up where it left off. Might also want to add a line to reset the global variable before the loop so that it starts over if it hits the end. Something like this should do it:

$global:SearchIndex = 0
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = $global:SearchIndex; $i -lt $dg.Items.Count; $i++)
{
    0..($dg.Columns.Count-1)|?{$dg.Items[$i].Row[$_] -eq "$SearchValue"}|%{
        $dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$_])
        $DGCell = $dg.Columns[$_].GetCellContent($dg.Items[$i]).Parent
        $DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
        $dg.SelectedCells.add($DGCellInfo)
        $global:SearchIndex = $i
        break
    }
}
#check if we hit the end of the table. If we did display a notice and reset the search index.
If($global:SearchIndex -ge $dg.Items.Count){
    [System.Windows.Forms.MessageBox]::Show("No more matches found, resetting search to begining of table.")
    $global:SearchIndex=0
}
})

Edit2: Ok, you're right, the break stops increments, so it will just find the same result each time. In order to fix that we need to also track the column. If we change the internal loop to a For loop as well we can just increment the internal loop each time we track. We also need to track breaks, so if the inner loop breaks the outer loop will too. So, something like this should do that:

$global:SearchIndex = 0
$global:SearchIndex2 = 0
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = $global:SearchIndex; $i -lt $dg.Items.Count; $i++)
{
    $global:BreakPoint = $false
    For($j=$global:SearchIndex2;$j -le 2;$j++){
        If($dg.Items[$i].Row[$j] -eq "$SearchValue"){
            $dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$j])
            $DGCell = $dg.Columns[$j].GetCellContent($dg.Items[$i]).Parent
            $DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
            $dg.SelectedCells.add($DGCellInfo)
            $global:SearchIndex = $i
            $global:SearchIndex2 = $j+1
            $global:BreakPoint = $true
            break
        }
    }

    If($global:BreakPoint){break}
    $global:SearchIndex2=0
}
#check if we hit the end of the table. If we did display a notice and reset the search index.
If($global:SearchIndex -ge $dg.Items.Count){
    [System.Windows.Forms.MessageBox]::Show("No more matches found, resetting search to begining of table.")
    $global:SearchIndex=0
    $global:SearchIndex2=0
}
})