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
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
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.
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.
