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?
"[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.
.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']