In the latest article about WPF and PowerShell, I will talk about using the ListBox control along with a little bit of Expander and some Data Binding when creating a GUI. I know that more people want to see Data binding and some examples of using it, but I feel it is better reserved for working with a GridView which will be discussed in a future article.
With a Listbox, you can add one or more items to it and then select one or more items that can be used for another piece of your GUI. As well as adding items, they can be removed as well.
Adding and Removing Items
The first example will show you how you can add and remove items from a ListBox as well as another method to add a collection of items from a text document.
#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" ResizeMode="NoResize" Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> <StackPanel > <TextBox x:Name="readonlyTextBox" IsReadOnly="True" TextWrapping="Wrap"> Type something into the text box below and click Add to update the listbox. </TextBox> <TextBox x:Name="inputTextBox" /> <Button x:Name="addButton" Content="Add"/> <Button x:Name="removeButton" Content="Remove Selected Item/s"/> <ListBox x:Name="listbox" MinHeight = "50" AllowDrop="True" SelectionMode="Extended"/> </StackPanel> </Window> "@ $reader=(New-Object System.Xml.XmlNodeReader $xaml) $Window=[Windows.Markup.XamlReader]::Load( $reader ) #Connect to Controls $inputTextBox = $Window.FindName('inputTextBox') $addButton = $Window.FindName('addButton') $listbox = $Window.FindName('listbox') $removeButton = $Window.FindName('removeButton') #Events $addButton.Add_Click({ If ((-NOT [string]::IsNullOrEmpty($inputTextBox.text))) { $listbox.Items.Add($inputTextBox.text) $inputTextBox.Clear() } }) $removeButton.Add_Click({ While ($listbox.SelectedItems.count -gt 0) { $listbox.Items.RemoveAt($listbox.SelectedIndex) } }) $listbox.Add_Drop({ (Get-Content $_.Data.GetFileDropList()) | ForEach { $listbox.Items.Add($_) } }) $Window.ShowDialog() | Out-Null
So nothing we haven’t seen before with a StackPanel and some buttons and textboxes. The ListBox is visible so that way we can take advantage of using the AllowDrop property and the Drop event that occurs when I drag and drop a text document on top of the listbox. For instance, I will drag a text file named Data.txt shown below that contains a list of items. Once dragged into the ListBox area, it will populate the ListBox with the information that was in the document. This is done using the ListBox.Items.Add() method.
You can additional items to the list by typing something in the TextBox and clicking the Add button. The Click event will take the text from the TextBox and add it into the existing ListBox. The same method used to add items with the Drag is also used with this.
To remove items from a ListBox, you can use the ListBox.Items.RemoveAt() method which requires the index of the selected item. For multiple items selected, I chose to use a While loop to handle each selected item and remove it. One thing to note that by default, you cannot select multiple items in a ListBox. To enable this, you have to set the SelectionMode property of the ListBox to Extended.
While ($listbox.SelectedItems.count -gt 0) { $listbox.Items.RemoveAt($listbox.SelectedIndex) }
Show Selected Item in a Different Control
Being able to display the selected item in another control scan be done a variety of ways. I will show you two of those ways here with a couple of examples. The first approach is to use the SelectionChanged event that is available with the ListBox. Every time that you change the selection, the event will fire and take the current item’s name and place it in the textbox.
#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" ResizeMode="NoResize" Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> <StackPanel > <TextBox x:Name="readonlyTextBox" IsReadOnly="True" TextWrapping="Wrap"> Type something into the text box below and click Add to update the listbox. </TextBox> <TextBox x:Name="inputTextBox" /> <Button x:Name="addButton" Content="Add"/> <ListBox x:Name="listbox" /> <TextBox IsReadOnly="True" TextWrapping="Wrap" Text = "Selected Item will appear below:"/> <TextBox x:Name="readonlyOutputBox" IsReadOnly="True" TextWrapping="Wrap"/> </StackPanel> </Window> "@ $reader=(New-Object System.Xml.XmlNodeReader $xaml) $Window=[Windows.Markup.XamlReader]::Load( $reader ) #Connect to Controls $inputTextBox = $Window.FindName('inputTextBox') $addButton = $Window.FindName('addButton') $readonlyOutputBox = $Window.FindName('readonlyOutputBox') $listbox = $Window.FindName('listbox') #Events $addButton.Add_Click({ If ((-NOT [string]::IsNullOrEmpty($inputTextBox.text))) { $listbox.Items.Add($inputTextBox.text) $inputTextBox.Clear() } }) #Alternate way to handle the selection change $listbox.Add_SelectionChanged({ $readonlyOutputBox.Text = ("Selected Item: {0}" -f $listbox.SelectedItem) }) $Window.ShowDialog() | Out-Null
The next approach to this is the Binding approach that can be coded into the XAML at the beginning and automatically takes care of handling the selection change and updating the TextBox. The code below is for the most part like the previous except that I have no need to create an event to handle the changing selections. Here is an example of the GUI using Binding.
#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" ResizeMode="NoResize" Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> <StackPanel > <TextBox x:Name="readonlyTextBox" IsReadOnly="True" TextWrapping="Wrap"> Type something into the text box below and click Add to update the listbox. </TextBox> <TextBox x:Name="inputTextBox" /> <Button x:Name="addButton" Content="Add"/> <ListBox x:Name="listbox" /> <TextBox IsReadOnly="True" TextWrapping="Wrap" Text = "Selected Item will appear below:"/> <TextBox x:Name="readonlyOutputBox" IsReadOnly="True" TextWrapping="Wrap" Text = "{Binding ElementName =listbox,Path =SelectedItem, Mode=OneWay}"/> </StackPanel> </Window> "@ $reader=(New-Object System.Xml.XmlNodeReader $xaml) $Window=[Windows.Markup.XamlReader]::Load( $reader ) #Connect to Controls $inputTextBox = $Window.FindName('inputTextBox') $addButton = $Window.FindName('addButton') $readonlyOutputBox = $Window.FindName('readonlyOutputBox') $listbox = $Window.FindName('listbox') #Events $addButton.Add_Click({ If ((-NOT [string]::IsNullOrEmpty($inputTextBox.text))) { $listbox.Items.Add($inputTextBox.text) $inputTextBox.Clear() } }) $Window.ShowDialog() | Out-Null $Window.ShowDialog() | Out-Null
The key piece of code that does the SelectedItem to TextBox Binding is here:
<TextBox x:Name="readonlyOutputBox" IsReadOnly="True" TextWrapping="Wrap" Text = "{Binding ElementName =listbox,Path =SelectedItem, Mode=OneWay}"/>
The requirements for the Binding to work on the TextBox in this situation is that we need to know the name of the control (this means you have to give the control a name using x:Name=’name’) and the property that we will be binding to on that control. I also chose One-Way mode because I only care about what happens on the ListBox, not what happens with the TextBox. By encasing all of the Binding attributes in curly brackets, the Text property of the TextBox does not interpret this as text and actually Binds to the ListBox.SelectedItem property.
Cmdlet Helper V2
In my article working with TextBoxes, I wrote a little cmdlet viewer that worked OK, but you couldn’t select anything. This changes with my next version using a ListBox. With this version you can select each cmdlet and then view the help for that cmdlet.
Clicking Show Help will bring up the help information for the selected cmdlet.
Add-Type -AssemblyName PresentationFramework #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="Powershell Cmdlet Help Window" WindowStartupLocation = "CenterScreen" ResizeMode="NoResize" Width = "313" Height = "425" ShowInTaskbar = "True" Background = "lightgray"> <StackPanel > <TextBox Text='Type in this textbox below and cmdlets will display. Click Show Help buttong to view help.' IsReadOnly = "True" TextWrapping = "Wrap"/> <TextBox x:Name="InputBox" Height = "30" /> <Button x:Name = "helpbutton" Content = "Show Help" /> <ListBox x:Name="listbox" Height = "300" DisplayMemberPath = 'Name'/> </StackPanel> </Window> "@ $reader=(New-Object System.Xml.XmlNodeReader $xaml) $Window=[Windows.Markup.XamlReader]::Load( $reader ) #Connect to Controls $listbox = $Window.FindName('listbox') $InputBox = $Window.FindName('InputBox') $helpbutton = $Window.FindName('helpbutton') #Events $InputBox.Add_TextChanged({ $cmdlets = @(Get-Command -CommandType Cmdlet -Name ("{0}*" -f $InputBox.Text)) $listbox.itemsSource = $cmdlets }) $helpbutton.Add_Click({ (Get-Help $listbox.SelectedItem -Full | Out-String) -split "\r" | Out-GridView -Title ("{0}" -f $listbox.SelectedItem ) }) $Window.Add_Activated({ $InputBox.Focus() }) $Window.ShowDialog() | Out-Null
If you notice, I was able to supply the entire object that was returned with the Get-Command cmdlet and it only showed me the Name in the ListBox. This is another example of Binding and one that I will show another example of in the next section. It is also important to note that instead of using the Items.Add() method, I am now using the ItemsSource property and adding the collection of objects to that instead.
One More Binding Example
The next Binding example with the ListBox is by taking a collection of objects returned by Get-Service and only displaying the DisplayName in the ListBox. This is done by defining the DisplayMemberPath property to look for only the DisplayName of whatever object is being saved to it. The result is that no matter what object I throw at it, as long as there is a Displayname property, it will always add that to the Listbox. The following example shows what happens when you use the DisplayMemberPath binding and when you don’t.
With Binding:
Without Binding:
You are probably thinking that the GUI looks a little different and you would be right! The reason is that in PowerShell V3, the Binding isn’t necessary to handle the DisplayName output on the ListBox, but it still need to be done with PowerShell V2. In this case, I only had PowerShell V2 still loaded on my Domain Controller running Server 2003.
Another thing that you might have noticed was the use of the Expander control. This nice control can be used to wrap other controls in it and then allows you to hide (collapse) or show (expand) a control as needed either manually or programmatically.
#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"> With Binding </TextBox> <Button x:Name="button1" Content="Get Services"/> <Expander IsExpanded="True"> <ListBox x:Name="listbox" DisplayMemberPath = "DisplayName"/> </Expander > <TextBox IsReadOnly="True" TextWrapping="Wrap"> Without Binding </TextBox> <Button x:Name="button2" Content="Get Services"/> <Expander IsExpanded="True"> <ListBox x:Name="listbox1"/> </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') $listbox = $Window.FindName('listbox') $listbox1 = $Window.FindName('listbox1') #Events $button1.Add_Click({ $services = Get-Service $listbox.ItemsSource = $services }) $button2.Add_Click({ $services = Get-Service $listbox1.ItemsSource = $services }) $Window.ShowDialog() | Out-Null
Alternating Row Colors
The final trick to show with ListBoxes are how you can set alternating colors for each row. That way it is easier on the eyes to read the data in a ListBox.
All of the work for this is done in the XAML code up front. We first have to setup the Resource that will be applied later to the ListBox control. I decide to do this on the StackPanel since it is the parent control. If you try to both create the Resource and apply it to the ListBox, it will not work. The initial background and foreground property is set and then you can set the Alternating background and foreground property based on the AlternationIndex.
<StackPanel.Resources> <Style x:Key="AlternatingRowStyle" TargetType="{x:Type Control}" > <Setter Property="Background" Value="LightGray"/> <Setter Property="Foreground" Value="Black"/> <Style.Triggers> <Trigger Property="ItemsControl.AlternationIndex" Value="1"> <Setter Property="Background" Value="White"/> <Setter Property="Foreground" Value="Black"/> </Trigger> </Style.Triggers> </Style> </StackPanel.Resources>
Now that this has been created, we can set it to the ListBox that will automatically apply the change to the ListBox when you begin adding data. On the ListBox, I have to set the AlternationCount to 2 and I set the ItemContainerStyle property to the Resource that I created on the StackPanel, named ‘AlternatingRowStyle’. With that, everything will now have alternating colors in the ListBox as shown earlier in this section.
<ListBox x:Name="listbox" DisplayMemberPath = "DisplayName" AlternationCount="2" ItemContainerStyle="{StaticResource AlternatingRowStyle}"/>
Full code is here to view:
#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 > <StackPanel.Resources> <Style x:Key="AlternatingRowStyle" TargetType="{x:Type Control}" > <Setter Property="Background" Value="LightGray"/> <Setter Property="Foreground" Value="Black"/> <Style.Triggers> <Trigger Property="ItemsControl.AlternationIndex" Value="1"> <Setter Property="Background" Value="White"/> <Setter Property="Foreground" Value="Black"/> </Trigger> </Style.Triggers> </Style> </StackPanel.Resources> <TextBox IsReadOnly="True" TextWrapping="Wrap"> With Binding </TextBox> <Button x:Name="button1" Content="Get Services"/> <Expander IsExpanded="True"> <ListBox x:Name="listbox" DisplayMemberPath = "DisplayName" AlternationCount="2" ItemContainerStyle="{StaticResource AlternatingRowStyle}"/> </Expander > <TextBox IsReadOnly="True" TextWrapping="Wrap"> Without Binding </TextBox> <Button x:Name="button2" Content="Get Services"/> <Expander IsExpanded="True"> <ListBox x:Name="listbox1" AlternationCount="2" ItemContainerStyle="{StaticResource AlternatingRowStyle}"/> </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') $listbox = $Window.FindName('listbox') $listbox1 = $Window.FindName('listbox1') #Events $button1.Add_Click({ $services = Get-Service $listbox.ItemsSource = $services }) $button2.Add_Click({ $services = Get-Service $listbox1.ItemsSource = $services }) $Window.ShowDialog() | Out-Null
There is much more available to ListBoxes and this article really just scratches what you can do with them. But I hope that you have found some nice uses for ListBoxes based on this article. Remember, if you have any questions, just let me know and I will do my best to answer them!
I have read your “Powershell and WPF” series and find them most useful. I have looked for information on using the Combobox and most of the articles on the net are using C# instead of Powershell.
Do you have, or can recommend an article for PowerShell and WPF: Combobox? As I understand it, it requires additional binding.
Thanks,
Mark
In the first couple of examples, you use this validation code: If (-NOT [string]::IsNullOrEmpty($inputTextBox.text)) {blah}
It took me a few seconds to read and understand that code. Is there some advantage to using this code instead of the more traditional: if ($inputTextBox.text.length -gt 0) {blah} ?
This DataBinding thing is still a bit magical to me. My first attempts at using it instead of a foreach {$listbox.Items.Add($_)} showed nothing at all in my list, so I’m re-reading this article s-l-o-w-l-y. I’m eager to read your deeper exploration of DataBinding in the DataGrid control article you mentioned.
In PowerShell 5.0 (Win10) there’s no difference showing between ‘With Binding’ and ‘Without Binding’
Hmmm… In PowerShell 5 on Win7, I see that the “One More Binding Example” works differently: With Binding works as expected. Without binding (not specifying a value for DisplayMemberPath), I see the services’ Name property by default.
Pingback: Building a Clipboard History Viewer Using PowerShell | Learn Powershell | Achieve More
never mind…good to see that it still waiting for moderation… i am deleting this link from my favorites…
Sorry, I had a flurry of comments (some spam, some not) that I have been trying to work through and determine if they are legitimate or not. Your comment was about 3 days ago and I hadn’t gotten to it just yet. As far as the question goes, I will have to take some time to research and figure it out (besides other things that I am working on).
I have 3 listboxes ,lets call them listbox 2, 3 and 4. Listbox 2 gets Computers from a button click event. Then I have created >> and << buttons to move them between 2 and 3. Now when i select multiple computers from 3 and click another button called "check" – it should check if the servers that I selected in 3 are pingable or not and move them to 4 if ping is success, while at the same time remove them from 3. Those that are not-pingable should stay back at 3… Please help
Pingback: PowerShell and WPF: ListBox Part 2–DataTriggers and ObservableCollection | Learn Powershell | Achieve More