Running WPF/XAML Code in Pow

Posted: April 13, 2017 in Uncategorized

I know I had planned to do the second part of the drain script, but I will come back to that.  (I am currently redesigning it)..  As part of one of my current projects, I am building a Windows Presentation Foundation (WPF) based GUI tool.  To that end, I started with Bo Prox’s excellent introductory articles on this subject.

In order to build the GUI, Bo recommends using Visual Studio Community Edition (excellent choice, since the other free options all seem to be “write the code, see the result” as opposed to WYSIWYG).  As part of the XAML code that gets created, he recommends that you remove 2 attributes from the default element, since it causes issues.  (I have forgotten to do this a couple of times on accident, and your PS script will absolutely throw terminating errors that make *no* sense..)

Now, as I went through multiple rounds of design changes, adjustments, etc., I became frustrated with having to constantly having to make copies (copy & paste) and then removing the attributes.  So, like any other scripter, I put together a small script to handle this automatically and save myself a *lot* of hassle.

The first part of the function is very straightforward:

function Run-XAMLCode
{
[cmdletbinding()]
param (
[parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateScript( { Test-Path -Path $_ } )]
[String]$XAMLPath
)

I’ve gone through detailed explanations on the start of advanced functions before, but for a quick overview:

  1. Declare the function name
  2. Assign [cmdletbinding()] which gives you the Common parameters (-verbose, -whatif, etc.)
  3. Create a new parameter that is required, and accepts input from the pipeline (not a critical item, but I typically just do this on every parameter)
  4. The ValidateScript line will throw an error if the supplied path can’t be found (just a nice piece of validation error that will kill the script if the path is mistyped accidentally)
  5. Declare the variable itself as a string ($XAMLPath)

The next 3 lines are important pieces, and in the process of creating some other scripts I learned something important..

Add-Type -AssemblyName PresentationFramework -ErrorAction SilentlyContinue
Add-Type -AssemblyName PresentationCore -ErrorAction SilentlyContinue
Add-Type -AssemblyName WindowsBase -ErrorAction SilentlyContinue

These 3 lines load the assemblies needed to run WPF forms. Without these, you will very likely have errors trying to run a WPF form. The point that I learned was that you don’t need to use the [System.Reflection.Assembly]::LoadWithPartialName(”) construction, you can simply use Add-Type to load them, and that is not being deprecated 🙂

The next two lines are very straightforward also.. create an XML object, and load the XAML file.

$XAML = New-Object -TypeName XML
$XAML.Load($XAMLPath)

And these lines are the critical step — removing the problematic attributes. Looking at the XAML code, the names feel unusual since they have the colons in the name. The only thing to be cautious about is if you change the main Window name in the form.. by default it is ‘Window’ which is what the script assumes. If you manually change that name, you will need to adjust these lines..

$XAML.Window.RemoveAttribute('mc:Ignorable')
$XAML.Window.RemoveAttribute('x:Class')

And the rest of the code is Bo’s code.

$xamlreader = New-Object -TypeName System.Xml.XmlNodeReader($XAML)
$Window = [windows.markup.XAMLReader]::Load($xamlreader)

$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object {
    New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force
}
$Null = $Window.ShowDialog()
  1. Create an XMLNodeReader
  2. Load the XAML file using a XAMLReader
  3. These next lines are a practical usability function to create new variables for each of the controls in the WPF form.  He uses an XPATH query to locate each of the nodes, and create a variable with that same name that points to that node. It’s not strictly necessary, but this easily allows this script to be used as a template for a full blown WPF powershell application, as opposed to just seeing the form (which is what this function is for)
  4. Run the ShowDialog() to show the form.

And as long as the form is up on the screen, the powershell prompt used to run it will wait for the form to exit.

So, in summation, this function will let you run the form repeatedly to show the design without having to manually modify it, and without having to wait for Visual Studio to compile the form into an executable. Simply dot source it in your PS prompt, and you’ll the function ready to go.

Full Code listing:

function Run-XAMLCode
{
	<#
	.SYNOPSIS
		Allows the user to run a XAML form created by Visual Studio.
	
	.DESCRIPTION
		This lets the user run the XAML form repeatedly and quickly without having to manually 'fix' the XAML file each time.  
		The primary purpose of this is to be able to quickly make changes in the form, and then check the "look and feel" without having to modify it, or wait for VS to compile it.
	
	.PARAMETER XAMLPath
		This is the path to the XAML file.  It needs to either be the full path to the file, or it must be in the working directory for the script. 
	
	.EXAMPLE
		PS C:\> Run-XAMLCode -XAMLPath "$env:userprofile\Documents\VisualStudio\MyProject\MyProject.xaml"
	
	.NOTES
		This function is based on Bo Prox' article on creating WPF forms.
		The output is just the running WPF form, not an addressable XMLDocument
	
	.INPUTS
		system.string
	
	.OUTPUTS
		system.xml.xmldocument
	
	.LINK
		https://mcpmag.com/articles/2016/04/28/building-ui-using-powershell.aspx
		https://mcpmag.com/Articles/2016/05/05/WPF-and-PowerShell-Part-2.aspx?Page=2
	#>
	[cmdletbinding()]
   	param (
	    [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript( { Test-Path -Path $_ } )]
	    [String]$XAMLPath
    )

    Add-Type -AssemblyName PresentationFramework -ErrorAction SilentlyContinue
    Add-Type -AssemblyName PresentationCore -ErrorAction SilentlyContinue
    Add-Type -AssemblyName WindowsBase -ErrorAction SilentlyContinue

    $XAML = New-Object -TypeName XML
    $XAML.Load($XAMLPath)
    $XAML.Window.RemoveAttribute('mc:Ignorable')
    $XAML.Window.RemoveAttribute('x:Class')

    $xamlreader = New-Object -TypeName System.Xml.XmlNodeReader($XAML)
    $Window = [windows.markup.XAMLReader]::Load($xamlreader)

    Write-Verbose -Message "Create variables for each control, based off of the name"
    $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object {
        New-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force
    }

    $Null = $Window.ShowDialog()
}

I hope you find it as useful as I have..

David F.
(updated to add the help to the code)
 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s