Integrating XAML into PowerShell

Overview

In laymen's terms, XAML provides a way to write a GUI for PowerShell. If you ever wrote HTA's for VBScript, the concept of how XAML works is much easier to understand. See here for a more detailed explanation of XAML: https://msdn.microsoft.com/en-us/library/ms752059(v=vs.110).aspx. In case you have never head of an HTA see here for more information: https://technet.microsoft.com/en-us/library/ee692768.aspx. This post is not about explaining the minute details of XAML or HTA, its meant to rapidly introduce you to what XAML can do and what better way of doing that than walking through the steps needed to create a quick simple PowerShell driven XAML application?

Compatibility

The following example was created using Visual Studio Express 2013 on Windows 8.1 and PowerShell 2.0. I have also created XAML applications on Windows 7. XAML can be created using nothing more than Notepad and ran on platforms as early as PowerShell 1.0, but I cannot confirm that the sample XAML shown below will work in all scenarios.

Plain Old PowerShell

Lets start with plain old PowerShell. If you wanted to quickly query some basic operating system details how would you do it? The following PowerShell uses the WMI object to query the Win32_OperatingSystem class to retrieve some useful information.

$oWMIOS = Get-WMIObject win32_OperatingSystem

$oWMIOS | fl PSComputerName,name,freephysicalmemory,OSArchitecture,WindowsDirectory,Version,SystemDrive

The results are below:

Of course, if you wanted even more information you could enter $oWMIOS | fl *. But for the purposes of this example, I'm going to stick to just seven attributes. So by using plain old PowerShell you have gotten the exact information you were looking for. This is all good and well, but what about when you want something you can give to end users for those scenarios where you want users to be able to more proactively perform self help activities? Do you really want them typing complex PowerShell commands from the command line? Or what about when you want to limit admins to specific input and ensure repeatable output without typos or mistakes?  Or what about when you simply want a polished application that is based on PowerShell that you can distribute and use like any other application? These types of scenarios is where XAML can be useful.

Tools Needed

Before you convert the previous plain old PowerShell into XAML, you have to have the right tools for the job. Although there are plenty of XAML editors out there, what I like to use is Microsoft's Visual Studio Express which is available here: https://www.microsoft.com/en-us/download/details.aspx?id=40787

Visual Studio Express 2013 WPF Application

After obtaining Visual Studio Express 2013, launch the application and select New Project > WPF Application. The first thing I typically do is rename the MainWindow to something useful, in this example I am going to call it OS Details. I then change the startup location to CenterScreen, and pin the toolbox to the display as shown below.

In the following image, I have added labels, text boxes, and an exit button. I have also changed the appearance of the window to have no border and removed the window controls from the window.

Pressing the Start button within Visual Studio Express reveals the following application.

As you can see, its starting to look like a real application but all of the fields are blank. This is to be expected because there is no PowerShell behind the scenes to fill the application with data. Now its time to marry plain old PowerShell to the XAML application. One of the most important prerequisites to doing so is to name each element of the form that you wish to interact with in PowerShell. For example, in the image above, the text field beside the Hostname label is called Name="txtHostname".

Importing XAML Into PowerShell ISE

Now that your XAML looks exactly the way you want,  open PowerShell ISE (Start > Run > PowerShell ISE) and copy and paste the source XAML code into PowerShell ISE as shown in the following image.

As you can see from the purple coloring, PowerShell ISE does not recognize the imported XAML as valid PowerShell code, which is to be expected. So the first thing you have to do is fix that by adding the following PowerShell code immediately before the XAML code which will create a XML object and store the XAML code in the PowerShell XML object:

[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$XAML = @'

You then have to add the following PowerShell code to terminate the multi-line string variable that contains the XAML code:

'@

Next up, you have to delete the following string from the code:

x:Class="MainWindow"

I typically also do a find and replace to find all instances of "x:" and replace them with nothing. When naming form elements, Visual Studio Express will create the name such as x:Name="txtsomething". PowerShell will not properly interpret the x: in front of the name and will not properly interact with the form element.

The following code then reads all of the XAML form elements stored in the $XAML variable and stores the names for the named elements in PowerShell. It does some error checking to ensure the .NET Framework XamlReader is available then shows the form.

#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. Some possible causes for this problem include: .NET Framework is missing PowerShell must be launched with PowerShell -sta, invalid XAML code was encountered."; exit}

#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================

$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}

#===========================================================================
# Shows the form
#===========================================================================
$Form.ShowDialog() | out-null

Now with all of that out of the way, its time to tie the XAML elements to the back end PowerShell WMI object. I won't go into detail on what every line does, but below is the completed application. With the comments, how it all works is pretty self explanatory. You have the actual XAML code with named elements, which are then stored in a PowerShell XML variable object. From there the back end PowerShell queries the Win32_OperatingSystem class of the WMI object to get some information about the operating system which is then displayed in the XAML GUI by calling the text attribute of the text fields.

The code below may be a bit difficult to read due to the way web browsers wrap web pages. See the attached text file for the complete script.

#==============================================================================================
# XAML Code - Imported from Visual Studio Express WPF Application
#==============================================================================================
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$XAML = @'
<Window
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="OS Details" Height="306" Width="525" WindowStartupLocation="CenterScreen" WindowStyle='None' ResizeMode='NoResize'>
    <Grid Margin="0,0,-0.2,0.2">
        <TextBox HorizontalAlignment="Center" Height="23" TextWrapping="Wrap" Text="Operating System Details" VerticalAlignment="Top" Width="525" Margin="0,-1,-0.2,0" TextAlignment="Center" Foreground="White" Background="#FF98D6EB"/>
        <Label Content="Hostname" HorizontalAlignment="Left" Margin="0,27,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="Operating System Name" HorizontalAlignment="Left" Margin="0,62,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="Available Memory" HorizontalAlignment="Left" Margin="0,97,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="OS Architecture" HorizontalAlignment="Left" Margin="0,132,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="Windows Directory" HorizontalAlignment="Left" Margin="0,167,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="Windows Version" HorizontalAlignment="Left" Margin="0,202,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Label Content="System Drive" HorizontalAlignment="Left" Margin="0,237,0,0" VerticalAlignment="Top" Height="30" Width="170" Background="#FF98D6EB" Foreground="White"/>
        <Button Name="btnExit" Content="Exit" HorizontalAlignment="Left" Margin="0,272,0,0" VerticalAlignment="Top" Width="525" Height="34" BorderThickness="0"/>
        <TextBox Name="txtHostName" HorizontalAlignment="Left" Height="30" Margin="175,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtOSName" HorizontalAlignment="Left" Height="30" Margin="175,62,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtAvailableMemory" HorizontalAlignment="Left" Height="30" Margin="175,97,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtOSArchitecture" HorizontalAlignment="Left" Height="30" Margin="175,132,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtWindowsDirectory" HorizontalAlignment="Left" Height="30" Margin="175,167,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtWindowsVersion" HorizontalAlignment="Left" Height="30" Margin="175,202,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
        <TextBox Name="txtSystemDrive" HorizontalAlignment="Left" Height="30" Margin="175,236,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="343" IsEnabled="False"/>
    </Grid>
</Window>
'@
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. Some possible causes for this problem include: .NET Framework is missing PowerShell must be launched with PowerShell -sta, invalid XAML code was encountered."; exit}

#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================
$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name ($_.Name) -Value $Form.FindName($_.Name)}

#===========================================================================
# Add events to Form Objects
#===========================================================================
$btnExit.Add_Click({$form.Close()})

#===========================================================================
# Stores WMI values in WMI Object from Win32_Operating System Class
#===========================================================================
$oWMIOS = Get-WmiObject win32_OperatingSystem

#===========================================================================
# Links WMI Object Values to XAML Form Fields
#===========================================================================
$txtHostName.Text = $oWMIOS.PSComputerName

#Formats and displays OS name
$aOSName = $oWMIOS.name.Split("|")
$txtOSName.Text = $aOSName[0]

#Formats and displays available memory
$sAvailableMemory = [math]::round($oWMIOS.freephysicalmemory/1000,0)
$sAvailableMemory = "$sAvailableMemory MB"
$txtAvailableMemory.Text = $sAvailableMemory

#Displays OS Architecture
$txtOSArchitecture.Text = $oWMIOS.OSArchitecture

#Displays Windows Directory
$txtWindowsDirectory.Text = $oWMIOS.WindowsDirectory

#Displays Version
$txtWindowsVersion.Text = $oWMIOS.Version

#Displays System Drive
$txtSystemDrive.Text = $oWMIOS.SystemDrive

#===========================================================================
# Shows the form
#===========================================================================
$Form.ShowDialog() | out-null

After running the preceding code in PowerShell ISE, the following GUI should be displayed.

Troubleshooting

  I have ran into some issues with missing .NET framework libraries when running XAML applications on Windows Server 20008 R2. Since .NET Framework is not loaded by default, yet the XML markup reader relies on the .NET Framework libraries to instantiate the XML object, the XAML application will not work without it.

  Another common mistake is to forget to change the names from the Visual Studio Express name of x:Name="myelement" to Name="myelement". The PowerShell script will crash if x:Class="ClassName" is encountered during the opening <Window XML tag.

  Last but not least, if you attempt to run the script with the PowerShell Execution Policy set too high, the script will not be able to run at all.

Wrap Up

In this post I have introduced you to the concept of integrating XAML into PowerShell. I walked through the basic setup of Visual Studio Express 2013, creating a basic PowerShell script, creating a basic XAML GUI with text, label, button, window, and grid elements, and exporting the results to PowerShell ISE where some minor syntax changes are necessary to integrate the results into PowerShell. I then displayed the completed form and provide the full form source code. Stay tuned for more tips on XAML / PowerShell integration. I have written XAML applications that do everything from deploy Active Directory to Exchange to PKI. In future posts we'll create more complex XAML applications that use more interactive XAML elements such as treeviews, tabs, and data grids.

OSDetails.txt