The Series


Edit - I have changed the title of this blog series from AngularJS GUIs with PowerShell to just PowerShell GUI with HTML. I will make a second series though covering AngularJS, how and why you should use it for making PowerShell GUIs but I figured that it needed to be split out.

The Concept

I am going to cover in the next few blog posts a method I have developed for developing user interfaces for PowerShell scripts using HTML. This is not an HTA. This is all built from within PowerShell.

PowerShell does not have a built in method for rendering HTML live and collecting feedback. PowerShell does however have full access to .NET. Which turns out you can make a web server. Which means you can serve HTML from PowerShell and collect feedback from a web browser (See Obscure sec’s dirty PowerShell WebServer and the MSDN docs for HTTP Listener).

PowerShell can also host a web browser in a winforms object. This means PowerShell has the full capability to both serve webpages and control / force the experience of navigating to this webpage. We have the makings of a GUI system in the works!

If all this sounds complicated… PowerShell makes it SUPER easy. Fear not, we are going to cover in the next few posts some basics of the web and how to the web server works in PowerShell as that is the most complicated portion and for the last post we will make up some dynamic user interfaces that can be fed to from ConvertTo-HTML and ConvertTo-JSON in less than 40 lines of code.

The Reasoning

There are a few reasons you would want to build a user interface in HTML.

  1. It is simple
  2. There are a lot of tutorials
  3. There are a lot of style libraries
  4. There are a lot of javascript libraries linke AngularJS to help render your view dynamically

Other Options

Before I make the first post and start getting into the goodness it is important to know there are other more standardized / accepted methods for developing user interfaces. If this is your first user interface in PowerShell it is probably better to start with them just to familiarize yourself with what is available in PowerShell:

The Basic One-Response Web Server

Step 1: Create HttpListener Object

 
$SimpleServer = New-Object Net.HttpListener
 

Step 2: Tell the HttpListener which port to listen on

As long as we use localhost we don’t need admin rights. To listen on externally accessible IP addresses we would need admin rights.

 
$SimpleServer.Prefixes.Add("http://localhost:8000/")
 

Step 3: Start up the server

 
$SimpleServer.Start()
 

Step 4: Tell the server to wait for a request to come in on that port.

 
$Context = $SimpleServer.GetContext()
 

Note your PowerShell session will hang at this point. It will wait until there is an HTTP request made on the port it is listening on to continue.

At this point, if you are following along go ahead and use your browser to visit http://localhost:8000. This should show on your browser as the webpage is loading (It’s waiting for PowerShell to process the response and send it back). You’ll notice your PowerShell session has continued and you should be able to type in the console more commands to process the request that is now contained in our $Context variable.

Once a request has been captured the details of the request and the template for the response are created in our $Context variable

$Context.Request - contains details about the request

$Context.Response - basically a template of what can be sent back to the browser

$Context.User - contains information about the user who sent the request. This is useful in situations where authentication is necessary. We won’t worry about authentication since we aren’t listening externally.

Step 5: Send a response to the browser

 
$result = " Hello World! "
 

In order to send it to the browser we need to convert it from ASCII encoded text into bytes.

 
$buffer = [System.Text.Encoding]::ASCII.GetBytes($result)
 

We need to let the browser know how many bytes we are going to be sending

 
$context.Response.ContentLength64 = $buffer.Length
 

We then send the response back to the browser

 
$context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
 

We close the response to let the browser know we are done sending the response

 
$Context.Response.Close()
 

We stop our server

 
$SimpleServer.Stop()
 

There we have it! Our first server written in PowerShell serving web pages to a web browser. Stay tuned for the next blog post where we will cover how to handle various URLs in PowerShell, then we will talk about query strings, followed by constructing it in a way that looks like a more standard GUI using WinForms so we don’t have to ever direct end users to open a browser or even worry about HTML and then we’ll spend a few posts on minimizing the amount of code we need and avoiding nasty CSS by using AngularJS and Angular Material to build our UI.

So I decided I wanted to add in a new feature to the PoshProgressBar module. It is always handy to be able to close out of a window but still be able to monitor progress via the notification area, so I decided an icon in the notification tray would be a good next step.

Problem #1 - Override the default close action of the PoshProgressBar window.

I didn’t want to clutter up the window with an extra button that said hide or something like that so I wanted to just have it hide to the notification tray when you click X on the window. It turns out that wasn’t so bad.

 
$Synchash.window.Add_Closing({
                
                $SyncHash.Window.Hide()
                $_.Cancel = $true
         
         })
 

The hide() method on the window will make it disappear. Setting the Cancel property of the closing event will then cancel out of the closing event. Nice!

Problem #2 - Getting NotifyIcon (WinForms) to play with WPF in the same thread

I looked at a few methods of adding a notification icon with wpf and XAML but I didn’t want to add another dll to the module. If you’re interested in doing a true WPF notification icon you can check out this guy.

I decided I was going to go with WinForms NotifyIcon. Getting them to play nicely was actually not difficult at all.

 
$syncHash.Window=[Windows.Markup.XamlReader]::parse( $SyncHash.XAML )

...

$SyncHash.NotifyIcon = New-Object System.Windows.Forms.NotifyIcon

...

$SyncHash.NotifyIcon.Visible = $true

...

$syncHash.Window.Show() | Out-Null
$appContext = [System.Windows.Forms.ApplicationContext]::new()
 

Changing from using the ShowDialog() method to using the Show() method prevented the Window object from hogging up the whole thread. The application context allows two forms to run and will not continue the script until both are closed or the application context is exited.

Problem #3 - Closing the progress bar

Now that I have my exit button magically used to just hide the progress bar. How do I let end users close it and exit the script if they do not want it running any more?

Adding a menu item to the notification icon seemed the easiest and most sensible method.

 
$menuitem = New-Object System.Windows.Forms.MenuItem
$menuitem.Text = "Exit"

$contextmenu = New-Object System.Windows.Forms.ContextMenu
$SyncHash.NotifyIcon.ContextMenu = $contextmenu
$SyncHash.NotifyIcon.contextMenu.MenuItems.AddRange($menuitem)

...

# When Exit is clicked, close everything and kill the PowerShell process
$menuitem.add_Click({
 
    $SyncHash.NotifyIcon.Visible = $false
    $syncHash.Closing = $True
    $syncHash.Window.Close()
    [System.Windows.Forms.Application]::Exit()

})
 

Note the addition of the Closing property. This is used to allow the window to actually close, as we had overridden this event to solve problem #2.

Our closing event now looks like this.

 
$Synchash.window.Add_Closing({

    if($SyncHash.Closing -eq $True)
    {
        
    }
    else
    {
        
        $SyncHash.Window.Hide()
        $SyncHash.NotifyIcon.BalloonTipTitle = "Your script is still running..."
        $SyncHash.NotifyIcon.BalloonTipText = "Double click to open the progress bar again."
        $SyncHash.NotifyIcon.ShowBalloonTip(100)
        $_.Cancel = $true

    }

})
 

Not too bad. Of course, you can still close the progress bar from within the script using the Close-ProgressBar cmdlet. Which sets the Closing variable to True. The closing variable is checked inside the update block of the progress bar and used to close it as follows.

 
$updateBlock = {            
            
            
            if($SyncHash.Closing -eq $True)
            {

                $SyncHash.NotifyIcon.Visible = $false
                $syncHash.Window.Close()
                [System.Windows.Forms.Application]::Exit()
                Break
            }
            
            ...
                     
        } 
 

That’s all folks!

Get the updated module

More in This Series

Alright, so just wanted to share some brief notes here on a technique for doing SAPGUI scripting from PowerShell.

It’s not pretty

I’m sure this isn’t the best way to integrate PowerShell and SAP. I’m not an SAP expert but this may help someone in a pinch so I thought I would share.

By default SAP GUI scripts are recorded in VBScript. The very first thing that is usually done is to call GetObject(“SAPGUI”). PowerShell can sorta run GetObject but that has some problems. None of us want to get into reflection. So what’s the trick? The MSScriptControl.ScriptControl in 32-bit PowerShell.

Running SAPGUI scripts

 

$ScriptControl = New-Object -comobject MSScriptControl.ScriptControl
$ScriptControl.language = "vbscript"

$Username = "Myusername"
$Password = "MyPassword"

$Login = @"
    If Not IsObject(application) Then
       Set SapGuiAuto  = GetObject("SAPGUI")
       Set application = SapGuiAuto.GetScriptingEngine
    End If
    If Not IsObject(connection) Then
       Set connection = application.Children(0)
    End If
    If Not IsObject(session) Then
       Set session    = connection.Children(0)
    End If
    If IsObject(WScript) Then
       WScript.ConnectObject session,     "on"
       WScript.ConnectObject application, "on"
    End If
    session.findById("wnd[0]").resizeWorkingPane 140,23,false
    session.findById("wnd[0]/usr/txtRSYST-BNAME").text = "$UserName"
    session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = "$Password"
    session.findById("wnd[0]").sendVKey 0
"@

$ScriptControl.AddCode($Login)

 

*Note the username and password are passed into the script using a here-string before the script is added to the script object.

Returning text from SAPGUI to PowerShell

 

$UserNameFromSAP = $ScriptControl.Eval('session.findById("wnd[0]/usr/txtRSYST-BNAME").text')

 

I have used AddCode to script / automate the GUI up to the point where I would need to pull data to SAP then at that point I would extract the data using the Eval method, run the PowerShell code, then feed it back in using a here-string built vbscript using the result of the PowerShell script.

Kind of clunky but it works. The scripting object is actually really cool. It can return entire objects from vbscriptland to PowerShell. You can almost fully bring it out of vbscript into PowerShell with this oneliner

 

$SAPGUI = $ScriptControl.Eval('(GetObject("SAPGUI")).GetScriptingEngine.Children(0).Children(0)')

 

Using this little chunk of code exposes the majorit of methods you would need to automate SAPGUI (i.e. $SAPGUI.FindByID(“wnd[0]/usr/txtRSYST-BNAME”).text would actually return the text value of that field, but I ran into areas where I would be needing to use reflection for certain methods so I dropped it.

The real problem though is that particular COM object is only available in 32-bit PowerShell which means you have to either script for 32-bit or run Start-Job -RunAs32 -ScriptBlock { [Code here] } which isn’t the funnest way to code.

There ya have it though, I hope someone finds it useful. Like I said at the beginning of this post, there are better solutions out there for this. I know the community at AutoIT have made some nice tooling around SAPGUI scripting. It would be nice if we could bring more of it into PowerShell somehow.

P.S. If you’re looking for a better script recorder than the SAPGUI built-in tooling http://tracker.stschnell.de/ is the best I’ve found.

Have you ever used the magical cmdlet called Show-Command? Ever wonder how it knows what type of GUI form object to assign to each particular parameter in your cmdlet? Did you even know it works with any cmdlet?

It is a pretty magical cmdlet. One with which you should play sometime. Today, I am going to show you briefly how to see the same data that is used to generate those GUIs. All you would need to do is determine the specific type (i.e. string, integer, boolean) and map that to a specific form object. for example if a parameter expected a string input you would map that to a simple text input. If it expected a boolean you would map it to a checkbox. Cool, but how would I store that information for my custom cmdlets and how would I access that information? The answers are not as bad as you would think.

Storing the info = Params Getting the info = Get-Command

Params

All the information regarding what information you expect to be given to your cmdlet is defined in your params. See this article for a simple walkthrough on params. It’s an oldie but a goodie.

Get-Command

Anyone who has watched a PowerShell tutorial knows Get-Command. It’s one of the first things they tell you. Not sure what you are looking for? Get command takes wildcards! Use it to search for the command you are looking for like this:

 

Get-Command *SQL*

 

It should return any cmdlets that have the keyword “SQL” in them! COOL! WOW! AWESOME! We then leave the cmdlet and go on to other awesome things. Meanwhile… Get-Command is saying… Wait! I can do SO much more for you!

Check out how much info I can get on a simple command like Read-Host. You can see the Parameters property should fulfill our needs.

 

(Get-Command Read-Host).Parameters | ConvertTo-Json -Depth 1

 

ALLLLL the info!

 

{
    "Prompt":  {
                   "Name":  "Prompt",
                   "ParameterType":  "System.Object",
                   "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                   "IsDynamic":  false,
                   "Aliases":  "",
                   "Attributes":  "System.Management.Automation.AllowNullAttribute System.Management.Automation.ParameterAttribute",
                   "SwitchParameter":  false
               },
    "AsSecureString":  {
                           "Name":  "AsSecureString",
                           "ParameterType":  "switch",
                           "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                           "IsDynamic":  false,
                           "Aliases":  "",
                           "Attributes":  "System.Management.Automation.ParameterAttribute",
                           "SwitchParameter":  true
                       },
    "Verbose":  {
                    "Name":  "Verbose",
                    "ParameterType":  "switch",
                    "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                    "IsDynamic":  false,
                    "Aliases":  "vb",
                    "Attributes":  "System.Management.Automation.AliasAttribute System.Management.Automation.ParameterAttribute",
                    "SwitchParameter":  true
                },
    "Debug":  {
                  "Name":  "Debug",
                  "ParameterType":  "switch",
                  "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                  "IsDynamic":  false,
                  "Aliases":  "db",
                  "Attributes":  "System.Management.Automation.AliasAttribute System.Management.Automation.ParameterAttribute",
                  "SwitchParameter":  true
              },
    "ErrorAction":  {
                        "Name":  "ErrorAction",
                        "ParameterType":  "System.Management.Automation.ActionPreference",
                        "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                        "IsDynamic":  false,
                        "Aliases":  "ea",
                        "Attributes":  "System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                        "SwitchParameter":  false
                    },
    "WarningAction":  {
                          "Name":  "WarningAction",
                          "ParameterType":  "System.Management.Automation.ActionPreference",
                          "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                          "IsDynamic":  false,
                          "Aliases":  "wa",
                          "Attributes":  "System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                          "SwitchParameter":  false
                      },
    "InformationAction":  {
                              "Name":  "InformationAction",
                              "ParameterType":  "System.Management.Automation.ActionPreference",
                              "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                              "IsDynamic":  false,
                              "Aliases":  "infa",
                              "Attributes":  "System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                              "SwitchParameter":  false
                          },
    "ErrorVariable":  {
                          "Name":  "ErrorVariable",
                          "ParameterType":  "string",
                          "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                          "IsDynamic":  false,
                          "Aliases":  "ev",
                          "Attributes":  "System.Management.Automation.AliasAttribute System.Management.Automation.ParameterAttribute System.Management.Automation.Internal.CommonParameters+ValidateVariableName",
                          "SwitchParameter":  false
                      },
    "WarningVariable":  {
                            "Name":  "WarningVariable",
                            "ParameterType":  "string",
                            "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                            "IsDynamic":  false,
                            "Aliases":  "wv",
                            "Attributes":  "System.Management.Automation.Internal.CommonParameters+ValidateVariableName System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                            "SwitchParameter":  false
                        },
    "InformationVariable":  {
                                "Name":  "InformationVariable",
                                "ParameterType":  "string",
                                "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                                "IsDynamic":  false,
                                "Aliases":  "iv",
                                "Attributes":  "System.Management.Automation.Internal.CommonParameters+ValidateVariableName System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                                "SwitchParameter":  false
                            },
    "OutVariable":  {
                        "Name":  "OutVariable",
                        "ParameterType":  "string",
                        "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                        "IsDynamic":  false,
                        "Aliases":  "ov",
                        "Attributes":  "System.Management.Automation.Internal.CommonParameters+ValidateVariableName System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                        "SwitchParameter":  false
                    },
    "OutBuffer":  {
                      "Name":  "OutBuffer",
                      "ParameterType":  "int",
                      "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                      "IsDynamic":  false,
                      "Aliases":  "ob",
                      "Attributes":  "System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute System.Management.Automation.ValidateRangeAttribute",
                      "SwitchParameter":  false
                  },
    "PipelineVariable":  {
                             "Name":  "PipelineVariable",
                             "ParameterType":  "string",
                             "ParameterSets":  "System.Collections.Generic.Dictionary`2[System.String,System.Management.Automation.ParameterSetMetadata]",
                             "IsDynamic":  false,
                             "Aliases":  "pv",
                             "Attributes":  "System.Management.Automation.Internal.CommonParameters+ValidateVariableName System.Management.Automation.ParameterAttribute System.Management.Automation.AliasAttribute",
                             "SwitchParameter":  false
                         }
}



 

So I had an idea the other day. Wouldn’t it be cool if I could run EVERY single possible command combinations for my PoshProgressBar Module?

That would be awesome! Wny would that be so awesome?

  1. Automated testing – To guarantee that every single progress bar combination actually ran as expected
  2. Screenshots – The website has a cool dynamic command builder form to fill out that will generate your PowerShell command. It would be sweet to hit “Preview” and see a screenshot of exactly what that particular progress bar looks like.

So… doesn’t sound too bad right? All of the parameters you can use in the module have parameter sets so the possible inputs are definitely limited.

I also know you can get to all that metadata using Get-Command Stay tuned for a later blog post. But would I be able to get all the combinations?

Let’s start doing the math… wait… math is hard… let’s keep it simple.

I have two arrays of three words each:

  1. Cat’s are scary
  2. Dog’s are cool

I have three possible combinations of the first word “Cat’s” and each of the three words in the second array. Then I have three words in the first array so I can put it simply as 3 x 3 = 9 combinations.

If I add to that a third array of three words we get 33 or 27 possibilities. Yikes this could get complicated to build especially with the amount of values I have in each of my parameter sets.

I did however get a basic function together that would generate all possible combinations. It does not take into consideration parameter sets or required parameters yet though.

Now I just have to find the time to run through all 97920 possible command lines for my New-ProgressBar cmdlet with an Invoke-Expression calling them followed by a cmdlet to grab them with a screenshot.

One screenshot a second though… this is going to take awhile…

Check out the code

 

function Get-StringCombinations
{

    Param(
        $MultiArray
    )
    
    function Recursive-Combine($MultiArray, $Count, $String)
    {

        foreach ($SubString in $MultiArray[$Count])
        {

             if( $Count -lt ( $MultiArray.count - 1 ) )
             {

                
                Recursive-Combine -MultiArray $MultiArray -Count ( $Count + 1 ) -String "$String $SubString"

             }
             else
             {

                "$String $SubString"

             }

        }

    }

    foreach ($String in $MultiArray[0]) {
            
            Recursive-Combine -MultiArray $MultiArray -Count ($Count+1) -String $String

    }

}

$CommonParamaters = @(
    "Verbose", "Debug", 
    "ErrorAction", "WarningAction", 
    "InformationAction", "ErrorVariable", 
    "WarningVariable", "InformationVariable", 
    "OutVariable", "OutBuffer", 
    "PipelineVariable"
    )

function Get-AllParameters ($Command)
{

    $Command = Get-Command $Command

    $Parameters = $Command.Parameters.Keys.ForEach({$Command.Parameters[$_]}) | where {$CommonParamaters -notcontains $_.Name}

    $ValidateSetParameters = $Parameters | where { 
    
        ( $_.Attributes | foreach { $_.TypeId.FullName } ) -contains "System.Management.Automation.ValidateSetAttribute" 
        
    }
    
    $AllParameters = @()

    foreach($Parameter in $ValidateSetParameters) { 
        
        $Array = @()

        ($Parameter.Attributes | where { 
        
                            $_.TypeId.FullName -eq "System.Management.Automation.ValidateSetAttribute" 
                        
                        } ).validValues | foreach { $Array += ( "-$($Parameter.Name) $_") } 

        $Array += ("")

        $AllParameters += @(,$Array)
        
    }

    $OtherParameters = $Parameters | where { $ValidateSetParameters -notcontains $_ }


    $PossibleValues = @{

        "System.String"="TestString"
        "System.String[]"=@("TestString1","TestString2")
        "System.Boolean"=@('$True','$False')

    }

    foreach ($Parameter in $OtherParameters) {
        
        $Array = @()

        if($Parameter.ParameterType.FullName -eq "System.Management.Automation.SwitchParameter")
        {

            $Array += ("-$($Parameter.Name)")

        } else {

            $PossibleValues["$($Parameter.ParameterType.FullName)"] | foreach {
        
                $Array += ("-$($Parameter.Name) $_")
        
            }

        }

        $Array += ("")

        $AllParameters += @(,$Array)

    }

    return $AllParameters

}

$AllParameters = Get-AllParameters -Command "New-ProgressBar"
$AllPossibleCommandlines = Get-StringCombinations $AllParameters