# Sunday, December 02, 2007

Using Windows Forms controls in PowerShell is a tricky thing, because it only supports very simple event listboxhandling. However, some time ago I wrote a Snap-In to allow anyone to work with .NET events, even asynchronous ones. You can download it from my PSEventing CodePlex project; it comes with full help via the normal PowerShell mechanisms of -? and get-help. Examples are also available online on how to use the project for more complex scenarios. Here's simple demonstration of autogenerating a listbox control filled with choices given as arguments to a simple script:

  1. # as simple as 1,2,3 :-)  
  2. $choice = .\get-choice.ps1 "one","two","three" 

And here is the source to the get-choice.ps1 script itself. This requires PSEventing 1.0 or 1.1 Beta.

  1. #requires -pssnapin pseventing  
  2.  
  3. # http://www.codeplex.com/pseventing (1.0 or 1.1)  
  4.  
  5. param([string[]]$choices = $(throw "please supply a string array of choices"))  
  6.  
  7. if ($choices.length -eq 0) {  
  8.     Write-Warning "cannot be a zero length array." 
  9.     return 
  10. }  
  11.  
  12. # initialize form  
  13. $form = new-object windows.forms.form  
  14. $form.Text = "Choose..." 
  15. $form.MinimizeBox = $false 
  16. $form.MaximizeBox = $false 
  17. $form.AutoSize = $true 
  18. $form.AutoSizeMode = "GrowAndShrink" 
  19.  
  20. # initialize listbox  
  21. $listbox = new-object windows.forms.listbox  
  22. $choices | % { [void]$listbox.items.add($_) }  
  23.  
  24. $form.controls.Add($listbox)  
  25.  
  26. # catch a choice in the listbox (remove -verbose for quiet mode)  
  27. Connect-EventListener -VariableName listbox -EventName SelectedIndexChanged -Verbose  
  28.  
  29. # catch someone closing the form (remove -verbose for quiet mode)  
  30. Connect-EventListener -VariableName form -EventName Closed -Verbose  
  31.  
  32. $form.Show()  
  33.  
  34. # wait for an event while performing sendmessage pumping (or ctrl+c to exit)  
  35. $event = Get-Event -Wait  
  36.  
  37. # don't pollute pipeline (remove in production)  
  38. write-host ($event | ft -auto | out-string)  
  39.  
  40. $form.Dispose()  
  41.  
  42. $choice = $listbox.SelectedItem  
  43.  
  44. # clean up; event listeners will be automatically unhooked ;-)  
  45. $form = $null 
  46. $listbox = $null 
  47.  
  48. # return chosen item, or $null if the form was closed  
  49. if ($event.Name -eq "SelectedIndexChanged") {  
  50.     $choice 
  51. } else {  
  52.     $null # cancelled  
Have fun!
posted on Sunday, December 02, 2007 4:48:09 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback

It's very easy sometimes to look at the PowerShell grammar and partition it into two distinct styles: pipeline and traditional imperative script. In fact, it took a number of weeks of playing around before I realised that this is leaving out a large portion of patterns that employ a fusion of these two styles. For example, Let's iterate over an array of numbers, skipping null elements; first, using the pipeline style:

  1. $arr = @(1,2,3,$null,5)  
  2.  
  3. $arr | where-object { $_ -ne $null } | foreach-object { $_

Then, using a traditional VBScript style, using the foreach keyword, as opposed to the foreach-object cmdlet:

  1. $arr = @(1,2,3,$null,5)  
  2.  
  3. foreach ($elem in $arr) {  
  4.     if ($elem -ne $null) {  
  5.         $elem 
  6.     }  

And finally, a fusion of the two styles whereby I insert a pipeline into the expression, and also take advantage of the fact that $null will evaluate to $false, thereby skipping the need to test with the -eq operator (update: Keith Hill reminded me that both 0 and "" will also evaluate to $false, so I added an explicit -ne $null):

  1. $arr = @(1,2,3,$null,5)  
  2.  
  3. foreach ($elem in $arr | where-object {$_ -ne $null} ) {  
  4.     $elem 

As Mr. Snover is fond of saying, this is a great example of PowerShell's pithiness. PithyShell at its best!

posted on Sunday, December 02, 2007 12:58:14 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1] Trackback
# Wednesday, November 21, 2007

Well, this is not exactly rocket science, but it helped me out today with some spreadsheets tomfoolery. It generates PSCustomObjects with properties named after the header row in the Excel sheet (you need a header row for this to work), just like Import-Csv. The parameters should be self-explanatory: filename and worksheet.

param (
    [
string]$filename = $(throw "need a filename, e.g. c:\temp\test.xls"),
   
[string]$worksheet
)

if (-not (Test-Path $filename)) {
    throw "Path '$filename' does not exist."
    exit
}

if (-not $worksheet) {
   
Write-Warning "Defaulting to Sheet1 in workbook."
   
$worksheet = "Sheet1"
}

# resolve relative paths
$filename = Resolve-Path $filename

# assume header row (HDR=YES)
$connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data" +
    "
Source=${filename};Extended Properties=`"Excel 8.0;HDR=YES`"";

$connection = New-Object data.OleDb.OleDbConnection $connectionString;
$connection.Open();
$command = New-Object data.OleDb.OleDbCommand "select * from [$worksheet`$]"

$command.connection = $connection
$reader
= $command.ExecuteReader("CloseConnection")

if ($reader.HasRows) {
   
# cache field names
   
$fields = @()
   
$count = $reader.FieldCount

    for ($i = 0; $i -lt $count; $i++) {
        $fields += $reader.GetName($i)
    }

    while ($reader.read()) {

        trap [exception] {
            Write-Warning "Error building row."
            break;
        }

        # needs to match field count
        $values = New-Object object[] $count

        # cache row values
        $reader.GetValues($values)

        $row = New-Object psobject
        $fields | foreach-object -begin {$i = 0} -process {
            $row | Add-Member -MemberType noteproperty -Name $fields[$i
                -Value $values[$i]; $i++
        }
        $row # emit row
    }
}

 

posted on Wednesday, November 21, 2007 5:51:47 PM (Eastern Standard Time, UTC-05:00)  #    Comments [2] Trackback
# Tuesday, November 20, 2007

It's been a really busy few weeks in work, but I'm finally starting to find some time to put together some weightier posts on PowerShell. By this stage, everyone is pretty good at hacking together Cmdlets, and to be honest, Cmdlets are where it's at for most applications. However, I think the provider system is pretty powerful and deserves a chance to become a more equal partner to its Cmdlet brethren. The bewildering array of methods and base classes at your disposal is a little intimidating, and let's be honest, not everyone's provider ideas can be compared to an Access Database provider model.

My new blog is reachable via http://oisin.powershell.com/ (thanks Klumsy!) and is a redirect to my spot on the new community site, http://www.powershellcommunity.org/ . The first post will cover PowerShell paths in all their guises.

 

posted on Tuesday, November 20, 2007 10:24:49 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0] Trackback
# Wednesday, October 24, 2007

Someone on the newsgroup ran into an issue whereby a instance of a non CLS Compliant type was fed to PowerShell's default formatter. The type in question had two properties with the same name, but in different casing. I'm pretty much just going to carbon-copy the qestion and answer here as I feel this is worth preserving in the annals of blogdom.

> I am in the process of writing a script to document SharePoint
> installations.  When I try to get information about the content database
> I get this error.

> "out-lineoutput : The field/property: "Id" for type:
> "Microsoft.SharePoint.Administration.SPContentDatabase" differs only in
> case from the field/property: "ID". Failed to use non CLS compliant type."

> Here is some of the PowerShell Script that I am using

> [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
> $site = New-Object Microsoft.SharePoint.SPSite("http://localhost")
> $site.ContentDatabase

> It is the final line that is giving me the error.  I can get other
> properties without error.  Can someone enlighten me on this error
> message and let me know if there is a way to work around it.

> Thanks
> Jerry

Hi Jerry,

This is a tricky one for people very new to powershell without any real experience of the .NET framework. One of the rules of a so-called "CLS compliant type" (cls = common lanaguage specification) is that you should not expose any public members that only differ in casing. The SPContentDatabase type has two properties, "Id" and "ID." This is perfectly legal in C# since case in important in that language and the compiler sees them as different as chalk and cheese. However, in VB.NET, case is not important and as such it [vb] has no native mechanism to differentiate between the two. The same goes for PowerShell's grammar. So, how do we get around this?

First, you grab a handle to the method directly by asking for it from the Type itself:

PS> $idMethod1 = [Microsoft.SharePoint.Administration.SPContentDatabase].getmethod("get_ID")

and for the second version,

PS> $idMethod2 = [Microsoft.SharePoint.Administration.SPContentDatabase].getmethod("get_Id")

Note the differing case for get_Id and get_ID. Btw, Properties like ID in .NET are actually served by special accessor methods prefixed with get_ and set_, hence the prefixes. Next, you need a way to invoke those methods on the instance itself: this is done by using the Invoke method on the MethodInfo object representing the method itself. You pass the instance to the Invoke method telling it that the method is "instance" (e.g. not static/shared) and "public" and it will call that method for you on the instance, returning the value, if any.

PS> $result = $idMethod1.Invoke($site.ContentDatabase, "instance,public", $null, $null, $null)

I know from the SharePoint documenation, that this property returns a guid. Also, due to another oversight, Posh does not know how to format Guids, so you must call ToString() on the result to get the value back or you'll be greeted by a blank line from powershell's formatter.

PS> $result.ToString()
1ade149d-2049-4668-b555-4b7be9374c65

Bizarrely, these two properties return the same guid, but hey, I'll let someone from the sharepoint team answer that one. Anyway, you should have enough information here to extract the properties that you need.

So, that's the interesting portion out of the way. Another way to do this might be to create a custom view for the SPContentDatabase type omitting the duplicated property and use the Update-FormatData cmdlet to activate it. This may not work though, as the non-cls compliancy issue may rear it's ugly head before posh's formatter gets this far.

Hope this helps,

- Oisin / x0n.

posted on Wednesday, October 24, 2007 4:18:50 PM (Eastern Daylight Time, UTC-04:00)  #    Comments [0] Trackback