Dealing with Variables in a WinForm Event Handler: An Alternative to Script Scope

I saw a question  with an answer a while back showing how to work around an issue that had previously worked up until PowerShell V3 involving variables and how they work in an event handler in a WinForm or WPF UI. The suggested answer was to create the variable with the Script variable scope so that way the data in the variable would be available in the event handler and its scope.

An example of this can be shown below, first with the code that the variable would not work with because it is out of the scope of the event handler.

 
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
 
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
 
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
    $text  = $windowTextBox.Text
    $window.Dispose()
})
 
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
 
[void]$window.ShowDialog()
 
Write-Host -ForegroundColor Yellow "Test: $($Text)"

What happens here is that even though we set the variable ($Text) in the event handler, we cannot access it outside of the event handler at all.

image

As you can see below, the actual text inputted doesn’t make it outside of the event handler.

image

The solution to was to change the scope of the variable to Script: (not Global which would be beyond what is needed) to allow for the variable to be accessible to commands run in the script. As shown with the code below, we need to ensure that each time the variable is used, that we specify it as Script: to ensure it is read properly, otherwise the variable will just be treated like it was in the previous example.

 
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId 
Write-Host -ForegroundColor Green "ThreadID: $($ThreadID) - ProcessID: $PID)" 
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
 
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
    $ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId 
    Write-Host -ForegroundColor Green "[EventHandler] ThreadID: $($ThreadID) - ProcessID: $PID)" 
    $Script:text  = $windowTextBox.Text
    $window.Dispose()
})
 
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
 
[void]$window.ShowDialog()
 
Write-Host -ForegroundColor Yellow "Test: $($Script:text)"

image

SNAGHTML1686c8

This time we have our variable available outside of the event handler scope and now shows up when we display the information. Definitely something to remember when you are working with UIs in PowerShell V3 and above so you are not left wondering why specific events are not working properly.

In case you are wondering if the event handler occurs in a different process id and/or thread, it doesn’t.

image

I started with this because it works. But it isn’t the only approach to dealing with this issue by adjusting the variable scope. My approach to this is done using synchronized collections, or what I commonly use with runspaces, the synchronized hash table.

 
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$hash = [hashtable]::Synchronized(@{}) 
$hash.text = ""
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
 
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
 
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
    $hash.text = $windowTextBox.Text
    $window.Dispose()
})
 
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
 
[void]$window.ShowDialog()
 
Write-Host -ForegroundColor Yellow "Test: $($hash.text)"

image

Works like a charm without having to mess with variable scopes. But it does require that you initialize the hash table and make sure it is synchronized. This is a nice approach also if you have multiple variables that you may need to track across other event handlers rather than dealing with the variable scope.

Now, this doesn’t actually just apply to working with the UIs in PowerShell. This can also be applied to other event handlers such as those that have action script blocks using the Register-*Event cmdlets as well as shown in the demo below.

 
$synchash = [hashtable]::Synchronized(@{})
$synchash.test = 'test1'
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId 
Write-Host -ForegroundColor Green "ThreadID: $($ThreadID) - ProcessID: $PID)" 
$Job = start-job -ScriptBlock {start-sleep -seconds 2}
Register-ObjectEvent -InputObject $Job -EventName StateChanged -Action {
    $ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId 
    Write-Host -ForegroundColor Green "[EventHandler] ThreadID: $($ThreadID) - ProcessID: $PID)" 
    Write-Host "Data: $($synchash.test)" -ForegroundColor Yellow -BackgroundColor Black
}

image

Using the same approach worked again to display the data from within the Event handler script block. And as you can see again, the process id and thread id is the same as what is outside of the handler. We could also write to the hash table as well from within the handler and the data would be available for us to use in the current script.

So with that, we have an alternative to messing with the scope of variables if needed if you would rather not have to deal with it. This doesn’t mean that you have to stop using scopes such as Script, it just means that like most things in PowerShell, there are always alternatives to performing a single action. It does have a little bit more work in creating and initializing the synchronized collection, but it gives you something that will work perfectly within the scope of event handlers as well as offering a way to transport multiple variables within a single collection.

Posted in powershell | Tagged , , , | 6 Comments

Trend Reporting using PowerShell and SQL on MCPMag.com

I have a new series of articles that will be focusing on trend reporting using PowerShell and SQL. Be sure to check them out and let me know what you think!

PowerShell Trend Reporting, Part 1: Data Gathering
PowerShell Trend Reporting, Part 2: Send and Receive SQL Server Data
PowerShell Trend Reporting, Part 3: Excel and Word Reports
Posted in powershell | Tagged , , , , | 4 Comments

Creating a Table in Word Using PowerShell

In this article, I will be showing you how to build a table in Word using PowerShell.

First we will go ahead and make the initial object creation and open up Word.

 
$Word = New-Object -ComObject Word.Application
$Word.Visible = $True
$Document = $Word.Documents.Add()
$Selection = $Word.Selection

Now that we have done that, we can start looking at building out a table to use in our document. The first example will look more like a typical table that has the headers on the first row and the rest of the rows will consist of the actual data. Of course, the first thing that I need first is some actual data that I can put into a table.

 
$Volume = Get-WmiObject Win32_Volume

I think listing out all of the volumes on my system should suffice. Now that I have this information, I can now start building out my table. One of the values that I want to document has a specific lookup that I need to quickly build using a hash table.

 
$DriveType = @{
    0x0 = 'Unknown'
    0x1 = 'No Root Directory'
    0x2 = 'Removable Disk'
    0x3 = 'Local Disk'
    0x4 = 'Network Drive'
    0x5 = 'Compact Disk'
    0x6 = 'RAM Disk'
}

I want to ensure that I am at the very end of my current spot in the document so I can put the table after whatever I may have written. I also need to make sure that the object is treated like a collection, otherwise I won’t be able to specify the index, in this case –1 for the last item in the collection.

 
$Range = @($Selection.Paragraphs)[-1].Range

The $Range will be used soon in the construction and placement of the table. Using the $Selection, I will create a new able by using the Tables.Add() method and then saving the output of the method which is the table object that I can then use to begin processing and adding data into the table.

Looking at the parameters really doesn’t help much.

 
$Selection.Tables.add

image

This is one of those cases where a trip to MSDN will provide more useful information to determine what we can actually use.

image

Now that is much more useful to us! we can see that we need to specify the Range that was found as well as some parameters for the number of Rows and Columns as well as the behavior of the table and how the items will fit. We don’t actually have to specify the Behaviors, but in this case I will be adding those as well.

As it shows, the rows and columns are pretty much a no-brainer as we can look at how many properties we are using and that will be the Column count and as for the Rows, we will take the number of objects returned and then add 1 to the count which will account for the header row. In this case, I am going to have 6 columns and 5 rows in my table.

image

Now for my Table and AutoFit behavior, I need to figure out exactly what is available to choose from. Again, I can find this inform on MSDN, but I think this is a great exercise in PowerShell’s ability to easily explore the environment to locate what we are looking for.

 
$AutoFit = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'autofit'}
$AutoFit | Select FullName

$DefaultTable = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'defaulttable'}
$DefaultTable | Select FullName

image

Perfect! Now to see what values we have to choose from.

 
[Enum]::GetNames($AutoFit)
[Enum]::GetNames($DefaultTable)

image

For my table, I am going with wdAutoFitContent and wdWord9TableBehavior when I build out my table.

 
$Table = $Selection.Tables.add(
    $Selection.Range,($Volume.Count + 1),6,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

image

I am next going to pick out the table style that will be used for this specific table. Because I want a focus on the distinction between the header and the data, I am going with Medium Shading 1 – Accent 1 which means that my table goes from this…

image

…to this:

 
$Table.Style = "Medium Shading 1 - Accent 1" 

image

Ok, right now this really doesn’t look all that impressive, but it will look nicer once we have finished with the data. And speaking of data, lets start adding the header to this now.

Think of the table as a multi-dimensional array and that you have to use coordinates to properly place the data. In this case we are using the $Table.Cell(column,row) property where we have to specify the coordinates within the parenthesis to say where the data going and if we are going to add some custom formatting to that particular cell. Once each column has been completed we simply move onto the next column and perform the same operation until completed and ready for the next row.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Drive"
$Table.cell(1,2).range.Bold=1
$Table.cell(1,2).range.text = "DriveType"
$Table.cell(1,3).range.Bold=1
$Table.cell(1,3).range.text = "Label"
$Table.cell(1,4).range.Bold=1
$Table.cell(1,4).range.text = "FileSystem"
$Table.cell(1,5).range.Bold=1
$Table.cell(1,5).range.text = "FreeSpaceGB"
$Table.cell(1,6).range.Bold=1
$Table.cell(1,6).range.text = "CapacityGB"

image

Starting to look like something a little more useful now. The last piece of this is to add the data. Given that I really don’t know just how much data might be available at the time of this, I am relying on a For() loop to handle this for me.

 
## Values
For ($i=0; $i -lt ($Volume.Count); $i++) {
    $Table.cell(($i+2),1).range.Bold = 0
    $Table.cell(($i+2),1).range.text = $Volume[$i].Name
    $Table.cell(($i+2),2).range.Bold = 0
    $Table.cell(($i+2),2).range.text = $DriveType[[int]$Volume[$i].DriveType]
    $Table.cell(($i+2),3).range.Bold = 0
    $Table.cell(($i+2),3).range.text = $Volume[$i].Label
    $Table.cell(($i+2),4).range.Bold = 0
    $Table.cell(($i+2),4).range.text = $Volume[$i].FileSystem
    $Table.cell(($i+2),5).range.Bold = 0
    $Table.cell(($i+2),5).range.text = [math]::Round($Volume[$i].FreeSpace/1GB,2)
    $Table.cell(($i+2),6).range.Bold = 0
    $Table.cell(($i+2),6).range.text = [math]::Round($Volume[$i].Capacity/1GB,2)
}

Same approach as my header but not only do it increment the columns on each pass, I also have to make sure to increment the rows as well to ensure that the data is accurate. The end result is something like this:

image

Lastly, I want to make sure I am at the end of this and outside of my table so I can continue adding data or writing if necessary without writing to the table. I also want to add a space after the table for formatting purposes.

 
$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

If you wanted more of a list type of format, that can also be accomplished using a similar approach. This time we will keep the number of columns to two and use as many rows as we need.

 
$BIOS = Get-WmiObject Win32_Bios

$Range = @($Selection.Paragraphs)[-1].Range
$Table = $Selection.Tables.add(
    $Selection.Range,5,2,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

Another different is that I don’t want to focus on the first row, so a different style will be used.

 
$Table.Style = "Light Shading - Accent 1"

And lastly, we start building out the list table.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Manufacturer"
$Table.cell(2,1).range.Bold=1
$Table.cell(2,1).range.text = "Name"
$Table.cell(3,1).range.Bold=1
$Table.cell(3,1).range.text = "Version"
$Table.cell(4,1).range.Bold=1
$Table.cell(4,1).range.text = "Serial Number"
$Table.cell(5,1).range.Bold=1
$Table.cell(5,1).range.text = "BIOS Version"
    
## Data
$Table.cell(1,2).range.Bold = 0
$Table.cell(1,2).range.text = $BIOS.Manufacturer
$Table.cell(2,2).range.Bold = 0
$Table.cell(2,2).range.text = $BIOS.Name
$Table.cell(3,2).range.Bold = 0
$Table.cell(3,2).range.text = $BIOS.Version
$Table.cell(4,2).range.Bold = 0
$Table.cell(4,2).range.text = $BIOS.SerialNumber
$Table.cell(5,2).range.Bold = 0
$Table.cell(5,2).range.text = $BIOS.SMBIOSBIOSVersion

$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

The end result is something that looks like this:

image

Of course, this can easily start adding to your line count if you plan on having several tables that you want to add in your document and writing code isn’t about who has the most rows. Smile

I wrote a small function that takes an object and can build out a Table or List fairly easily and all it requires is the $Object to be added to the table and the $Selection object. Note that this is one that I use on my personal projects but can be altered to meet your needs. Another thing is that you will need to tailor your object to only have specific properties, otherwise your table/list will be overrun with data.

An example of it in use is here:

 
$Volume = @(Get-WmiObject Win32_Volume  | ForEach {
    [pscustomobject]@{
        Drive = $_.Name
        DriveType = $DriveType[[int]$_.DriveType]
        Label = $_.label
        FileSystem = $_.FileSystem
        "FreeSpace(GB)" = "{0:N2}" -f ($_.FreeSpace /1GB)
        "Capacity(GB)" = "{0:N2}" -f ($_.Capacity/1GB)
    }
})

New-WordTable -Object $Volume -Columns 6 -Rows ($Volume.Count+1) –AsTable

And here:

 
$BIOS = @(Get-WmiObject Win32_Bios | ForEach {
    [pscustomobject] @{
        Manufacturer = $_.Manufacturer
        Name = $_.Name
        Version = $_.Version
        SerialNumber = $_.SerialNumber
        BIOSVersion = $_.SMBIOSBIOSVersion
    }
})

New-WordTable -Object $BIOS -Columns 2 -Rows ($BIOS.PSObject.Properties | Measure-Object).Count -AsList

Source Code

 
Function New-WordTable {
    [cmdletbinding(
        DefaultParameterSetName='Table'
    )]
    Param (
        [parameter()]
        [object]$WordObject,
        [parameter()]
        [object]$Object,
        [parameter()]
        [int]$Columns,
        [parameter()]
        [int]$Rows,
        [parameter(ParameterSetName='Table')]
        [switch]$AsTable,
        [parameter(ParameterSetName='List')]
        [switch]$AsList,
        [parameter()]
        [string]$TableStyle,
        [parameter()]
        [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]$TableBehavior = 'wdWord9TableBehavior',
        [parameter()]
        [Microsoft.Office.Interop.Word.WdAutoFitBehavior]$AutoFitBehavior = 'wdAutoFitContent'
    )
    #Specifying 0 index ensures we get accurate data from a single object
    $Properties = $Object[0].psobject.properties.name
    $Range = @($WordObject.Paragraphs)[-1].Range
    $Table = $WordObject.Tables.add(
    $WordObject.Range,$Rows,$Columns,$TableBehavior, $AutoFitBehavior)

    Switch ($PSCmdlet.ParameterSetName) {
        'Table' {
            If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
                $Table.Style = "Medium Shading 1 - Accent 1"
            }
            $c = 1
            $r = 1
            #Build header
            $Properties | ForEach {
                Write-Verbose "Adding $($_)"
                $Table.cell($r,$c).range.Bold=1
                $Table.cell($r,$c).range.text = $_
                $c++
            }  
            $c = 1    
            #Add Data
            For ($i=0; $i -lt (($Object | Measure-Object).Count); $i++) {
                $Properties | ForEach {
                    $Table.cell(($i+2),$c).range.Bold=0
                    $Table.cell(($i+2),$c).range.text = $Object[$i].$_
                    $c++
                }
                $c = 1 
            }                 
        }
        'List' {
            If (-NOT $PSBoundParameters.ContainsKey('TableStyle')) {
                $Table.Style = "Light Shading - Accent 1"
            }
            $c = 1
            $r = 1
            $Properties | ForEach {
                $Table.cell($r,$c).range.Bold=1
                $Table.cell($r,$c).range.text = $_
                $c++
                $Table.cell($r,$c).range.Bold=0
                $Table.cell($r,$c).range.text = $Object.$_
                $c--
                $r++
            }
        }
    }
}
Posted in powershell | Tagged , , , | 16 Comments

Speaking at Nebraska Code Conference in March

imageI’ll be speaking at  the Nebraska Code Conference in March and I’ll be talking Trend Reporting Using PowerShell and SQL. This will be my first in person speaking gig at a conference so I am both excited and a little nervous about it but definitely looking forward to talking about PowerShell and showing something cool in the process.

If you happen to be in the area of Lincoln, Nebraska during 19 – 21 March, be sure to attend this conference and check out my session!

My Session: http://nebraskacode.com/sessions/trend-reporting-using-powershell-and-sql

Nebraska Code Conference site: http://nebraskacode.com/

Posted in News, powershell | Tagged , , , , , | Leave a comment

Creating a Table of Contents in Word Using PowerShell

Continuing on from my last article, this one will show you how to build a Table of Contents in Word using PowerShell. We will be using the same techniques and some of the same code to set up our word document and I will be pointing out some areas of interest that will be used to help create a table of contents.

First off, let’s go ahead and get our Word object ready to go:

 
$Word = New-Object -ComObject Word.Application
$Word.Visible = $True
$Document = $Word.Documents.Add()
$Selection = $Word.Selection
New-WordText -Text "$($Env:Computername)" -Style 'Title'
New-WordText -Text "Report compiled at $(Get-Date)."

image

The New-WordText is not a built-in function or cmdlet that you can use natively in PowerShell. It is simply a helper function that I have in a different script that works well for what I want to do here. The source code for it is here:

 
Function New-WordText {
    Param (
        [string]$Text,
        [int]$Size = 11,
        [string]$Style = 'Normal',
        [Microsoft.Office.Interop.Word.WdColor]$ForegroundColor = "wdColorAutomatic",
        [switch]$Bold,
        [switch]$Italic,
        [switch]$NoNewLine
    )  
    Try {
        $Selection.Style = $Style
    } Catch {
        Write-Warning "Style: `"$Style`" doesn't exist! Try another name."
        Break
    }

    If ($Style -notmatch 'Title|^Heading'){
        $Selection.Font.Size = $Size  
        If ($PSBoundParameters.ContainsKey('Bold')) {
            $Selection.Font.Bold = 1
        } Else {
            $Selection.Font.Bold = 0
        }
        If ($PSBoundParameters.ContainsKey('Italic')) {
            $Selection.Font.Italic = 1
        } Else {
            $Selection.Font.Italic = 0
        }          
        $Selection.Font.Color = $ForegroundColor
    }

    $Selection.TypeText($Text)

    If (-NOT $PSBoundParameters.ContainsKey('NoNewLine')) {
        $Selection.TypeParagraph()
    }
}

Using this function will make writing text to the word document simpler and save on code later on. With this done, now we can focus on building out the Table of Contents in the word document. Because I want this to be at the front of the document, I need to build it out now rather than later on in the document or after I am finished.

In order to do this I will be using the $Document.TablesOfContents.Add() method which has a single required parameter: Range and several other optional parameters that you can supply if you want extra configuration of the table of contents. For more information, you can check out the Add() method information here. I won’t be using any of those here and will just focus on the default configuration of the table of contents so only the Range is important to me. The range is nothing more than a selection of where I want the table of contents to be at.  Speaking of the range, I can pull that information by using the following code:

 
$range = $Selection.Range

With that out of the way, I can now create the table of contents. When I do this, I need to keep the resulting object that is outputted when using the Add() method.

 
$toc = $Document.TablesOfContents.Add($range)
$Selection.TypeParagraph()

image

Nothing to worry about here. This is just letting use know that while the table of contents has been created, we haven’t actually defined anything  that should appear there yet. That will change shortly as we begin adding data to the word document.

A quick view of the table of contents object shows some nice information such as the Update() method. This may be useful at some point in time to us.

image

Moving on, we now need to set up a way to have specific items appear in the table of contents. But what do we need to do in order for this to happen? The answer lies in the Heading style that exists in Word. Depending on your Word configuration, you may have a set number of heading styles available to use. In my case, I have quite a few.

image

I actually have 6 heading styles but the other one is on the second row. Regardless of that, I now know what I can work with in my document and at the end, we can see how this affects the table of contents.

With that, I am going to randomly create some heading styles with random values so we can have some data available.

 
For($i=0;$i-lt 8;$i++) {
    New-WordText -Text "Main Heading $i" -Style 'Heading 1'
    For($i=0;$i-lt 8;$i++) {
        New-WordText -Text "Secondary Heading $i" -Style 'Heading 2'
        For($i=0;$i-lt 8;$i++) {
            New-WordText -Text "Third Heading $i" -Style 'Heading 3'
        }
    }
}

Now I have about 5 pages of useless information in my document! Now comes the fun part where I tell the table of contents to update itself based on all of the heading information and see what happens!

 
$toc.Update()

You won’t actually see anything return after you run this, so it is assumed that if no errors occurred, then it at least made the attempt to catalog all of the heading styles and constructed a table of contents. In this case, it worked like a champ and we can now see the table of contents listed at the top of the document.

image

I have truncated the rest of the output shown here as it spans a couple of pages to support the five pages of heading information, but you can get the idea of what it can do. This provides a great way to add some additional configuration to a report that is generated via a script which is especially useful if you have large amounts of data that covers a lot of pages and helps whoever views it to navigate more quickly to the information that they may need to see.

Hopefully you find this information useful in your future Word reporting needs!

Posted in powershell | Tagged , , , | 1 Comment