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.

Posted in GUI, powershell, WPF | Tagged , , , , | 6 Comments

Vote On the PoshWSUS Command Name Change

In an effort to avoid a naming collision with the new WSUS PowerShell module, I am looking to rename the commands that I have in PoshWSUS to something else. I figure a poll would be useful in this instance to list my ideas and receive any other feedback.

The original command is something like the following: Get-WSUSClient

My thoughts on what to change this to would be either Get-PSWSUSClient or Get-PoshWSUS.

Thanks for taking the time to vote!

Posted in News, powershell | Tagged , , , | 3 Comments

PowerShell and WPF: ListBox

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

image

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.

image

 

image

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.

image

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

image

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

image

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.

image

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

image

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:

image

Without Binding:

image

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.

image

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!

Posted in powershell, WPF | Tagged , , , , , | 9 Comments

Use a PowerShell Logon Script To Update Printer Mappings

I was recently asked to come up with a PowerShell solution to re-map all of the printers in our domain from a 32 bit print server to a print server that was 64 bit. This had to be done at logon which meant that this needed to be a logon script. Fortunately we are running Windows 7 which means that this is a perfect candidate for a PowerShell logon script!

The requirements of this logon script are:

  • Requires no interaction by the user
  • Maps the same printer on the new server that was on the old server (the migrated servers kept the same printer name Old: \\Server1\B24-R New: \\Server2\B24-R)
  • Remove the old printer mapping
  • Write a logfile of the removal, adding and any errors encountered to a shared directory

The first requirement is pretty much a no-brainer. If you are writing a logon script, it had better not require any effort by the user. Luckily, we were able to take advantage of the feature in Windows 2008 R2 that allowed your Login Scripts for Group Policy to specify a PowerShell script as the logon script. This will automatically make sure that the script runs with the Bypass execution policy and runs it in a hidden window.

image

For the rest of the requirements, I will first show you the code and then explain how I met each requirement below.

Param (
    $newPrintServer = "Server2",
    $PrinterLog = "\\LogSVR\PrintMigration$\PrintMigration.csv"
)
<#
    #Header for CSV log file:
    "COMPUTERNAME,USERNAME,PRINTERNAME,RETURNCODE-ERRORMESSAGE,DATETIME,STATUS" | 
        Out-File -FilePath $PrinterLog -Encoding ASCII
#>

This part is the initial setup for the parameters to include the new print server and the log that will be used to track the mappings (meeting the requirement of the logging). The comment block is the code that should be ran first to setup the log file. Note: This script is assuming that you are running PowerShell V2. If running V3, you can skip this and actually update the Out-File commands with Export-Csv with the –Append parameter.

Try {
    Write-Verbose ("{0}: Checking for printers mapped to old print server" -f $Env:USERNAME)
    $printers = @(Get-WmiObject -Class Win32_Printer -Filter "SystemName='\\\\Server1'" -ErrorAction Stop)
    
    If ($printers.count -gt 0) {        
        ForEach ($printer in $printers) {
            Write-Verbose ("{0}: Replacing with new print server name: {1}" -f $Printer.Name,$newPrintServer)
            $newPrinter = $printer.Name -replace "Server1",$newPrintServer  
            $returnValue = ([wmiclass]"Win32_Printer").AddPrinterConnection($newPrinter).ReturnValue   

I first verify that there are still old print server mappings prior to proceeding with the logon script. Once that is done, I then iterate through each old printer mapping and do a –Replace of the old print server with a new one. This will then be used to attempt to map to the new print server.  I keep the returnvalue of the printer connection attempt using the Win32_Printer class and using the AddPrinterConnection() method. This is used in the next part of the script to determine the next action.

            If ($returnValue -eq 0) {
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $newPrinter,
                                             $returnValue,
                                             (Get-Date),
                                             "Added Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII            
                Write-Verbose ("{0}: Removing" -f $printer.name)
                $printer.Delete()
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $printer.Name,
                                             $returnValue,
                                             (Get-Date),
                                             "Removed Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
            } Else {
                Write-Verbose ("{0} returned error code: {1}" -f $newPrinter,$returnValue) -Verbose
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $newPrinter,
                                             $returnValue,
                                             (Get-Date),
                                             "Error Adding Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
            }
        }
    }
} Catch {
    "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                 $env:USERNAME,
                                 "WMIERROR",
                                 $_.Exception.Message,
                                 (Get-Date),
                                 "Error Querying Printers" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
}

The rest of the code handles what happens based on the returnvalue. If the value is 0, that means that the connection was successful and logged. Anything else is considered a failure and will then be logged as such. With the successful connection, the removal of the other printer mapping is then performed. Once completed, that is then logged and the process continues until all of the printers have been taken care of. An example of the logfile is below:

image

Pretty simple but gets the job done with no user interaction and any errors, failures to map printers are kept away from the user and logged to the remote log file for future review/troubleshooting by the system administrators.

Feel free to take the code below and use/modify to your liking.

Full Code

<#
    .SYNOPSIS
        Logon Script to migrate printer mapping
    
    .DESCRIPTION
        Logon Script to migrate printer mappings
    
    .NOTES
        Author: Boe Prox
        Create: 09 NOV 2012
        Modified:
        Version 1.0 - Initial Script Creation
                1.1 Added Header Text for CSV file
#>
Param (
    $newPrintServer = "Server2",
    $PrinterLog = "\\LogSVR\PrintMigration$\PrintMigration.csv"
)
<#
    #Header for CSV log file:
    "COMPUTERNAME,USERNAME,PRINTERNAME,RETURNCODE-ERRORMESSAGE,DATETIME,STATUS" | 
        Out-File -FilePath $PrinterLog -Encoding ASCII
#>
Try {
    Write-Verbose ("{0}: Checking for printers mapped to old print server" -f $Env:USERNAME)
    $printers = @(Get-WmiObject -Class Win32_Printer -Filter "SystemName='\\\\Server1'" -ErrorAction Stop)
    
    If ($printers.count -gt 0) {        
        ForEach ($printer in $printers) {
            Write-Verbose ("{0}: Replacing with new print server name: {1}" -f $Printer.Name,$newPrintServer)
            $newPrinter = $printer.Name -replace "Server1",$newPrintServer  
            $returnValue = ([wmiclass]"Win32_Printer").AddPrinterConnection($newPrinter).ReturnValue                
            If ($returnValue -eq 0) {
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $newPrinter,
                                             $returnValue,
                                             (Get-Date),
                                             "Added Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII            
                Write-Verbose ("{0}: Removing" -f $printer.name)
                $printer.Delete()
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $printer.Name,
                                             $returnValue,
                                             (Get-Date),
                                             "Removed Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
            } Else {
                Write-Verbose ("{0} returned error code: {1}" -f $newPrinter,$returnValue) -Verbose
                "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                             $env:USERNAME,
                                             $newPrinter,
                                             $returnValue,
                                             (Get-Date),
                                             "Error Adding Printer" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
            }
        }
    }
} Catch {
    "{0},{1},{2},{3},{4},{5}" -f $Env:COMPUTERNAME,
                                 $env:USERNAME,
                                 "WMIERROR",
                                 $_.Exception.Message,
                                 (Get-Date),
                                 "Error Querying Printers" | Out-File -FilePath $PrinterLog -Append -Encoding ASCII
}
Posted in powershell, scripts | Tagged , , | 7 Comments

PowerShell and WPF: TextBlock

Working again on my series on WPF and PowerShell, we are now going to take a look at TextBlocks, which is a lightweight control for displaying small amounts of flow content,  and how they can be used in your GUI to display text. You might be thinking to yourself, “why in the world would we need to look at yet another way to display text?”. Whereas a label and textbox and in fact display text to the user, it can only display it in one font, one color, etc… With a TextBlock, you can actually change all of these styles at any given point in time which can prove useful if your are parsing a log and want to highlight different types of data you find, which makes it like a RichTextbox (that I used in my project, PoshChat) with a few differences that will be shown later on in the article.

The following example shows some examples of using the TextBlock to display various styles for text.

#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 >
        <TextBlock x:Name = 'textblock' TextWrapping = "Wrap">
            <TextBlock.Inlines>
                <Run FontWeight="Bold" FontSize="14" Text="This is WPF TextBlock Example in Bold. " />
                <LineBreak />
                <Run FontStyle="Italic" Foreground="Green" Text="This is green text with italics. " />
                <LineBreak />
                <Run FontStyle="Italic" FontSize="18" Text="Here is some linear gradient text. ">
                    <Run.Foreground>
                        <LinearGradientBrush>
                            <GradientStop Color="Red" Offset="0.0" />
                            <GradientStop Color="Orange" Offset="0.25" />
                            <GradientStop Color="Yellow" Offset="0.5" />
                            <GradientStop Color="Blue" Offset="0.75" />
                            <GradientStop Color="Green" Offset="1" />
                        </LinearGradientBrush>
                    </Run.Foreground>
                </Run>
                <Run FontStyle="Italic" Foreground="Blue" Text="How about adding some Blue to the mix? " />
            </TextBlock.Inlines>
        </TextBlock>
        <Label Background="Black"/>
        <TextBlock TextWrapping = "Wrap">
            Some <Italic> italic words </Italic> along with some <Bold> bold words </Bold> to show
            another way of using TextBlocks.
        </TextBlock>
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#Connect to Controls
$textblock = $Window.FindName('textblock')


$Window.ShowDialog() | Out-Null

image

The first TextBlock shows how you can use the Inlines property that you can then add text styles to using the Run class or inserting a line break using the LineBreak class. The second TextBlock is a little less involved, but you can still notice how I was able to add different font styles in with the text to create the italic words and the bold words.

Below is an example that shows a “logging window” that displays errors, warning, etc… with a specified color for the output.

$uiHash = [hashtable]::Synchronized(@{})
$runspaceHash = [hashtable]::Synchronized(@{})
$jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.Arraylist))
$uiHash.jobFlag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)          
$newRunspace.SessionStateProxy.SetVariable("runspaceHash",$runspaceHash)     
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)     
$psCmd = [PowerShell]::Create().AddScript({  
    Add-Type –assemblyName PresentationFramework
    Add-Type –assemblyName PresentationCore
    Add-Type –assemblyName WindowsBase  
    #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 >    
            <ScrollViewer x:Name = "scrollviewer" VerticalScrollBarVisibility="Visible"  Height="365">    
                <TextBlock x:Name = 'textblock' TextWrapping = "Wrap" />
            </ScrollViewer >            
            <Label Background="Black" />
            <Button x:Name="button" Content="Start Demo" Background="White" />
        </StackPanel>
    </Window>
"@
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #Connect to Controls
    $uiHash.textblock = $uiHash.Window.FindName('textblock')
    $uiHash.button = $uiHash.Window.FindName('button')
    $uiHash.scrollviewer = $uiHash.Window.FindName('scrollviewer')

    #Jobs runspace
    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.Open()
    $runspace.SessionStateProxy.SetVariable("uihash",$uihash)
    $runspace.SessionStateProxy.SetVariable("jobs",$jobs)
    $runspaceHash.PowerShell = [powershell]::Create().AddScript({
        While ($uihash.jobFlag) {
            If ($jobs.Handle.IsCompleted) {
                $jobs.PowerShell.EndInvoke($jobs.handle)
                $jobs.PowerShell.Dispose()
                $jobs.clear()
            }
        }
    })
    $runspaceHash.PowerShell.Runspace = $runspace
    $runspaceHash.Handle = $runspaceHash.PowerShell.BeginInvoke()

    $running = $False

    #Events
    $uiHash.Window.Add_Closed({
        $uiHash.jobFlag = $False
        sleep -Milliseconds 500
        $runspaceHash.PowerShell.EndInvoke($runspaceHash.Handle)
        $runspaceHash.PowerShell.Dispose()
        $runspaceHash.Clear()
    })

    $uiHash.button.Add_Click({
        Write-Verbose ("Running: {0}" -f $running) -Verbose
        Switch ($running) {
            $True {
                #Stop demo
                $uiHash.Flag = $False
                $uiHash.button.Content = 'Start Demo'
                $Script:running = $False
            }
            $False {
                $uiHash.Flag = $True
                $scriptBlock = {
                    While ($uiHash.Flag){                    
                        Start-Sleep -Milliseconds 500
                        $uiHash.textblock.Dispatcher.Invoke("Normal",[action]{
                            $messages = "ERROR: Something bad has just happened!",
                                        "WARNING: A potential issue could occurr!",
                                        "VERBOSE: Making a change from this to that.",
                                        "INFO: Everything is going according to plan."                        
                            $Run = New-Object System.Windows.Documents.Run
                            $message = Get-Random $messages
                            Write-Verbose ("Type: {0}" -f $message) -Verbose
                            Switch -regex ($message) {
                                "^Verbose" {
                                    $Run.Foreground = "Yellow"
                                }
                                "^Warning" {
                                    $Run.Foreground = "Blue"
                                }
                                "^Info" {
                                    $Run.Foreground = "Black"
                                }
                                "^Error" {
                                    $Run.Foreground = "Red"
                                }
                            }
                            $Run.Text = ("{0}" -f $message)
                            Write-Verbose ("Adding a new line") -Verbose
                            $uiHash.TextBlock.Inlines.Add($Run)
                            Write-Verbose ("Adding a new linebreak") -Verbose
                            $uiHash.TextBlock.Inlines.Add((New-Object System.Windows.Documents.LineBreak))                                                  
                        })
                        $uiHash.scrollviewer.Dispatcher.Invoke("Normal",[action]{
                            $uiHash.scrollviewer.ScrollToEnd()
                        })
                    }
                }
                $runspace = [runspacefactory]::CreateRunspace()
                $runspace.Open()
                $runspace.SessionStateProxy.SetVariable("uiHash",$uiHash)
                $temp = "" | Select PowerShell,Handle
                $temp.PowerShell = [powershell]::Create().AddScript($scriptBlock)
                $temp.PowerShell.Runspace = $runspace
                $temp.Handle = $temp.PowerShell.BeginInvoke()
                $jobs.Add($temp)
                #Start demo
                $uiHash.button.Content = 'Stop Demo'
                $Script:running = $True
            }
        }
    })
    $uiHash.Window.ShowDialog() | Out-Null  
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

image

There are a couple of important things here to look at. The first thing is the use of a vertical scrollbar to handle the data as it goe s beyond the height of the window. A TextBlock does not have the capability to use a scrollbar unlike a TextBox or RichTextBox. To get around this issue, I have to wrap the TextBlock in a ScrollViewer control which then allows me to have a scrollbar on the TextBlock. Just as important is that I also set the size of the TextBlock through the ScrollViewer, otherwise the scrolling will not work like you think it should.

The next item to look at is how the focus is always on the last line being displayed. Regardless of how much data is being processed, the focus will always be at the bottom. This is done using the ScrollToEnd() method from the ScrollViewer. As long as the method is called after writing the line of text, it will always ensure that the data is shown and saves you from scrolling to the bottom to find out what is going on.

On a side note, you will notice that I am using background runspaces to handle all of my data and displaying of information. This is a great idea if you have GUIs that have long running operations. More on the use of that can be found here.

Posted in powershell, WPF | Tagged , , | 4 Comments