wpfpowershelldata-bindingpropertychanged

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


Solution

  • Note:


    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:

    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:


    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:
    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
    }