Monday 21 May 2012

Dynamic form in Powershell


I needed an easy way to create forms dynamically on the fly in PowerShell. As usual, I looked to Google, more out of sheer laziness than desperation. Being a self-proclaimed power-searcher I thought I'd have it licked in no time. So many generous coders out there willing to share their secrets and show off their 'mad skillz'.

Unsurprisingly I was able to find tonnes of useful information, however, much to my surprise, after poking around for the good part of an hour I did not find what I was looking for, nor did my attempts to hack it out on the command line bear any fruit.

So I threw in the search towel (which is a hard thing for a self-proclaimed 'power-seacher' to admit) and put on my under-used spinner hat and started to create a new function. After diving in and out of it for a week, and quite a few lightbulb moments, I created exactly what I was looking for. And what better way to celebrate than leaving the cocoon I entered as a 'code leech' to emerge as one of the more admiral sharers of this wonderful World-Wide-Web.

So here it is, my first instalment on this newfangled blog thingamajiggy....






<#
      FUNCTION   : Flexible dynamic form control populator.
      CREATED BY : Brendan DeCelis
      DATE       : 15/05/2012
      VERSION        : v0.3
      LAST UPDATE: Brendan  DeCelis, 21/05/2012
     
      USAGE      : The function "Test-Form" loads a very straightforward sample form using simply a
                              direct text string. It's unlikely you would bother going to the effort for
                              this usage, but it is an easy to follow example.
                             
                              The function "Test-DynamicForm" shows how this might be applied to create a
                              form using variables as the input to creating the form. This is where the
                              use for this script comes into its own.
                       
                              The usage is documented extensively within each function.

      LICENSE    : May be used and distributed freely, with acknowledgement to the creator under
                        GNU General Public License v3.0. See http://www.gnu.org/licenses/gpl.html
     
      DISCLAIMER : These functions are provided "AS IS" and "WITH ALL FAULTS,"
                              without warranty of any kind, including without limitation the warranties
                              of merchantability, fitness for a particular purpose and non-infringement.
                              The creator makes no warranty that these functions are free of defects or
                              is suitable for any particular purpose. In no event shall the creator be
                              responsible for loss or damages arising from the installation or use of
                              these functions, including but not limited to any indirect, punitive, special,
                              incidental or consequential damages of any character including, without
                              limitation, damages for loss of goodwill, work stoppage, computer failure
                              or malfunction, or any and all other commercial damages or losses. The entire
                              risk as to the quality and performance of the Software is borne by you.
                              Should any part of these functions prove defective, you and not the creator
                              assume the entire cost of any service and repair.
                             
      UPDATE 1   : 21/05/2012 - Updated to include "noResize" option to prevent auto-resizing of
                                    the form. Particularly handy if using to update an existing form
                                          of set dimensions.
      UPDATE 2   : 21/05/2012 - Updated to allow form variable to be passed as a string and new form
                                          to be created if it does not already exist. Seems like a lame way to do
                                          this. Hope to find a better way, however passing a null variable as the
                                          form  variable kills the function at the moment. This works well enough.
#>

#region Main Function - populate form from string

function Populate-FormFromString
{
      <#
            NB This will populate an existing form.
            You will need to create the form previous to calling this function, and display the form dialog outside of this function.
            You can do this be creating a wrapping function, such as:
                  >     $testForm = New-Object System.Windows.Forms.Form
                  >     Populate-FormFromString $testForm "CheckBox||chkNewChk1||This is not checked - vanilla;;`
                                                                              CheckBox||chkNewChk2||This checked and blue||Checked==`$true^^ForeColor==`"Blue`";;`
                                                                              Button||btnClose||Click My Red Spot||add_Click==`$testForm.Dispose()^^ForeColor==`"Red`""
                  >     $testForm.ShowDialog()
           
            The controls are separated by a double-delimiter.
            A custom function called "Split-MultiCharDelimiter" has been defined to process this delimiter and must be available to this function.
           
            Controls to add input format:
            Each control is separated by the delimiter ";;"
            Each control varaiable is separated by "||"
               The 4 variables (in order) that can be defined are:
                 Control Type - Mandatory, can be a Checkbox, Button, Label, TextBox or potentially any other label (not tested)
                  Control Name - The name to give the
             Each action or control variable separated by "^^",
               action / variable type and action / variable value separated by "=="
                  Type | Name | DefaultText/Title | Enabled/Checked/Codeblock
            ie. Populate-FormFromString "myForm" "CheckBox||chkMyNewCheckbox||This is a checkbox||Checked==`$true"
                  Populate-FormFromString "frmADSIUser" "CheckBox||chkNewChk1||This is not checked - vanilla;;`
                                                            CheckBox||chkNewChk2||This checked and blue||Checked==`$true^^ForeColor==`"Blue`";;`
                                                            Button||btnClose||Click My Red Spot||add_Click==`$frmADSIUser.Dispose()^^ForeColor==`"Red`""
            The form will automatically resize to the outer dimensions of the controls added by the function
                  UNLESS the optional switch -noResize is explicitly declared
      #>
      Param
      (
            #[Parameter(Mandatory = $true, Position = 0)][System.Windows.Forms.Form] $thisForm,
            [Parameter(Mandatory = $true, Position = 0)] [string$thisFormVariable,
            [Parameter(Mandatory = $true, Position = 1)] [string$controlsToAdd,
            [Parameter(Mandatory = $false, Position = 2)]
                    [ValidateSet("Horizontal""Vertical")] [string$controlDirection = "Vertical",
            [Parameter(Mandatory = $false, Position = 3)]    [int$controlsPerDimension = 10,
            [Parameter(Mandatory = $false, Position = 4)]    [int$OriginX = 30,
            [Parameter(Mandatory = $false, Position = 5)]    [int$OriginY = 30,
            [Parameter(Mandatory = $false, Position = 6)]    [int$vertControlSpacing = 30,
            [Parameter(Mandatory = $false, Position = 7)]    [int$horzControlSpacing = 80,
            [Parameter(Mandatory = $false)]               [switch$noResize,
            [Parameter(Mandatory = $false)]               [switch$showForm
      )
     
      Process
      {
     
            # If the form has not yet been created, create using black magic
            [bool$global:formExists = $true
            #[string] $formCheck = "if (`$$thisFormVariable -eq `$null){`$$thisFormVariable = new-object System.Windows.Forms.Form;`$createForm = `$executioncontext.InvokeCommand.NewScriptBlock(`$codeForForm);Invoke-Command `$createForm;}"
            [string$formCheckString = "if (`$$thisFormVariable -eq `$null){`$global:formExists = `$false}"
            $formCheck = $executioncontext.InvokeCommand.NewScriptBlock($formCheckString)
            Invoke-Command $formCheck
            if ($formExists -eq $false)
            {
                  $codeForForm = "`$$thisFormVariable = new-object System.Windows.Forms.Form"
                  $createForm = $executioncontext.InvokeCommand.NewScriptBlock($codeForForm)
                  Invoke-Command $createForm
            }
           
            # Split controls if multiple are defined
            $arrControls = @()
            $arrControls = Split-MultiCharDelimiter "$controlsToAdd" ";;"
           
            # Account for existing controls that have been added and set start position accordingly
            $controlCount    = 0
            $stackCount      = 0
            $horzStartPos    = $OriginX
            $vertStartPos    = $OriginY
            [int$maxHeight = 0
            [int$maxWidth  = 0
            foreach($checkControl in $thisForm.Controls)
            {
                  # Check to see if the control was created by this function
                  if ($checkControl.AccessibleDescription -eq "DynamixControl")
                  {
                        $controlCount++
                        $stackCount++
                        if ($controlDirection -eq "Horizontal")
                        {
                              if ($stackCount -eq $controlsPerDimension)
                              {
                                    $OriginX = $horzStartPos
                                    $OriginY += $vertControlSpacing
                                    $stackCount = 0
                              }
                              else
                              {
                                    $OriginX += $horzControlSpacing
                              }
                        }
                        else
                        {
                              if ($stackCount -eq $controlsPerDimension)
                              {
                                    $OriginY = $vertStartPos
                                    $OriginX += $horzControlSpacing
                                    $stackCount = 0
                              }
                              else
                              {
                                    $OriginY += $vertControlSpacing
                              }
                        }
                  }
                  #check for maximum height and width of form
                  if (($OriginX + $horzControlSpacing-gt $maxWidth)
                  {
                        $maxWidth = ($OriginX + $horzControlSpacing)
                  }
                  if (($OriginY + $vertControlSpacing-gt $maxHeight)
                  {
                        $maxHeight = ($OriginY + $vertControlSpacing)
                  }
            }
           
            foreach($control in $arrControls)
            {
                  $arrControlProperties = Split-MultiCharDelimiter "$control" "||"
                  $controlParameterCount = $arrControlProperties.GetUpperBound(0)
                  $controlToCreate = $arrControlProperties[0].ToString()
                  #Replace any funky characters in the control type, which may be created by formatting as per the example in Load-TestForm
                  $controlToCreate = $controlToCreate -Replace('[^\w\.]','')
                  $codeForControl = "`$global:newControl = New-Object System.Windows.Forms.$controlToCreate"
                  $createControl = $executioncontext.InvokeCommand.NewScriptBlock($codeForControl)
                  Invoke-Command $createControl
                  $newcontrol.AccessibleDescription = "DynamixControl"
                  #The second part of the control string contains the name of the control object
                  if ($controlParameterCount -ge 1)
                  {
                        $newControl.Name = $arrControlProperties[1].ToString().Replace(" ","")
                        # The third part of the control string contains the text property of the control
                        if ($controlParameterCount -ge 2)
                        {
                              $newControl.Text = $arrControlProperties[2]
                              # The fourth part of the object contains any additional values with
                              #     property name and value separated by "==" and additional properties separated by "^^"
                              if ($controlParameterCount -ge 3)
                              {
                                    $arrControlDefinitions = Split-MultiCharDelimiter "$($arrControlProperties[3])" "^^"
                                    foreach ($definition in $arrControlDefinitions)
                                    {
                                          $arrPropertyAndValue = Split-MultiCharDelimiter "$definition" "=="
                                          #foreach ($action in $arrPropertyAndValue)
                                          #{
                                                [string]$ActionType = $arrPropertyAndValue[0]
                                                [string]$Action     = $arrPropertyAndValue[1]
                                                if ($ActionType.Contains("_"))
                                                {
                                                      $newAction = "`$newControl.$ActionType(`$executioncontext.InvokeCommand.NewScriptBlock({$Action}))"
                                                }
                                                else
                                                {
                                                      $newAction = "`$newControl.$ActionType = $Action"
                                                }
                                                Invoke-Expression $newAction
                                          #}
                                    }
                              }
                        }
                        else
                        {
                              $newControl.Text = "$($arrControlProperties[1])"
                        }
                  }
                  else
                  {
                        $newControl.Name = "control$controlCount"
                        $newControl.Text = "control$controlCount"
                  }
                 
                  $newControl.Location = New-Object System.Drawing.Point($OriginX$OriginY)
                  $newControl.AutoSize = $true
                  $newControl.TabIndex = $controlCount
                  $thisForm.Controls.Add($newControl)
                  $controlCount++
                  $stackCount++
                  if ($controlDirection -eq "Horizontal")
                  {
                        if ($stackCount -eq $controlsPerDimension)
                        {
                              $OriginX = $horzStartPos
                              $OriginY += $vertControlSpacing
                              $stackCount = 0
                        }
                        else
                        {
                              $OriginX += $horzControlSpacing
                        }
                  }
                  else
                  {
                        if ($stackCount -eq $controlsPerDimension)
                        {
                              $OriginY = $vertStartPos
                              $OriginX += $horzControlSpacing
                              $stackCount = 0
                        }
                        else
                        {
                              $OriginY += $vertControlSpacing
                        }
                  }
                  #check for maximum height and width
                  if (($OriginX + $horzControlSpacing-gt $maxWidth)
                  {
                        $maxWidth = ($OriginX + $horzControlSpacing)
                  }
                  if (($OriginY + $vertControlSpacing-gt $maxHeight)
                  {
                        $maxHeight = ($OriginY + $vertControlSpacing)
                  }
            }
            # Scale the form to fit controls
            if (!$noResize)
            {
                  if ($thisForm.Width -lt $maxWidth)
                  {
                        $thisForm.Width  = $maxWidth
                  }
                  if ($thisForm.Height -lt $maxHeight)
                  {
                        $thisForm.Height = $maxHeight + $vertControlSpacing + $vertStartPos
                  }
            }
            if ($showForm)
            {
                  $thisForm.ShowDialogue()
            }
      }
}

#endregion

#region Custom split function

function Split-MultiCharDelimiter
{
      Param
      (
            [Parameter(Mandatory = $true, Position = 0)] [string$originalString,
            [Parameter(Mandatory = $true, Position = 1)] [char[]] $delimiterChars
      )
     
      [array$arrMySplit = @()
      [int]   $charsInDelimiter = $delimiterChars.Count
     
      if ($charsInDelimiter -eq 1)
      {
            $arrMySplit = $originalString.Split($delimiterChars)
            return $arrMySplit
      }
      else
      {          
            [int]    $startChar      = 1
            [int]    $currChar       = 1
            [Char[]] $originalChars  = $originalString
            [int]    $charsToProcess = $originalChars.Count
            [bool]   $passedTest     = $true
            [int]    $charsToCount   = 1
            # Step through array and identify empty entries.
            #  Starts at LowerBound + 1 to prevent the script from trying to access a record lower than the lower bound
            for ($char = $startChar$char -le $charsToProcess - 1; $char++)
            {
                  [char[]] $compareChar  = ""
                  [int]    $delimCount   = 0

                  for ($delimiterChar = $currChar$delimiterChar -le ($currChar + $charsInDelimiter - 1); $delimiterChar++)
                  {
                        #$compareChar += $originalChars[$delimiterChar]
                        if ($originalChars[$delimiterChar-ne $delimiterChars[$delimCount])
                        {
                              $passedTest = $false
                              #$charsToCount++
                        }
                  }
                  #if ($compareChar -eq $delimiterChars)
                  if ($passedTest -eq $true)
                  {
                        #Write-Host "Found it at $currChar"
                        $arrMySplit += $originalString.Substring($startChar-1,$charsToCount)
                        $startChar = $currChar + $charsInDelimiter + 1
                        $currChar  = $currChar + $charsInDelimiter
                        $charsToCount = 0
                  }
                  else
                  {
                        $currChar++
                        $charsToCount++
                  }
                  [bool]   $passedTest  = $true
            }
      }
      if (([int]$remaining = $originalString.Length - $startChar + 1) -gt $charsInDelimiter)
      {
            $arrMySplit += $originalString.Substring($startChar-1,$remaining)
      }
      return $arrMySplit
}    

#endregion

#region Test functions

function Test-Form
{
      # Create new instance of a form
      $testForm = New-Object System.Windows.Forms.Form
      # Populate form using a direct string to define the controls
      Populate-FormFromString "testForm" "CheckBox||chkNewChk1||This is not checked - vanilla;;`
                                                            CheckBox||chkNewChk2||This checked and blue||Checked==`$true^^ForeColor==`"Blue`";;`
Button||btnClose||Click My Red Spot||add_Click==`$testForm.Dispose()^^ForeColor==`"Red`""
      # Show the form
      $testForm.ShowDialog()
}


function Test-Form-WithoutExistingForm
{
      # Populate form using a direct string to define the controls, creating a new form from the populate function
      Populate-FormFromString "testForm" "CheckBox||chkNewChk1||This is not checked - vanilla;;`
                                                            CheckBox||chkNewChk2||This checked and blue||Checked==`$true^^ForeColor==`"Blue`";;`
Button||btnClose||Click My Red Spot||add_Click==`$testForm.Dispose()^^ForeColor==`"Red`""
      # Show the form
      $testForm.ShowDialog()
}

function Test-DynamicForm
{
      # Create new instance of a form and provide a title
      $testForm = New-Object System.Windows.Forms.Form
      $testForm.Text = "Testing dynamic form"
     
      #region Test Button - Comment this out to remove pesky button on test dynamic form
      # add a button on the form to show that it does not get counted as a dymaic control
      $testbutton = New-Object System.Windows.Forms.Button
      $testbutton.Text = "Not Dynamic"
      $testbutton.Location = New-Object System.Drawing.Point(50, 70)
      $testForm.Controls.Add($testbutton)
      #endregion
     
      # Populate form with checkboxes named incrementally
      for ($i=1;$i -le 40;$i++)
      {
            [string$controlString = "CheckBox||chkNewChk$i||Checkbox #$i"
            #set every even box to checked and make the text red
           
            if ([bool]!($i%2))
            {
                  $controlString += "||Checked==`$true^^Forecolor==`"Red`""
            }
            #set every odd box to blue, align to middle left and add a click action which disables and enables the maximise box on the form
            else
            {
                  $controlString += "||TextAlign==`"MiddleLeft`"^^Forecolor==`"Blue`"^^add_Click==if(`$testForm.MaximizeBox -eq `$true){`$testForm.MaximizeBox = `$false}else{`$testForm.MaximizeBox = `$true}"
            }
            Populate-FormFromString  "testForm"  $controlString -horzControlSpacing 120
      }
      # Show the form
      $testForm.ShowDialog()
}

#endregion

No comments:

Post a Comment