WPF & PowerShell -- Part 4 (XAML & Show-Control)

WPF & PowerShell -- Part 4 (XAML & Show-Control)

  • Comments 9

We're not halfway through our week of WPF, and I'm pretty sure at this point that you have an all right grounding in the basics of WPF & PowerShell, but so far, the scripts haven't really been very much like most PowerShell scripts, and the UIs have not been like most WPF UIs.  The scripts have been a little odd because they haven't really made use of a common convention of PowerShell... the pipeline.   The WPF examples have also been a little verbose because they do not include any XAML.

XAML is an XML format you can use to write user interfaces that use WPF.  Because WPF is very property oriented, it is often more concise to write an interface using XAML than it is to write it by setting properties in PowerShell.  XAML also has many developer tools, such as Microsoft Blend and Visual Studio, to help design your application.  However, XAML cannot contain event handlers.  In order to make writing interfaces in XAML easier, I've written Show-Control. a function that takes a control, shape, or XAML document from the pipeline and a dictionary of ScriptBlocks as the event handlers.  Show-Control is a little large (~ 90 lines), so lets walk through how we can make the examples shorter with XAML & Show-Control.  Show-Control's source code will be at the bottom.

Here are all of the examples from Part 1, Part 2, and Part 3, redone to use Show-Control

Large Font Hello World (was 7 lines, now 1 line):

"<Label xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' FontSize='24'>Hello World</Label>" | Show-Control

InkCanvas (was 7 lines, now 1 line):

"<InkCanvas xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' />" | Show-Control

Random Circle (was 9 lines, now 6 lines)

$circleSize = Get-Random -min 200 -max 450
$color = "Red", "Green","Blue","Orange","Yellow" | Get-Random
"<Ellipse xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
  Width='$circleSize'
  Height='$circleSize'
  Fill='$color' />" |  Show-Control

Slider (was 8 lines, now 1 line)

"<Slider xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Minimum='1' Maximum='10'/>"| Show-Control

Label & Textbox (was 11 lines, now 6 lines)

@"
<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<Label FontSize='20'>Type Something</Label>
<TextBox />
</StackPanel>
"@ |  Show-Control

Click & Close (was 6 lines, now 3 lines)

@"
<Button FontSize='20' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>Click Me</Button>
"@ | Show-Control @{"Click" = {$window.close()}}

Select-Command (was 26 lines, now 19)

@"
<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<Label FontSize='14'>Type a Command</Label>
<TextBox Name="CommandTextBox"/>
<ListBox Name="CommandListBox" Width='200' Height='200'/>
<Button Name="SelectCommandButton" FontSize='14'>Select Command</Button>
</StackPanel>
"@ | Show-Control @{
   "CommandTextBox.TextChanged" = {      
       $listBox = $window.Content.FindName("CommandListBox")
       $textBox = $window.Content.FindName("CommandTextBox")
       $listBox.ItemsSource = @(Get-Command "*$($textbox.Text)*" | % { $_.Name })
   }
   "CommandListBox.SelectionChanged" = {
       $textBox = $window.Content.FindName("CommandTextBox")
       $textBox.Text = $this.SelectedItem
   }
   "SelectCommandButton.Click" = {$window.Close()}
}

Drag & Drop (was 33 lines, now 31)

@"
<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<Label FontSize='14'>Drag Scripts Here, DoubleClick to Run</Label>
<ListBox Name="CommandListBox" AllowDrop='True' Height='200'/>
<Button Name="RunCommandButton" FontSize='14'>Run File</Button>
<Button Name="ClearCommandButton" FontSize='14'>Clear List</Button>
</StackPanel>
"@ | Show-Control @{
   "CommandListBox.MouseDoubleClick" = {
       Invoke-Expression "$($this.SelectedItem)" -ea SilentlyContinue
   }
   "CommandListBox.Drop" = {
       $files = $_.Data.GetFileDropList()
       foreach ($file in $files) {
           if ($file -is [IO.FileInfo]) {
               $displayedFiles = $file
           } else {
               $displayedFiles += dir $file -recurse | ? { $_ -is [IO.FileInfo]} | % { $_.FullName }
           }           
       }
       $listBox.ItemsSource = $displayedFiles | sort   
   }
   "RunCommandButton.Click" = {
       $listBox = $window.Content.FindName("CommandListBox")
       Invoke-Expression "$($listbox.SelectedItem)" -ea SilentlyContinue
   }
   "ClearCommandButton.Click" = {
       $window.Content.FindName("CommandListBox").ItemsSource=@()
   }
}

Finally, in order to make it easier for you to script WPF with PowerShell, I'm going to give you the Show-Control function that helped make our scripts shorter.  I would recommend putting this function in a module file or your profile, so it's always there when you need it:

Show-Control

# Displays one or more controls.
# Controls are piped to Show-Control as XAML or as a .NET object.
# Events are passed as a dictionary of name/scriptblocks
# Event Hashtable keys can be like
#   EVENTNAME (event on the object piped to Show-Control)
#   TARGET.EVENTNAME (event on the named object within the control)
#   WINDOW.EVENTNAME (event on the window)
function Show-Control {
  cmdlet -DefaultParameterSet VisualElement
 
   param(
   [Parameter(Mandatory=$true,
   ParameterSetName="VisualElement",
   ValueFromPipeline=$true,
   ValueFromPipelineByPropertyName=$true)]     
   [Windows.Media.Visual]
   $control,    
   [Parameter(Mandatory=$true,
   ParameterSetName="Xaml",
   ValueFromPipeline=$true,
   ValueFromPipelineByPropertyName=$true)]     
   [string]
   $xaml,    
   [Parameter(ValueFromPipelineByPropertyName=$true,Position=0)]     
   [Hashtable]
   $event,
   [Hashtable]
   $windowProperties
   )

   Begin
   {
       $window = New-Object Windows.Window
       $window.SizeToContent = "WidthAndHeight"
       if ($windowProperties) {
           foreach ($kv in $windowProperties.GetEnumerator()) {
               $window."$($kv.Key)" = $kv.Value
           }
       }
       $visibleElements = @()
       $windowEvents = @()
   }

   Process
   {      
       switch ($psCmdlet.ParameterSetName)
       {
       "Xaml" {
           $f = [System.xml.xmlreader]::Create([System.IO.StringReader] $xaml)
           $visibleElements+=([system.windows.markup.xamlreader]::load($f))      
       }
       "VisualElement" {
           $visibleElements+=$control
       }
       }
       if ($event) {
           $element = $visibleElements[-1]      
           foreach ($evt in $event.GetEnumerator()) {
               # If the event name is like *.*, it is an event on a named target, otherwise, it's on any of the events on the top level object
               if ($evt.Key.Contains(".")) {
                   $targetName = $evt.Key.Split(".")[1].Trim()
                   if ($evt.Key -like "Window.*") {
                       $target = $window
                   } else {
                       $target = ($visibleElements[-1]).FindName(($evt.Key.Split(".")[0]))                  
                   }                      
               } else {
                   $target = $visibleElements[-1]
                   $targetName = $evt.Key
               }
               $target | Get-Member -type Event |
                 ? { $_.Name -eq $targetName } |
                 % {
                   $eventMethod = $target."add_$targetName"
                   $eventMethod.Invoke($evt.Value)
                 }              
           }
       }
    }

    End
    {
        if ($visibleElements.Count -gt 1) {
            $wrapPanel = New-Object Windows.Controls.WrapPanel
            $visibleElements | % { $null = $wrapPanel.Children.Add($_) }
            $window.Content = $wrapPanel
        } else {
            if ($visibleElements) {
                $window.Content = $visibleElements[0]
            }
        }
        $null = $window.ShowDialog()
    }
}

Now that I've introduced you to XAML, and given you a nice function to make using WPF in PowerShell, we're ready to really start making some functions that make WPF controls.  Stay tuned.

Hope this helps,

James Brundage [MSFT]

Leave a Comment
  • Please add 4 and 6 and type the answer here:
  • Post
  • Terrific series.

    There is a 'close brace' missing in the Show-Control.

    The following replacement works.

    if ($evt.Key -like "Window.*") {

     $target = $window

     } else {

       $target = ($visibleElements[-1]).FindName(($evt.Key.Split(".")[0]))

    }                  

  • PingBack from http://www.alvinashcraft.com/2008/05/25/dew-drop-may-25-2008/

  • I fixed the missing brace, sorry.  Window events were a last minute change, and demonstrate the perils of last minute changes.

    James Brundage [MSFT]

  • Unable to load, I get a missing closing ')' on line 9 when cut and pasting into my profile.

  • not working  

    "Missing closing ')' in expression."

  • In order to make it work in PowerShell v2 RTM, be sure to remove the line "cmdlet -DefaultParameterSet VisualElement".

  • Just FYI, slider no longer returns a value in this example.

  • Excellent post.  Quick question.  If I run a powershell script event that runs a native command (i.e. ipconfig), the output is not displayed in the console window from which the script was started.  If I pipe of the native command to Out-Host it works.  Does WPF actual redirect console output by default?

  • #Here is the full function that worked for me:

    function Show-Control {

    param([Parameter(Mandatory=$true, ParameterSetName="VisualElement", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [Windows.Media.Visual] $control, [Parameter(Mandatory=$true, ParameterSetName="Xaml", ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $xaml, [Parameter(ValueFromPipelineByPropertyName=$true,Position=0)][Hashtable] $event, [Hashtable] $windowProperties)

      Begin

      {

          $window = New-Object Windows.Window

          $window.SizeToContent = "WidthAndHeight"

          if ($windowProperties) {

              foreach ($kv in $windowProperties.GetEnumerator()) {

                  $window."$($kv.Key)" = $kv.Value

              }

          }

          $visibleElements = @()

          $windowEvents = @()

      }

      Process

      {      

          switch ($psCmdlet.ParameterSetName)

          {

          "Xaml" {

              $f = [System.xml.xmlreader]::Create([System.IO.StringReader] $xaml)

              $visibleElements+=([system.windows.markup.xamlreader]::load($f))      

          }

          "VisualElement" {

              $visibleElements+=$control

          }

          }

          if ($event) {

              $element = $visibleElements[-1]      

              foreach ($evt in $event.GetEnumerator()) {

                  # If the event name is like *.*, it is an event on a named target, otherwise, it's on any of the events on the top level object

                  if ($evt.Key.Contains(".")) {

                      $targetName = $evt.Key.Split(".")[1].Trim()

                      if ($evt.Key -like "Window.*") {

                          $target = $window

                      } else {

                          $target = ($visibleElements[-1]).FindName(($evt.Key.Split(".")[0]))                  

                      }                      

                  } else {

                      $target = $visibleElements[-1]

                      $targetName = $evt.Key

                  }

                  $target | Get-Member -type Event |

                    ? { $_.Name -eq $targetName } |

                    % {

                      $eventMethod = $target."add_$targetName"

                      $eventMethod.Invoke($evt.Value)

                    }              

              }

          }

       }

       End

       {

           if ($visibleElements.Count -gt 1) {

               $wrapPanel = New-Object Windows.Controls.WrapPanel

               $visibleElements | % { $null = $wrapPanel.Children.Add($_) }

               $window.Content = $wrapPanel

           } else {

               if ($visibleElements) {

                   $window.Content = $visibleElements[0]

               }

           }

           $null = $window.ShowDialog()

       }

    }

    #calling the function to display Ink Canvas

    "<InkCanvas xmlns='schemas.microsoft.com/.../presentation& />" | Show-Control

Page 1 of 1 (9 items)