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

PowerShell and WPF: Textbox

The next control in my series on working with WPF using PowerShell is Textboxes. The textbox control is as it sounds, a box that can hold text. You can use it both as a input box as well as an output box to display information. Using this control, you can give people greater control over what they can add to query, pick a computer to run something against or anything else that you can think of.

Lets get started by creating a single textbox to write text in.

#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"
    SizeToContent = "WidthAndHeight" ShowInTaskbar = "True" Background = "lightgray"> 
    <StackPanel >  
        <Label Content='Type in this textbox' />
        <TextBox x:Name="InputBox" Height = "50" />  
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )


$Window.ShowDialog() | Out-Null

You can type in the textbox and you will notice that as soon as you hit the end of the textbox, the text will just continue on but you won’t be able to see what is at the beginning of the textbox unless you make the window larger! How can we fix this? I will show you after the pictures below.

image

image

image

Returns and Text Wrapping

Ok, so this works and such, but try hitting Enter on your keyboard and watch what happens. Absolutely nothing! Why does this happen you ask? It is because we did not specify the AcceptsReturn property and set it to True. Once we do that, the Textbox will respect the Enter key and move to the next line. Also, to make this respond better, I will set the TextWrapping property on this control to Wrap. By doing this, instead of cutting of some of the text when it reaches the end, it will actually continue displaying the text on the next line.

#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 = "25" Height = "120" ShowInTaskbar = "True" Background = "lightgray"> 
    <StackPanel >  
        <Label Content='Type in this textbox' />
        <TextBox x:Name="InputBox" Height = "80" AcceptsTab="True" AcceptsReturn="True"
        TextWrapping="Wrap" VerticalScrollBarVisibility = "Auto"/> 
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )


$Window.ShowDialog() | Out-Null

image

Tabbing and ScrollBars

A couple of other items that I added were the AcceptsTab=True and VerticalScrollBarVisibility=Auto. This allows me to use the Tab key and also if the text goes beyond the set height of the Textbox, it will add a scroll bar that you can use to scroll and read the text as shown below.

image

Input/Output Window

Ok, now lets do something a little different by setting a input/output window. With this, whatever you type in the bottom of the window, will show up in the top window but will be in a read-only state. So you can copy it, but not write to the top textbox. Kind of like what you would see with a chat client.

#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 = "280" ShowInTaskbar = "True" Background = "lightgray"> 
    <StackPanel >
        <Label Content='Text sent will appear below' />
        <TextBox x:Name="OutputBox" IsReadOnly = "True" Height = "75"
        TextWrapping="Wrap" VerticalScrollBarVisibility = "Auto"/>    
        <StackPanel Orientation = 'Horizontal'>  
            <Button x:Name = "button1" Height = "75" Width = "75" Content = 'Send' Background="Yellow" />
            <Button x:Name = "button2" Height = "75" Width = "75" Content = 'Clear' Background="Yellow" Margin = "50,0,0,0" />  
        </StackPanel>
        <Label Content='Type in this textbox and press Send' />
        <TextBox x:Name="InputBox" Height = "50" AcceptsTab="True" AcceptsReturn="True" 
        TextWrapping="Wrap" VerticalScrollBarVisibility = "Auto"/>  
    </StackPanel>
</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')
$OutputBox = $Window.FindName('OutputBox')
$InputBox = $Window.FindName('InputBox')

#Events
$button1.Add_Click({
    $OutputBox.AppendText(("{0}`r" -f $InputBox.Text))
    $InputBox.Clear()
})

$button2.Add_Click({
    $InputBox.Clear()
    $OutputBox.Clear()
})

$Window.ShowDialog() | Out-Null

You will notice some events have been set up to handle the button clicks for the Send and Clear buttons. The send button will take the text from the input textbox that resides in the $Inputbox.Text property and copy it over to the $Outputbox. The event will also clear out the $Inputbox by using the Clear() method. The same method is used during the Clear button event when pressed.

image

Also notice that everything I did with tabbing and returning as well as the text wrapping is respected in the output textbox. Now if the input textbox was smaller and the output textbox was larger, the text wrapping may have not happened.

Spellchecking

Believe it or not, but spellchecking is pretty painless to setup with a textbox. Simply add the following to your XAML code for the Textbox that will support spell checking:

SpellCheck.IsEnabled="True"

Now when you type a misspelled word, it will give you the familiar red underline and let you change the spelling or ignore by right clicking on the word.

image

TextChanged Event

One last thing to show that is pretty cool is the TextChanged event. What this means that whenever the text changes (just like the event says!), you can specify an action to take! Pretty cool stuff to use! Lets take a look at an example that will filter the available PowerShell cmdlets as you start typing the name.

#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 >
        <Label Content='Cmdlets will appear below' />
        <TextBox x:Name="OutputBox" IsReadOnly = "True" Height = "300"
        TextWrapping="Wrap" VerticalScrollBarVisibility = "Auto"/>    
        <StackPanel Orientation = 'Horizontal'>  
 
        </StackPanel>
        <Label Content='Type in this textbox' />
        <TextBox x:Name="InputBox" Height = "50" AcceptsTab="True" AcceptsReturn="True" 
        TextWrapping="Wrap" VerticalScrollBarVisibility = "Auto"  SpellCheck.IsEnabled="True"/>  
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

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

#Events
$InputBox.Add_TextChanged({
    $cmdlets = @(Get-Command -CommandType Cmdlet -Name ("{0}*" -f $InputBox.Text) | Select -ExpandProperty Name)
    $OutputBox.Text = ($cmdlets | ForEach {"{0}`r" -f $_})
})

$Window.ShowDialog() | Out-Null

image

image

Again, it is pretty neat to do. If I were using a listbox to display the output, I could then add some other events to allow you to view the help for a cmdlet, but that will be for another article. Other items that I didn’t touch on but can be used are the various Font properties available for the textboxes that I have shown in previous articles.

Updated SysInfo Tool

If you remember the simple utility I wrote in my last article to get system information via WMI, it was lacking a bit by using labels to handle the output. That changes with textboxes for both handling the output better, but also allowing me to add better query information such as picking a remote system and even specifying the WMI class to query!

image

image

image

Updated SysInfo_Textbox Code

Function Invoke-WMIQuery {
        $wmiClass = ($WMI_txtbx.Text).Trim()
    Try {        
        $Data = Get-WmiObject -ComputerName $Computer_txtbx.Text -Class $wmiClass -ErrorAction Stop
        $OutputBox.Foreground = "White"
        $OutputBox.Text = $Data | Out-String
    } Catch {
        $OutputBox.Foreground = "Red"
        $OutputBox.Text = $_.Exception.Message
    }
}
#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="WMI Query" WindowStartupLocation = "CenterScreen" ResizeMode="NoResize"
    SizeToContent = "WidthAndHeight" ShowInTaskbar = "True" Background = "lightgray"> 
    <StackPanel Orientation = 'Horizontal'>
        <StackPanel>    
            <StackPanel Orientation = 'Horizontal'>  
                <Label Content = "Computername"/>        
                <TextBox x:Name="Computer_txtbx" Height = "25" Width = "122" AcceptsReturn="True" SpellCheck.IsEnabled="True" />   
            </StackPanel>
            <StackPanel Orientation = 'Horizontal'>  
                <Label Content = "WMI Class"/>        
                <TextBox x:Name="WMI_txtbx" Height = "25" Width = "150" AcceptsReturn="True" SpellCheck.IsEnabled="True" />   
            </StackPanel>            
            <StackPanel Orientation = 'Horizontal'>
                <Button x:Name = "query_btn" Height = "50" Width = "75" Content = 'Query' Background="Yellow"/>
                <Button x:Name = "clear_btn" Height = "50" Width = "75" Content = 'Clear' Background="Yellow" Margin = "50,0,0,0" />  
            </StackPanel>
        </StackPanel>    
        <TextBox x:Name="OutputBox" IsReadOnly = "True" Height = "300" Width = "500" TextWrapping="Wrap" 
        Background = "Black" Foreground = 'White' FontWeight = 'Bold' VerticalScrollBarVisibility = "Visible"/>            
    </StackPanel>
</Window>
"@
 
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#Connect to Controls
$query_btn = $Window.FindName('query_btn')
$clear_btn = $Window.FindName('clear_btn')
$OutputBox = $Window.FindName('OutputBox')
$Computer_txtbx = $Window.FindName('Computer_txtbx')
$WMI_txtbx = $Window.FindName('WMI_txtbx')

#Events
$query_btn.Add_Click({    
    Invoke-WMIQuery
})

$clear_btn.Add_Click({
    $OutputBox.Clear()
})

$Window.Add_KeyDown({
    If ($_.Key -eq "F5") {
        Invoke-WMIQuery
    }
})

$Window.ShowDialog() | Out-Null

Up Next

Up next in my continuing series on WPF will be TextBlock.

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