El doctor Sertato
El doctor Sertato

Reputation: 43

Variable in WPF Binding with PropertyChanged

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

Upvotes: 2

Views: 88

Answers (1)

mklement0
mklement0

Reputation: 437042

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
}

Variant solution with ad-hoc compiled C# code to define [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:

    • In the two-label solution above, replace the class definition with the Add-Type statement below.
    • Remove the $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
}

Upvotes: 1

Related Questions