I implement a WPF interface in a PowerShell script. The value of the variable MyFolder.SelectedFolder is displayed when the window is opened. But when the value is modified, it is not updated.
Class cFolder {
[String] $Path
[String] $SelectedFolder
}
$MyFolder = [cFolder]::new()
$MyFolder.Path = "C:\temp"
$MyFolder.SelectedFolder = "temp"
<Label x:Name ="Lbl_SelFolder" Content="{Binding Path, UpdateSourceTrigger=PropertyChanged}"/>
$XMLReader = (New-Object System.Xml.XmlNodeReader $Form)
$XMLForm = [Windows.Markup.XamlReader]::Load($XMLReader)
$XMLForm.DataContext = $MyFolder
$LblSelFolder = $XMLForm.FindName('Lbl_SelFolder')
Thanks for your help
Note:
If you want to bind the .SelectedFolder
property of your $MyFolder
object to your label, add =SelectedFolder
to the {Binding Path}
Content
attribute value in your XAML (the Path=
part may actually be omitted):
<Label x:Name ="Lbl_SelFolder" Content="{Binding Path=SelectedFolder}" />
The problem is that no one is observing later changes you're making to the properties of the $MyFolder
object that serves as your data context.
Note:
The following is a workaround to compensate for the current inability to declare your [cFolder]
as implementing the [System.ComponentModel.INotifyPropertyChanged]
interface using a PowerShell class
definition, due to non-support for property getters and setters.
GitHub issue #2219 requests adding such support in future versions of PowerShell (Core) 7.
If you don't mind embedding C# code in your PowerShell script and compiling it on demand with Add-Type
, you can implement [cFolder]
so that it directly implements said interface, in which case your $MyFolder
object can itself act as the data context, the way you attempted - see the bottom section.
One way to fix this is to store your object in a System.Collections.ObjectModel.ObservableCollection<T>
instance and assign the latter to the .DataContext
property of your WPF form (window):
$XMLForm.DataContext =
[System.Collections.ObjectModel.ObservableCollection[cFolder]] $MyFolder
To then trigger an update of the label control based on an update to $MyFolder
:
Update the data-bound property of $MyFolder
, e.g. .SelectedFolder
:
$MyFolder.SelectedFolder = 'tempNEW'
Then re-assign $MyFolder
to the observable collection, as its one and only element; this is what triggers the label update:
$XMLForm.DataContext[0] = $MyFolder
A self-contained example that updates the data-context object's .SelectedFolder
property in a loop to demonstrate that the label is updated in response:
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
Add-Type -AssemblyName System.Windows.Forms
Class cFolder {
[String] $Path
[String] $SelectedFolder
}
# Create the [cFolder] instance that serves as the data source.
$MyFolder = [cFolder] @{ Path = 'C:\temp'; SelectedFolder = 'temp'}
# Define the XAML document defining a WPF form with a single label.
[xml] $xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Label data-binding demo"
Height="80" Width="350"
>
<Label x:Name ="Lbl_SelFolder" Content="{Binding Path=SelectedFolder}"/>
</Window>
"@
# Parse the XAML, which returns a [System.Windows.Window] instance.
$form = [XamlReader]::Load([XmlNodeReader] $xaml)
# Use an observable collection as the data context that
# contains $MyFolder as its first and only element.
$form.DataContext = [ObservableCollection[cFolder]] $MyFolder
# Show the window non-modally and activate it.
$null = $form.Show()
$null = $form.Activate()
# While the window is open, process pending GUI events
# and update the selected folder.
$i = 0
while ($form.IsVisible) {
# Note: Even though this is designed for WinForms, it works for WPF too.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little.
Start-Sleep -Milliseconds 100
# Update the selected folder.
$MyFolder.SelectedFolder = 'temp' + ++$i
# It is through *re-assigning* the object as the first (and only)
# element of the observable collection that the control update
# is triggered.
$form.DataContext[0] = $MyFolder
}
A variant solution that uses two [cFolder]
instances as the data context, each binding to a separate label; note the [0].SelectedFolder
and [1].SelectedFolder
binding paths, which refer to the first and second element ([cFolder]
instances) of the observable collection:
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
Add-Type -AssemblyName System.Windows.Forms
Class cFolder {
[String] $Path
[String] $SelectedFolder
}
# Create *two* [cFolder] instances this time.
$MyFolder1 = [cFolder] @{ Path = 'C:\temp1'; SelectedFolder = 'temp1'}
$MyFolder2 = [cFolder] @{ Path = 'C:\temp2'; SelectedFolder = 'temp2'}
# Define the XAML document defining a WPF form with two labels.
# Note the use of [0].SelectedFolder and [1].SelectedFolder as the
# Binding argument to bind to specific elements of the observable collection
# created below.
[xml] $xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Label data-binding demo"
Height="100" Width="350"
>
<Grid>
<Label x:Name ="Lbl_SelFolder1" Content="{Binding [0].SelectedFolder}"/>
<Label x:Name ="Lbl_SelFolder2" VerticalAlignment="Bottom" Content="{Binding [1].SelectedFolder}"/>
</Grid>
</Window>
"@
# Parse the XAML, which returns a [System.Windows.Window] instance.
$form = [XamlReader]::Load([XmlNodeReader] $xaml)
# Use an observable collection as the data context that
# contains the two [cFolder] instances as its elements.
$form.DataContext = [ObservableCollection[cFolder]] ($MyFolder1, $MyFolder2)
# Show the window non-modally and activate it.
$null = $form.Show()
$null = $form.Activate()
# While the window is open, process pending GUI events
# and update the selected folder.
$i = 0
while ($form.IsVisible) {
# Note: Even though this is designed for WinForms, it works for WPF too.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little.
Start-Sleep -Milliseconds 100
# Update the properties of both [cFolder] instances.
$MyFolder1.SelectedFolder = 'temp1-' + ++$i
$MyFolder2.SelectedFolder = 'temp2-' + $i
# It is through *re-assigning* one of the objects as an element of the
# observable collection that the control update is triggered.
$form.DataContext[0] = $MyFolder1
}
[cFolder]
so that it implements [System.ComponentModel.INotifyPropertyChanged]
itself:The C# code is passed as string to the Add-Type
cmdlet, and incurs a once-per-session performance penalty for the compilation.
The features used in the C# code aren't the most modern ones, so as to ensure that compilation also succeeds in Windows PowerShell.
If you adapt this solution to the two-label variant above, you will again need a System.Collections.ObjectModel.ObservableCollection<T>
collection to house your two [cFolder]
instances; however, you will not need to re-assign any elements in order to trigger an update, because the collection propagates the property-changed events automatically from INotifyPropertyChanged
-implementing elements. In concrete terms:
class
definition with the Add-Type
statement below.$form.DataContext[0] = $MyFolder1
statement at the end, which is then no longer necessary.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
Add-Type -AssemblyName System.Windows.Forms
# Use on-demand compilation of C# code to define your [cFolder]
# class as implementing the [System.ComponentModel.INotifyPropertyChanged]
# with property setters that notify an observer of a property-value change.
Add-Type -ErrorAction Stop @'
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class cFolder : INotifyPropertyChanged
{
private string _path;
private string _selectedFolder;
// Define the event that notifies observers of a property change.
public event PropertyChangedEventHandler PropertyChanged;
public string Path { get { return _path; } set { _path = value; NotifyPropertyChanged(); } }
public string SelectedFolder { get { return _selectedFolder; } set { _selectedFolder = value; NotifyPropertyChanged(); } }
// This method is called by the `set` accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
// The parameter *must* be optional.
private void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null) {
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
'@
# Create an instance of [cFolder] that serves as the data source.
$MyFolder = [cFolder] @{ Path = 'C:\temp'; SelectedFolder = 'temp'}
# Define the XAML document defining the WPF form with a single label.
[xml] $xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Label data-binding demo"
Height="80" Width="350"
>
<Label x:Name ="Lbl_SelFolder" Content="{Binding SelectedFolder}"/>
</Window>
"@
# Parse the XAML, which returns a [System.Windows.Window] instance.
$form = [XamlReader]::Load([XmlNodeReader] $xaml)
# Now that [cFolder] implements [System.ComponentModel.INotifyPropertyChanged],
# it can *directly* server as the data context.
$form.DataContext = $MyFolder
# Show the window non-modally and activate it.
$null = $form.Show()
$null = $form.Activate()
# While the window is open, process pending GUI events
# and update the selected folder.
$i = 0
while ($form.IsVisible) {
# Note: Even though this is designed for WinForms, it works for WPF too.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little.
Start-Sleep -Milliseconds 100
# Update the selected folder. Due to $MyFolder implementing
# [System.ComponentModel.INotifyPropertyChanged] and acting as the data
# context of the form, the label will update automatically.
$MyFolder.SelectedFolder = 'temp' + ++$i
}