Summary: Microsoft PFE, Chris Conte, talks about adding a GUI for Windows PowerShell scripts.
Microsoft Scripting Guy, Ed Wilson, is here. Today Chris Conte is our guest blogger.
Chris Conte is a premier field engineer at Microsoft who grew up with a passion for computers and the creative arts. At age 12, he started programming by copying assembly game code on his dad's Commodore 64. Eventually, he parlayed that experience into learning about Visual Basic for Applications (VBA), ASP.NET, C#, and a variety of client scripting languages and frameworks. You can learn more about Chris via LinkedIn or Twitter @CrashConte.
Now, Chris...
This is not my original idea, but I’ve been able to document the methodology presented in this post. Before we start, I’d like the opportunity to thank my mentor, Donald Scott, and my colleague Michael Melone. Mike is the guru who figured this out, and Don and I created the helper cmdlet.
Windows Server admins have a lot of choices for automating routine tasks, but for those looking to stay up-to-date, the lingua franca is certainly Windows PowerShell. Windows PowerShell allows you to deal with objects instead of only strings and it has a syntax that C# and JavaScript programmers can easily relate to. In addition, with Microsoft heavily investing to propagate this awesome language throughout the ecosystem, you can be sure it's not going away soon.
So it’s settled, right? Learn Windows PowerShell and thrive. But honestly, I didn't get into Windows to work from the command line. I'm what you call a "GUI Guy." I love Windows, and as a developer, I have spent more than my share of time designing beautiful interfaces for the web. I believe that good design can have a huge influence on the success or failure of the underlying code. Don't get me wrong, I love the power and speed of scripted tasks. But that's why this "secret" is so special. You don't lose anything that Windows PowerShell has to offer.
What do you get when you mix XML, XPath, XAML, WPF, Windows PowerShell and a little creativity? Well, I say it's nothing short of magic. Even after you understand how it works, I hope you still get that little twinkle in your eyes that keeps the inner geek excited.
Tooling up
To follow along, you'll need to get a few tools. You've probably got these lying around. Here's what you'll need:
- Microsoft .NET Framework 4 (Web Installer)
- Windows PowerShell and the Windows PowerShell ISE
The Windows PowerShell ISE is an optional feature, and it requires .NET Framework 3.51 to be installed. To install the Windows PowerShell ISE on Windows Server 2008 R2, use the Add Features Wizard or run the following Windows PowerShell command:
Import-Module ServerManager; Add-WindowsFeature PowerShell-ISE
- Visual Studio 2013 or Visual Studio Express 2013 for Windows Desktop
Visual Studio Express for Windows Desktop lets you take full advantage of Windows with XAML designers, a productive IDE, and a variety of programming languages.
Getting started
I'm going to presume a lot here. I'm going to assume you've got modern Windows Server admin skills and are a proficient Windows PowerShell scripter. I'm also going to assume you've heard of XML and XPath, event-driven programming, and some basics about object-oriented programming. If not, please get started with the following resources.
Note The mention of these resources is not an endorsement.
- Scripting with Windows PowerShell
- Understanding Event Driven Programming - 23
- XPath Tutorial
- Windows Presentation Foundation (WPF) interface fundamentals
WPF and XAML are important concepts to grasp for this methodology. To begin, let's review a bit about WPF and XAML (from the documentation). Here are some significant pieces of the documentation that should help you understand a bit more about why we are using them.
WPF is a next-generation presentation system for building Windows client applications. It is a resolution-independent and vector-based rendering engine. WPF extends the core with a comprehensive set of application development features that include Extensible Application Markup Language (XAML). WPF is included in the Microsoft .NET Framework, so you can build applications that incorporate other elements of the .NET Framework class library.
WPF exists as a subset of .NET Framework types that are for the most part located in the System.Windows namespace. You instantiate classes, set properties, call methods, and handle events by using your favorite .NET Framework programming language.
WPF offers the ability to develop an application by using markup and code-behind, which is an experience that ASP.NET developers should be familiar with. You generally use XAML markup to implement the appearance of an application while using managed programming languages (code-behind) to implement its behavior.
XAML is an XML-based markup language that is used to implement an application's appearance declaratively. It is typically used to create windows, dialog boxes, pages, and user controls, and to fill them with controls, shapes, and graphics. Because XAML is XML-based, the UI that you compose with it is assembled in a hierarchy of nested elements known as an element tree.
Building the interface
To create the interface, fire up Visual Studio and create a new WPF project. The path is: File > New > Project.
When the project loads, you will see a prebuilt Window. Simply drag-and-drop controls onto the canvas from common WPF controls. In the following screenshot, I've added a text box and a button. Notice the handles that surround the control. You are welcome to use these handles to resize the control.
Notice the XAML in the window under the design canvas. You can see a Label and Button control added to the Grid element. Use the XAML resource documentation if you want to get fancy, but you now have all the XAML you need to continue. Simply copy and XAML and paste it into a Notepad window. Remove the x:Class attribute from the Window element. Leave the XML namespace elements (xmlns and xmlns:x), Title, Height, and Width. Save the file with the .xaml extension (for example, MyForm.xaml). Here is the code:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Label Content="Label" HorizontalAlignment="Left" Margin="68,38,0,0" VerticalAlignment="Top" Width="197"/>
<Button Content="Button" HorizontalAlignment="Left" Margin="307,41,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
Loading the dialog box
To make this easier (but admittedly more complex to understand), you'll need the following helper cmdlet to load and interpret the XAML. Simply save the following text as loadDialog.ps1:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,Position=1)]
[string]$XamlPath
)
[xml]$Global:xmlWPF = Get-Content -Path $XamlPath
#Add WPF and Windows Forms assemblies
try{
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
} catch {
Throw "Failed to load Windows Presentation Framework assemblies."
}
#Create the XAML reader using a new XML node reader
$Global:xamGUI = [Windows.Markup.XamlReader]::Load((new-object System.Xml.XmlNodeReader $xmlWPF))
#Create hooks to each named object in the XAML
$xmlWPF.SelectNodes("//*[@Name]") | %{
Set-Variable -Name ($_.Name) -Value $xamGUI.FindName($_.Name) -Scope Global
}
LoadDialog walkthrough
This loadDialog helper cmdlet is extremely important, so let's break it down. This cmdlet accepts one parameter: the FileName and Path of the XAML file. The contents are loaded into a global variable (typed as XML) by using the following line:
[xml]$Global:xmlWPF = Get-Content -Path $XamlPath
Next, WPF and Windows Forms assemblies are loaded from the .NET Framework. These allow us to make sense of the XAML to display the dialog.
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,system.windows.forms
With the power of our loaded assemblies, we load the XML variable, xmlWPF, as a parameter into a XmlNodeReader method, and subsequently, XamlReader. The result is loaded into a global XAML-typed variable. Thanks to our loaded assemblies, our code now "understands" how to interpret this XAML (more about this later).
Last, we wire the XAML notes in the tree to Windows PowerShell variables. By using the power of XPath, we scan through all the XAML tree nodes to extract the Name attribute. This name is passed down the pipe and turned into a global variable.
$xmlWPF.SelectNodes("//*[@Name]") | %{
Set-Variable -Name ($_.Name) -Value $xamGUI.FindName($_.Name) -Scope Global
}
Those of you who are following intently have noticed that there isn't a Name attribute by default using Visual Studio. You'll need to add this manually. Simple enough. Simply edit the XAML file and add a Name attribute in the elements that you want to control via Windows PowerShell. Continuing with our example, I have added name "Label1" to our Label element and "Button1" to the Button element in the XAML.
<Label Name="Label1" Content="Label" HorizontalAlignment="Left" Margin="68,38,0,0" VerticalAlignment="Top" Width="197"/>
<Button Name="Button1" Content="Button" HorizontalAlignment="Left" Margin="307,41,0,0" VerticalAlignment="Top" Width="75"/>
Finally, notice that nothing is actually displaying our dialog box...yet! We're getting there.
Putting it all together
So, we've finally arrived. You're ready to build your HelloWorld.ps1. Of course, by now you've realized that to make this work, there are actually three files involved. This may seem like a lot, but as you build multiple scripts, this will make sense. You have your primary script named "HelloWorld.ps1", the loadDialog.ps1 helper cmdlet, and your XAML form (MyForm.xaml).
For clean separation, I recommend that you store your .ps1 files in a Scripts folder and the XAML forms in a Forms folder.
To review what's going on, your original Windows PowerShell script calls the loadDialog.ps1 helper cmdlet, which in turn, loads the XAML form into a variable and creates matching Windows PowerShell variables for all the elements in the tree, based on the Name attribute. Near the top of your HelloWorld.ps1 script, you should see a line like this:
#Required to load the XAML form and create the PowerShell Variables
.\loadDialog.ps1 -XamlPath '..\Forms\MyForm.xaml'
After this line successfully executes, we have access to the form via Windows PowerShell variables.
$Label1
$Button1
The properties to set and get are a matter of research on websites like MSDN or exploring the properties window inside of Visual Studio. For example, here is an example of the Button class.
Adding events
Remember when I mentioned event-driven programming? At this point, you'll want to wire-up an event to the button. Here's how:
#EVENT Handler
$button1.add_Click({
$Label1.Content = "Hello World"
})
When the button is clicked, the code is setting the Content property of our Label1 Windows PowerShell variable with "Hello World." Because this is wired directly to the WPF form, we actually get to see the results on our screen.
Launching the window
With everything set, we are finally able to load the window by using the ShowDialog() method of our XAML object:
#Launch the window
$xamGUI.ShowDialog() | out-null
This must be the last statement in your script. From that point forward, everything is controlled via the events you have configured.
Execution policy
We have it all put together and we're ready to launch. If you haven't already, remember to set the execution policy so that you can run the script:
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
Also, remember to run Windows PowerShell as an administrator. Otherwise you'll hit this error message:
Set-ExecutionPolicy : Access to the registry key
'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell' is denied. To change the execution policy for the default (LocalMachine) scope, start Windows PowerShell with the "Run as administrator" option. To change the execution policy for the current user, run "Set-ExecutionPolicy -Scope CurrentUser".
Kick off your HelloWorld script: .\HelloWorld.ps1
When the window loads, simply click the button to see the results. They should look like this:
Methods like this don’t come along by chance. This mountain top of Windows PowerShell GUI is viewed on the shoulders of giant minds. My heartfelt thanks go to the engineering rock stars who made this possible: Microsoft senior premier field engineers, Mike Melone and Don Scott.
The methodology to create a Windows PowerShell GUI is somewhat advanced, but not beyond the reach of those who spend a great deal of time scripting Windows PowerShell solutions for others to use. I have personally used this method to create input validation and to control the choices of input from my users.
This is a very basic example, but WPF forms can be very sophisticated. The fun part is the creativity you bring that will help empower and guide others to be successful without getting stuck at the command line.
The files demonstrated in this blog post are available for download from the Scripting Guys Script Repository: PowerShell GUI.
~Chris
Thanks for this innovative post, Chris.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy