PowerShell DeepDive: WPF, Data Binding and INotifyPropertyChanged

PowerShell can be used to create some nice UI front-ends using the Windows Presentation Framework (WPF). You can create anything from a simple popup window to a full-blown, self-contained application. A major concept in WPF is that of data binding. Data binding allows you to create some separation between the design of the UI and data it operates with. Binding UI element attributes to a data source means that when you need to update data in the UI, you don’t need to edit the object property itself, rather you simply update the data source it is bound to. For that to work automatically, you need to bind to a collection type that implements the INotifyPropertyChanged interface. This interface provides a notification mechanism that can notify the bound element when a value in the datasource has changed. The UI can then automatically update to reflect the change.

Without using data binding you need to create some code to update the UI element and tell it what new data to display. In a single-threaded UI you can simply set the object property directly, and in a multi-threaded UI you will invoke the dispatcher on the object, so that the property can be safely updated without contention from another thread.

Maybe this all sound Greek (no offence if you speak Greek!), so lets do a couple of examples to illustrate.

Increment Me!

Here I created a simple function that will display a WPF UI window. Then I created an event handler so that when the button is clicked, the textbox will update. The textbox contains a number that will be incremented every time you click the button.


Add-Type -AssemblyName PresentationFramework

function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "WPF Window"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create a textbox object
    $TextBox = New-Object System.Windows.Controls.TextBox
    $TextBox.Height = 85
    $TextBox.HorizontalContentAlignment = "Center"
    $TextBox.VerticalContentAlignment = "Center"
    $TextBox.FontSize = 30
    $Hash.TextBox = $TextBox

    # Create a button object
    $Button = New-Object System.Windows.Controls.Button
    $Button.Height = 85
    $Button.HorizontalContentAlignment = "Center"
    $Button.VerticalContentAlignment = "Center"
    $Button.FontSize = 30
    $Button.Content = "Increment Me!"
    $Hash.Button = $Button

    # Assemble the window
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Margin = "5,5,5,5"
    $StackPanel.AddChild($TextBox)
    $StackPanel.AddChild($Button)
    $Window.AddChild($StackPanel)
    $Hash.Window = $Window
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Set the value of the textbox
$Hash.TextBox.Text = [int]0

# Add an event for the button click
$Hash.Button.Add_Click{

    # Get the current value
    [int]$CurrentValue = $Hash.TextBox.Text

    # Increment the value
    $CurrentValue ++

    # Update the text property with the new value
    $Hash.TextBox.Text = $CurrentValue
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

gif

Since the data type for the text property of the textbox is a string, I cannot increment the value directly, I must first cast it into an integer, then return it back to the textbox where it will again be stored as a string. Since the textbox text property is not bound to a data source, I must update the property directly. In this simple example that is not a problem, but it does mean that I cannot work independently of the data.

Creating a data binding

In the following example I have bound the textbox text property to a data source. Data binding in WPF can be done either in XAML or code, but here we are using code to illustrate how that can be done.

First, we will create a datasource object. This will be what we bind to. Since the datasource must notify the UI when the bound value has changed, we must use a collection that implements the INotifyPropertyChanged interface. You can actually create a custom class in C# to do that, as Ryan Ephgrave nicely describes on his blog. This is quite a flexible method, however it does mean you need to create all the properties used in your datasource up front in the class. A ‘native’ way to do that in .Net is to use an observable collection.

In this example, I am going to use an observable collection with a simple System.Object type, so I can add multiple kinds of object to the collection if I want.


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

After creating the observable collection, I then add my initial value for the textbox – 0, and finally set the collection as the datacontext for the WPF window.

When using data binding, one can either declare the binding source directly, or set the datacontext for one of the parent elements. When you set the datacontext at the window level for example, this will be inherited by default by any child elements in the window, such as textboxes, panels etc. For more granular control, you can set the datacontext on the object itself rather than at the top-level. In either case, the bound object will use this datacontext.

Next, I create a binding object and pass to it which object in the datasource I want to bind to. In this case, I only have one item in the collection so I can simply reference it using it’s index number, which is [0]. If the collection contains multiple items, you need to keep a reference of which index your items are using.


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding -ArgumentList "[0]"

Another way to do that would be to set the path property of the binding object:


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding
$Binding.Path = "[0]"

If you prefer not to use a datacontext, you can also declare a binding source in the binding object instead:


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding
$Binding.Path = "[0]"
$Binding.Source = $DataContext

Now I need to set the binding mode, which is oneway in the case:


$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay

And then finally I create the binding on the WPF element property. For the overload of the SetBinding() method, I need to provide the object I am binding to ($Hash.TextBox), then the property I am binding to (Text), then the binding object itself.


[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

Now I can handle the button click event, but this time instead of updating the “text” property directly, I simply update the datasource it is bound to. In this instance, the datasource is an integer not a string, so I can increment it directly.


# Add an event for the button click
$Hash.Button.Add_Click{
    $DataContext[0] ++
} 

And finally now I run the window and the result is the same: the number is incremented with every click of the button. Only the datasource is being updated, but the text property of the textbox is bound to it and updates automatically when notified by the observable collection.

gif3

Here is the full code (using the same WPF Window function):


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding # -ArgumentList "[0]"
$Binding.Path = "[0]"
$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

# Add an event for the button click
$Hash.Button.Add_Click{
    $DataContext[0] ++
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Multi-threading using the Dispatcher

Using data binding in this way can simplify the way you design your UI, and can also result in better UI performance, especially when using multiple threads because calling the dispatcher from another thread has a higher overhead. Using background threads is essential whenever you run any code that takes long enough that it will cause the UI to freeze and be unresponsive. Offloading that code to a background thread allows the UI to continue running happily in the main thread.

Does data binding work across threads? Yes it does, although when dealing with additional threads you need to consider the thread safety of the collections you are using as your datasource, as not all collections are inherently thread safe. There are some ways to achieve that though, but that’s for another post. Thread safety is important when you have multiple threads that may wish to access the same shared object at the same time.

For now, to illustrate data binding with a multi-threaded UI, lets first create the same WPF window but we will create and use a background thread to update the UI directly using the dispatcher. Each time the button is clicked, a new thread is created and the UI updated from that thread. The dispatcher allows us to queue work items for the window. This is a safe way to update the UI when using multiple threads as the dispatcher will manage each request in turn ensuring we do not suffer contention.

We are using the same WPF window function as previously. When the button is clicked, we read the current value of the texbox into a variable then increment it. We spin up a background thread and call the dispatcher on the window to update the UI. I’ve also included some code to cleanup completed threads using the Get-Runspace cmdlet (this is only available since PowerShell 5)


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Set textbox value
$Hash.TextBox.Text = "0"

# Add an event for the button click
$Hash.Button.Add_Click{

    # Cleanup any completed runspaces
    $FinishedRS = (Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"})
    If ($FinishedRS)
    {
        $FinishedRS.Dispose()
    }

    # Get current textbox value and increment
    [int]$CurrentValue = $Hash.TextBox.Text
    $CurrentValue ++

    # Create and invoke a background thread
    $ScriptBlock = {
        Param($Hash,$CurrentValue)
        $Hash.Window.Dispatcher.Invoke({
            $Hash.TextBox.Text = $CurrentValue
        })
    }
     $PowerShell = [PowerShell]::Create()
    [void]$PowerShell.AddScript($ScriptBlock)
    [void]$PowerShell.AddArgument($Hash)
    [void]$PowerShell.AddArgument($CurrentValue)
    $PowerShell.BeginInvoke()
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

# Cleanup runspaces
(Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"}).Dispose()

The result is that it works the same as before, but the response is somewhat slower. This is due to the additional overhead of creating a background thread and calling the dispatcher.

singlethreaddispatch

Multi-threading using data binding

Now lets do the same thing and use data binding. In this case, I only need to pass the datasource object to the background thread. When I update it in the background thread, the change is also reflected in the main UI thread.


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding -ArgumentList "[0]"
$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

# Add an event for the button click
$Hash.Button.Add_Click{

    # Cleanup any completed runspaces
    $FinishedRS = (Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"})
    If ($FinishedRS)
    {
        $FinishedRS.Dispose()
    }

    # Create and invoke a background thread
    $ScriptBlock = {
        Param($DataContext)
        $DataContext[0] ++
    }
     $PowerShell = [PowerShell]::Create()
    [void]$PowerShell.AddScript($ScriptBlock)
    [void]$PowerShell.AddArgument($DataContext)
    $PowerShell.BeginInvoke()
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

# Cleanup runspaces
(Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"}).Dispose()

The result this time is that even though there remains a small overhead in creating and managing a background thread, we get a much quicker response. Score!

singlethreaddb

Binding to the UI

Data binding can also be used to bind to items in the UI itself. In the following example, we have two textboxes. The one on the left is bound to a datasource that is incremented everytime the button is clicked. The one on the right is bound to the text property of the textbox on the left. The result is that both textboxes update incrementally, even though only one is bound to a datasource.

uibinding

This time, in the code, I have created a simple function to create a binding. When binding to a UI element, it is necessary toĀ set both the source and path properties of the binding.


function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "WPF Window"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create a button object
    $Button = New-Object System.Windows.Controls.Button
    $Button.Height = 85
    $Button.Width = [System.Double]::NaN # "auto" in XAML
    $Button.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $Button.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $Button.FontSize = 30
    $Button.Content = "Increment Me!"
    $Hash.Button = $Button

    # Create a textbox object
    $TextBox1 = New-Object System.Windows.Controls.TextBox
    $TextBox1.Name = "FirstTextBox"
    $TextBox1.Height = 85
    $TextBox1.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $TextBox1.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $TextBox1.FontSize = 30
    $TextBox1.BorderThickness = 0
    $Hash.TextBox1 = $TextBox1

    # Create a textbox object
    $TextBox2 = New-Object System.Windows.Controls.TextBox
    $TextBox2.Height = 85
    $TextBox2.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $TextBox2.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $TextBox2.FontSize = 30
    $TextBox2.BorderThickness = 0
    $Hash.TextBox2 = $TextBox2

    # Assemble the window
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $StackPanel.AddChild($TextBox1)
    $StackPanel.AddChild($TextBox2)

    $MainStackPanel = New-Object System.Windows.Controls.StackPanel
    $MainStackPanel.Margin = "5,5,5,5"
    $MainStackPanel.AddChild($StackPanel)
    $MainStackPanel.AddChild($Button)
    $Window.AddChild($MainStackPanel)
    $Hash.Window = $Window
}

Function Set-Binding {
    Param($Target,$Property,$Path,$Source)

    $Binding = New-Object System.Windows.Data.Binding
    $Binding.Path = $Path
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    If ($Source)
    {
        $Binding.Source = $Source
    }
    [void][System.Windows.Data.BindingOperations]::SetBinding($Target,$Property,$Binding)

    # Another way to do it...
    #[void]$Target.SetBinding($Property,$Binding)
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datasource and set the initial value
$DataSource = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$DataSource.Add([int]0)
$DataSource.Add([int]0)
$Hash.Window.DataContext = $DataSource

# Bind the first text box to the data source
Set-Binding -Target $Hash.TextBox1 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[0]"

# Bind the second text box to the first textbox, text property
Set-Binding -Target $Hash.TextBox2 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Source $Hash.TextBox1 -Path $([System.Windows.Controls.TextBox]::TextProperty)

# Event: Window Loaded
$Hash.Window.Add_Loaded{

    # Set the textbox widths to half the size of the button
    $Hash.TextBox1, $Hash.TextBox2 | foreach {
        $_.Width = $Hash.Button.ActualWidth / 2
    }

}

# Event: Button Clicked
$Hash.Button.Add_Click{

    # Increment the data source value
    $DataSource[0] ++

}

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Learn your times tables!

Finally, my last example is a simple tool to help you learn your multiplication tables (you know you need reminding šŸ˜‰ ). Here, the 3 text boxes with numbers are all bound to a datasource. When a button is clicked, the value is incremented or decremented in the datasource, and this is reflected in the UI by the binding. The result is then calculated based on the new values, and this is done by handling the TextChanged event on the first 2Ā text boxes. Again, the UI is never being updated directly, only the datasource is being changed, but the UI also updates because of the bindings.

multipier

The code for this is a bit long because I am creating the entire WPF window the ‘old-school’ way – in code, rather than in XAML. In practice, it is much better to create the UI definition in XAML, and then you will use a combination of XAML and PowerShell code to manage the UI.


function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "Multiplication Tables"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create the first value textbox
    $TextBoxProperties = @{Height = 85; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True}
    $TextBox1 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox1.$($_.Name) = $_.Value
    }
    $Hash.TextBox1 = $TextBox1

    # Create the "x" textbox
    $TextBoxProperties = @{Height = 85; Width = 40; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; Text = "x"; IsReadOnly = $True}
    $TextBox2 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox2.$($_.Name) = $_.Value
    }
    $Hash.TextBox2 = $TextBox2

     # Create the second value textbox
    $TextBoxProperties = @{Height = 85; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True }
    $TextBox3 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox3.$($_.Name) = $_.Value
    }
    $Hash.TextBox3 = $TextBox3

    # Create the "=" textbox
    $TextBoxProperties = @{Height = 85; Width = 40; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; Text = "="; IsReadOnly = $True }
    $TextBox4 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox4.$($_.Name) = $_.Value
    }
    $Hash.TextBox4 = $TextBox4

    # Create the calculated value textbox
    $TextBoxProperties = @{Height = 85; Width = 80; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True}
    $TextBox5 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox5.$($_.Name) = $_.Value
    }
    $Hash.TextBox5 = $TextBox5

    # Create the first "+" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "+"; Margin = "5,0,0,0"}
    $Button1 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button1.$($_.Name) = $_.Value
    }
    $Hash.Button1 = $Button1

    # Create the first "-" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "-"; Margin = "5,0,0,0"}
    $Button2 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button2.$($_.Name) = $_.Value
    }
    $Hash.Button2 = $Button2

    # Create the second "+" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "+"; Margin = "28,0,0,0"}
    $Button3 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button3.$($_.Name) = $_.Value
    }
    $Hash.Button3 = $Button3

    # Create the second "-" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "-"; Margin = "5,0,0,0"}
    $Button4 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button4.$($_.Name) = $_.Value
    }
    $Hash.Button4 = $Button4

     # Create the reset button
    $ButtonProperties = @{Height = 30; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "Reset"; Margin = "40,0,0,0"}
    $Button5 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button5.$($_.Name) = $_.Value
    }
    $Hash.Button5 = $Button5

    # Assemble the first stackpanel
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $TextBox1, $TextBox2, $TextBox3, $TextBox4, $TextBox5 | foreach {
        $StackPanel.AddChild($_)
    }

    # Assemble the second stackpanel
    $StackPanel2 = New-Object System.Windows.Controls.StackPanel
    $StackPanel2.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $Button1, $Button2, $Button3, $Button4, $Button5 | foreach {
        $StackPanel2.AddChild($_)
    }

    # Assemble the window
    $MainStackPanel = New-Object System.Windows.Controls.StackPanel
    $MainStackPanel.Margin = "5,5,5,5"
    $MainStackPanel.AddChild($StackPanel)
    $MainStackPanel.AddChild($StackPanel2)
    $Window.AddChild($MainStackPanel)
    $Hash.Window = $Window
}

Function Set-Binding {
    Param($Target,$Property,$Path,$Source)

    $Binding = New-Object System.Windows.Data.Binding
    $Binding.Path = $Path
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    If ($Source)
    {
        $Binding.Source = $Source
    }
    [void][System.Windows.Data.BindingOperations]::SetBinding($Target,$Property,$Binding)

    # Another way to do it...
    #[void]$Target.SetBinding($Property,$Binding)
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datasource and set the initial values
$DataSource = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$DataSource.Add([int]1)
$DataSource.Add([int]1)
$DataSource.Add([int]1)
$Hash.Window.DataContext = $DataSource

# Bind the value text boxes to the data source
Set-Binding -Target $Hash.TextBox1 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[0]"
Set-Binding -Target $Hash.TextBox3 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[1]"
Set-Binding -Target $Hash.TextBox5 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[2]"

# Events: Button Clicks
$Hash.Button1.Add_Click{
    # Increment
    $DataSource[0] ++
}

$Hash.Button2.Add_Click{
    # Decrement
    $DataSource[0] --
}

$Hash.Button3.Add_Click{
    # Increment
    $DataSource[1] ++
}

$Hash.Button4.Add_Click{
    # Decrement
    $DataSource[1] --
}

$Hash.Button5.Add_Click{
    # Reset Values to 1
    $DataSource[0] = [int]1
    $DataSource[1] = [int]1
}

# Events: TextBox values changed
$Hash.TextBox1, $Hash.TextBox3, $hash.TextBox5 | foreach {
    $_.Add_TextChanged{
        # Calculate
        $DataSource[2] = $DataSource[0] * $DataSource[1]
    }
}

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Conclusion

Data binding can get much more complex than this including two-way bindings, priority binding and multiple bindings, but hopefully these examples will whet your appetite to explore it further. When using data binding in your UI you are taking a step towards implementing the MVVM, a kind of best-practice concept for designing UI applications. Whether you really need to use data binding or not depends a lot on the type of UI you are creating and what it does, but certainly consider taking advantage of it to create a more efficient and logically designed PowerShell UI.

Have fun!

6 thoughts on “PowerShell DeepDive: WPF, Data Binding and INotifyPropertyChanged

  1. Awesome resource Trevor. I would like an example of seeing a value change without having to refresh. ie: monitoring a service. I want to UI to automatically show the update and not have a button clicked to show the changes. Would appreciate an example. Boe Prox has an example, but it doesn’t use INotifyPropertyChanged. Frustrating to have to click refresh and I don’t wish to use a loop to have it refresh. Thanks for including RunSpaces in your examples.

    Cheers!

    1. To handle auto refresh I usually use a timer. There are a couple of timers available in .Net. If you are updating the UI directly you can use the dispatcher timer which works well with WPF because it adds the UI update work to the dispatcher queue to avoid any contention. If you are using databinding, you could use any timer since you simply update your datasource and the binding will dispatch the UI update work. Here’s a quick example of using the dispatcher timer:
      $TimerCode = {
      # Code here
      }
      $DispatcherTimer = New-Object -TypeName System.Windows.Threading.DispatcherTimer
      $DispatcherTimer.Interval = [TimeSpan]::FromSeconds(10)
      $DispatcherTimer.Add_Tick($TimerCode)
      $DispatcherTimer.Start()

      1. Awesome Trevor. Thanks for the reply. Something for me to work towards.
        As per Boe’s example, the binding is in the XAML, then in a some code
        $button2.Add_Click({
        $observableCollection | ForEach {
        $_.Refresh()
        }
        [System.Windows.Data.CollectionViewSource]::GetDefaultView( $Listbox.ItemsSource ).Refresh()
        })

        From that I can’t tell how the service status is known to have changed with re-running the query. Sounds like it might be holding an object in memory for each collection.
        REF: https://learn-powershell.net/2012/12/08/powershell-and-wpf-listbox-part-2datatriggers-and-observablecollection/

        Anyway, thanks again.

  2. Thank you for putting this together.

    I’m trying to get it to work with DataGrid and not having any luck. Would really appreciate any feedback! I’m trying to refactor an RPG I made away from using the invoke.dispatcher method to update controls so really excited to get this working.

    $ClassWIndow = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
    $class = Import-Csv -Path “D:\Spirits\Moonlight\classes.csv”
    $data = [pscustomobject]$class
    $ClassWIndow.Add($data)
    $syncHash.classwindow = $ClassWIndow
    $syncHash.spiritclassgrid.DataContext = $synchash.classwindow

    $Binding = New-Object System.Windows.Data.Binding -ArgumentList “[0]”
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    [void][System.Windows.Data.BindingOperations]::SetBinding($syncHash.spiritclassgrid,[System.Windows.Controls.DataGrid]::ItemsSource, $Binding)

  3. Hello Trevor, I’m having a hard time trying to figure out how to bind a listview itemssource to a variable containing multiple pscustomobject for items which have multiple elements (one for each column in the listview). If you could point me a way how to do it, it would be greatly appreciated. Do I need to bind a path for each item? What is the binding source? Thank you!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.