Search code examples
wpfpowershelldata-binding

WPF Data Binding to a property of an object in PowerShell


I am attempting to create a two-way binding between an object in an observable collection and a WPF form object. This works fine until the property to which I want to bind is a "sub-property" (apologies if my terminology is wrong).

Here's the code:

$HashDataContexts = @{}
$FormMain.DataContext = $HashDataContexts

$DGSourceData = @()
$SourceData = @(@("Item One", 1), @("Item Two", 2), @("Item Three", 3))
foreach($SourceItem in $SourceData) {
    $SourceObject = [PSCustomObject]@{
        SubObject = [PSCustomObject]@{
            ItemName = $SourceItem[0]
            ItemCount = $SourceItem[1]
        }
    }
    $DGSourceData += $SourceObject
}
$DataContext = New-Object -TypeName System.Collections.ObjectModel.ObservableCollection[Object] -ArgumentList (,$DGSourceData)
$HashDataContexts.Add("Grid1",$DataContext)

$DGBinding = New-Object System.Windows.Data.Binding
$DGBinding.Path = "[Grid1.SubObject]"
$DGBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
[System.Windows.Data.BindingOperations]::SetBinding($DG1,[System.Windows.Controls.DataGrid]::ItemsSourceProperty, $DGBinding)

# Show the window
$FormMain.Dispatcher.InvokeAsync{$FormMain.ShowDialog()}.Wait()

The issue is the $DGBinding.Path whereby it doesn't seem possible to use the dotted notation to specify a sub-property. If I create the object without the sub-property, i.e. ItemName and ItemCount are direct properties of $SourceObject and I create the binding using "[Grid1]" only, all is well.

Any ideas?


Solution

    • "[Grid1.SubObject]" as the Binding.Path property value won't work as intended, because what is inside [...] is as a whole considered a dictionary key or collection index, with no support for accessing nested properties.

    • While there is a syntax for accessing properties of a collection - "[Grid1]/SubObject" - this too will not work as intended, as it accesses the collection item that is considered current only (defaulting to the first), not all of them.

      • As a moot aside: Since you're binding to the .ItemsSource property of a data grid, whatever single property value "[Grid1]/SubObject" returns would itself have to implement the System.Collections.IEnumerable interface in order to render anything, and a single [pscustomobject] instance does not implement this interface; if you omitted [PSCustomObject] in SubObject = [PSCustomObject]@{ ... }, you'd get a hashtable instead, which does implement this interface, resulting in its key-value pairs getting treated as the elements of the collection to render in the data grid.

    Therefore:

    • Use just '[Grid1]' as the .Path property value (it specifies the 'Grid1' key of the hashtable stored in $FormMain.DataContext):

      $DGBinding.Path = '[Grid1]'     
      
    • Use member-access enumeration to populate the observable collection only with the .SubObject property values (sub-objects), not its wrapper objects (note the use of a cast for concision and syntactic simplicity).

      $DataContext =
        [Collections.ObjectModel.ObservableCollection[Object]] $DGSourceData.SubObject
      

    A self-contained example (requires PSv5+) that demonstrates the solution:

    using namespace System.Windows
    using namespace System.Windows.Data
    using namespace System.Windows.Controls
    using namespace System.Windows.Markup
    using namespace System.Xml
    using namespace System.Collections.ObjectModel
    # Note: `using assembly PresentationFramework` works in Windows PowerShell,
    #       but seemingly not in PowerShell (Core) as of v7.3.5
    Add-Type -AssemblyName PresentationFramework
    
    # Define the XAML document defining the WPF form with a single data grid.
    [xml] $xaml = @"
      <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="450" Width="500"
      >
          <Grid>
              <DataGrid x:Name="DataGrid" />
          </Grid>
      </Window>
    "@
    
    # Parse the XAML, which returns a [System.Windows.Window] instance.
    $FormMain = [XamlReader]::Load([XmlNodeReader] $xaml)
    
    # Define the collection of objects to bind to the data grid.
    $dgSourceData =
      foreach ($sourceItem in ("Item One", 1), ("Item Two", 2), ("Item Three", 3)) {
        [PSCustomObject] @{
          SubObject = [PSCustomObject] @{
            ItemName  = $sourceItem[0]
            ItemCount = $sourceItem[1]
          }
        }
      }
    
    # Add an observable collection to a hashtable and assign it
    # to $FormMain.DataContext
    $FormMain.DataContext =
     @{
       # The collection must *directly* contain the objects to bind, hence the use of .SubObject
       GridDataSource = [ObservableCollection[Object]] $dgSourceData.SubObject
    }
    
    # Create a data binding.
    $dgBinding = 
      [Binding] @{
        Mode = [BindingMode]::TwoWay
        Path = '[GridDataSource]' # Index into the hashtable stored in $FormMain.DataContext  
      }
    
    # Connect the binding to the data grid's .ItemsSource property
    # Note: For troubleshooting, examine the object returned from this call. 
    $null = 
      [BindingOperations]::SetBinding(
        $FormMain.FindName('DataGrid'), # the target grid control
        [DataGrid]::ItemsSourceProperty, # what grid property to bind to 
        $dgBinding # the data binding
      )
    
    # Show the window modally.
    # You can modify the data in the grid, and it will be reflected in 
    # the observable collection, 
    $null = $FormMain.ShowDialog()
    
    # Show the potentially modified collection. 
    Write-Verbose -Verbose "The potentially modified collection:"
    $FormMain.DataContext['GridDataSource']