Controlling the type returned by a Function in PowerShell

Viewed 271

So, I'm trying to create a GUI for a search application using a WPF form. My form has a TextBox and ComboBox for input, and the contents are applied as a filter every time the change events fire. $var_SearchRegion is my ComboBox, $var_SearchFor is my TextBox, and $var_DGDisplay is my DataGrid for the return data.

In order to set the filter, I have created a basic function. I know there are cleaner ways to do this than an If Then ElseIf tree, but this was quick and it works. The function looks like this:

Function GetFilteredItems
{
    $RSelect = $var_SearchRegion.SelectedValue.Content
    $PF = $var_SearchFor.Text
    $RF = If ($RSelect -eq 'All Regions') {''} Else {$RSelect}
    If ($PF -eq '' -and $RF -eq '')
    {
        $DPST
    }
    ElseIf ($PF -eq '')
    {
        $DPST | ? {$_.Region -eq $RF}
    }
    ElseIf ($RF -eq '')
    {
        $DPST | ? {$_.FilePath -like "*$PF*"}
    }
    Else
    {
        $DPST | ? {$_.Region -eq $RF -and $_.FilePath -like "*$PF*"}
    }
}

The issue happens when the filter reduces the dataset to exactly one entry and returns it. I have had this problem before with PowerShell using .NET methods, and I am assuming the type changes from [Array] of "something" to just "something", and that is causing issues with the WPF objects.

Here is an example of the event handler code which is causing problems when the filter gets my return set down to 1 item:

$var_SearchFor.Add_TextChanged({
    $var_DGDisplay.ItemsSource = GetFilteredItems
    $var_DGDisplay.Refresh
})

When GetFilteredItems returns 1 item, I get this error:

Exception setting "ItemsSource": "Cannot convert the "@{Region=SW; 
FilePath=\\server\share\folder\file.ext}" value of type 
"Deserialized.System.Management.Automation.PSCustomObject" to type "System.Collections.IEnumerable"."
At C:\Users\Matthew\GUITest.ps1:90 char:5
+     $var_DGDisplay.ItemsSource = GetFilteredItems
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationException
    + FullyQualifiedErrorId : ExceptionWhenSetting

I can easily "fix" this problem by wrapping the function call and explicitly converting to an [array] type, like this:

$var_SearchFor.Add_TextChanged({
    $var_DGDisplay.ItemsSource = [array](GetFilteredItems)
    $var_DGDisplay.Refresh
})

But it feels backwards to have to do this. I should be able to fix this inside the function somehow. However, wrapping the output lines of the function (in 4 places) with the same [array]() did not do the trick. I found some information on PowerShell Advanced Functions, where I could use the syntax [OutputType([Array])] to designate the output type, but even though I verified I was getting System.Array as my output type using (Get-Command GetFilteredItems).OutputType, it would still fail with the same error.

TL;DR - Is there a way to avoid having to wrap my function call with [array]() and still make it work with my WPF DataGrid control's ItemSource Property?

1 Answers
  • PowerShell by default enumerates collections that you output from a function (whether you output them implicitly or via a return statement)

    • To prevent that, use Write-Output -NoEnumerate or, more simply and efficiently (but more obscurely), wrap the output collection in a single-element aux. array (, $collection) - see this answer for more information.
  • In your case, you additionally need to make sure that what you're wrapping is itself always a collection, for which you can use @() or cast to [array].

Function GetFilteredItems
{
    $RSelect = $var_SearchRegion.SelectedValue.Content
    $PF = $var_SearchFor.Text
    $RF = If ($RSelect -eq 'All Regions') {''} Else {$RSelect}

    # Collect your command's output in an array-typed variable ([object[]])
    [array] $result = 
      If ($PF -eq '' -and $RF -eq '')
      {
        $DPST
      }
      ElseIf ($PF -eq '')
      {
        $DPST | ? {$_.Region -eq $RF}
      }
      ElseIf ($RF -eq '')
      {
        $DPST | ? {$_.FilePath -like "*$PF*"}
      }
      Else
      {
        $DPST | ? {$_.Region -eq $RF -and $_.FilePath -like "*$PF*"}
      }

    # Output the array $result as-is, via an aux. wrapper array.
    , $result

}

Note: You don't strictly need the immediate $result variable; you could use , @(if ...) directly.

However, note that in neither case will the command's output stream (emit objects as they're being created), because the entire output must be collected first, before the aux. array can be constructed around it.

Related