PowerShell and WPF: ListBox Part 2–DataTriggers and ObservableCollection

I wanted to share a couple of more things when working with ListBoxes that I did not cover in my previous article. The items that I will cover are:

  • Using an Observable Collection to handle data in the ListBox
  • Using DataTriggers to change the row color based on the object property

Using an Observable Collection

An observable collection is a specialized collection that accepts a specific type of object (like using generics) and has an event called CollectionChanged that notifies whenever the collection changes. When using this through the console, you need to use Register-ObjectEvent to handle this type of change. The following example will take you through creating the collection and then using Register-ObjectEvent to handle whenever the collection changes as well as showing what happens when you attempt to add an object that is not valid for the collection (in this case I am only allowing Integers).

#Create an observable collection that only accepts integers
$observableCollection = New-Object System.Collections.ObjectModel.ObservableCollection[int]

#Show that it only accepts integers
$observableCollection.Add("Test") #This will fail
$observableCollection.Add(2) #This will succeed

#Set up an event watcher
Register-ObjectEvent -InputObject $observableCollection -EventName CollectionChanged -Action {
    $Global:test = $Event
    Switch ($test.SourceEventArgs.Action) {
        "Add" {
            $test.SourceEventArgs.NewItems | ForEach {
                Write-Host ("{0} was added" -f $_) -ForegroundColor Yellow -BackgroundColor Black
            }
        }
        "Remove" {
            $test.SourceEventArgs.OldItems | ForEach {
                Write-Host ("{0} was removed" -f $_) -ForegroundColor Yellow -BackgroundColor Black
            }
        }
        Default {
            Write-Host ("The following action occurred: {0}" -f $test.SourceEventArgs.Action) -ForegroundColor Yellow -BackgroundColor Black
        }
    }
}

#Add another integer and the event will kick off
$observableCollection.Add(10) | Out-Null

#Remove an integer
$observableCollection.Remove(2) | Out-Null

#Clear the collection
$observableCollection.Clear()

Get-EventSubscriber | Unregister-Event                                                      
Get-Job | Remove-Job -Force      

 

image

As you can see, my initial attempt at adding a string to this collection failed as it was not an Integer. After creating the event watcher, you can see what happens when I add, remove and clear the collection.

Using this with a Listbox in a GUI is actually much simpler than this. All you have to do is bind the collection to the ItemsSource property of the Listbox and it will handle the rest. No need for creating an event to handle whenever the collection changes. All of that is handled by the collection automatically and the changes are made to the ListBox. Very cool stuff! The example below shows it in action.

#Build the GUI
[xml]$xaml = @"
<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen" 
    Width = "313" Height = "800" ShowInTaskbar = "True" Background = "lightgray"> 
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <StackPanel >
            <TextBox  IsReadOnly="True" TextWrapping="Wrap">
                Type something and click Add
            </TextBox>
            <TextBox x:Name = "inputbox"/>
            <Button x:Name="button1" Content="Add"/>
            <Button x:Name="button2" Content="Remove"/>
            <ListBox x:Name="listbox" SelectionMode="Extended" />
        </StackPanel>
    </ScrollViewer >
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )
 
#Connect to Controls
$inputbox = $Window.FindName('inputbox')
$button1 = $Window.FindName('button1')
$button2 = $Window.FindName('button2')
$listbox = $Window.FindName('listbox')

$Window.Add_Activated({
    #Have to have something initially in the collection
    $Script:observableCollection = New-Object System.Collections.ObjectModel.ObservableCollection[string]
    $listbox.ItemsSource = $observableCollection
    $inputbox.Focus()
})
 
#Events
$button1.Add_Click({
     $observableCollection.Add($inputbox.text)
     $inputbox.Clear()
})
$button2.Add_Click({
    ForEach ($item in @($listbox.SelectedItems)) {
        $observableCollection.Remove($item)
    }
}) 
$Window.ShowDialog() | Out-Null
     $inputbox.Clear()
})
$button2.Add_Click({
    ForEach ($item in @($listbox.SelectedItems)) {
        $observableCollection.Remove($item)
    }
}) 
$Window.ShowDialog() | Out-Null

 

image

This behaves exactly like the Listbox example I showed in the previous Listbox article. The exception is that instead of writing to the Items property of the Listbox, everything happens with the ObservableCollection and that object notifies the Listbox to update its collection.

DataTrigger with a Listbox

The next section shows how you can use DataTriggers on a Listbox to automatically change the row color based on a given property. The results will look something like the following example.

image

While it does not actually show the property, the list of services in the Listbox are color coded by the Status of the service. Red means the service is stopped and Green is running. I use data binding to only show the DisplayName in the Listbox even though I actually use the entire service object using the DisplayMemberPath property.

Setting the up DateTrigger requires some more XAML code that is added to the Listbox to determine what part of the Listbox will be looked at (ListBoxItem in this case). This code is used on the ItemContainerStyle property of the Listbox and then proceeds to set up the Triggers that then uses the DataTrigger for each property and action. The XAML code below shows how I setup the Triggers for the Status to look at either Running or Stopped and how it sets the background color.

<ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding Path=Status}" Value="Running">
                <Setter Property="ListBoxItem.Background" Value="Green" />
            </DataTrigger>
            <DataTrigger Binding="{Binding Path=Status}" Value="Stopped">
                <Setter Property="ListBoxItem.Background" Value="Red" />
            </DataTrigger>                                
        </Style.Triggers>
    </Style>
</ListBox.ItemContainerStyle>

Again, we have to bind the path of the property to Status so the trigger will know where to look to see if the service is either Running or Stopped and then using the Setter property to change the background color to Red or Green as shown in the picture above. Full code that does this is below.

#Build the GUI
[xml]$xaml = @"
<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen" 
    Width = "313" Height = "800" ShowInTaskbar = "True" Background = "lightgray"> 
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <StackPanel >
            <TextBox  IsReadOnly="True" TextWrapping="Wrap">
                Click button to get Services
            </TextBox>
            <TextBox x:Name = "inputbox"/>
            <Button x:Name="button1" Content="Get-Services"/>
            <Button x:Name="button2" Content="Refresh"/>
            <Expander IsExpanded="True">
                <ListBox x:Name="listbox" SelectionMode="Extended" DisplayMemberPath="DisplayName">            
                    <ListBox.ItemContainerStyle>
                        <Style TargetType="{x:Type ListBoxItem}">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=Status}" Value="Running">
                                    <Setter Property="ListBoxItem.Background" Value="Green" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Path=Status}" Value="Stopped">
                                    <Setter Property="ListBoxItem.Background" Value="Red" />
                                </DataTrigger>                                
                            </Style.Triggers>
                        </Style>
                    </ListBox.ItemContainerStyle>
                </ListBox>
            </Expander >
        </StackPanel>
    </ScrollViewer >
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )
 
#Connect to Controls
$button1 = $Window.FindName('button1')
$button2 = $Window.FindName('button2')
$Global:listbox = $Window.FindName('listbox')

$Window.Add_Loaded({
    #Have to have something initially in the collection
    $Global:observableCollection = New-Object System.Collections.ObjectModel.ObservableCollection[System.Object]
    $listbox.ItemsSource = $observableCollection
})
 
#Events
$button1.Add_Click({
    $observableCollection.Clear()
    Get-Service | ForEach {
        $observableCollection.Add($_)
    }
})
$button2.Add_Click({
    $observableCollection | ForEach {
        Write-Host ("Refreshing {0}" -f $_.displayName) -BackgroundColor Black -ForegroundColor Yellow    
        $_.Refresh()
    }    
    [System.Windows.Data.CollectionViewSource]::GetDefaultView( $Listbox.ItemsSource ).Refresh()
}) 
$Window.ShowDialog() | Out-Null

As you have seen, the ideas of techniques that you can use with Listboxes are many. Using Observable Collections and/or DataTriggers are just another tool in building a GUI to make it more usable and user friendly based on your requirements.

This entry was posted in GUI, powershell, WPF and tagged , , , , . Bookmark the permalink.

6 Responses to PowerShell and WPF: ListBox Part 2–DataTriggers and ObservableCollection

  1. Daniel Petcher says:

    OK, I think I understand the System.Collections.ObjectModel.ObservableCollection structure as something similar to the array of String objects that I would have used for this purpose if I hadn’t read your article.

    But I can’t quite wrap my head around the line that says:
    [System.Windows.Data.CollectionViewSource]::GetDefaultView( $Listbox.ItemsSource ).Refresh()

    You’re calling the GetDefault View static method on the CollectionViewSource (whatever THAT kind of object might be) related to the $Listbox’s ItemSource. On the resulting object, you’re calling a Refresh method. That has lost me in jargon, though. Does this method somehow call a new Get-Service ? … or does it get the same results via a different procedure?

    Oh, wait! You’re binding on $Listbox to an “Observable Collection” of “System.Objects” ….
    “System.Object” sounds very generic. Does it have a .Refresh() method?

    I’m hoping to combine DataBinding with the RunSpaces techniques you’ve described, but I don’t understand either topic very deeply, yet. Thanks again for your efforts to translate these complex topics for an old Systems Administrator who is working to expand his career horizons beyond clicking “Next, Next, Next, Finish”
    🙂

  2. EM says:

    This is awesome Boe. I’ve expanded on this concept for checking multiple servers and catered for the different status’ of the services. See Below..

    <ListBox.ItemContainerStyle>

    <Style.Triggers>

    </Style.Triggers>

    </ListBox.ItemContainerStyle>

    Unfortunately, I can only ‘hard code’ a listbox for each server I want to check. This means I need a custom version for each environment I want this for. ie: Env 1 has 10 servers, Env2 has six, and the third has eight. I want to dynamically generate the above code for the count of servers. So, you run a query and if it returns 6 servers, you then spawn six of these listboxes. All I can think of right now is to have the maximum coded up, but hide them and after running a query, that that quantity un-hide the listboxes. Surely there’s an easier way?

  3. Pingback: Building a Clipboard History Viewer Using PowerShell | Learn Powershell | Achieve More

  4. Pingback: PowerShell and Events: Object Events | Learn Powershell | Achieve More

  5. JustDaft says:

    Many thanks for this Boe, I have been using PowerShell and WPF for a while now, but I always learn something new from your posts.

    Billy Westbury

    • Boe Prox says:

      Thanks, Billy! I am glad that you find new things from my articles. There is just so much with WPF that I hope to cover the basics as well as throwing in a couple of other unique things to help others out.

Leave a comment