[Tutorial] Creating Extensive PowerShell GUI Applications – PART 1

Applies to Visual Studio, Tool-Making
Requirements: Visual Studio, ConvertForm (Module)

Introduction

Scripting, module creation, and tool-making are invaluable skills for any IT administrator- in IT Operations or elsewhere. The inherent problem with complex scripts and tools is the skills required to understand and use them. Scripting and automation has a primary goal of making processes easier and faster to complete, with minimal variance. But for lower-level IT technicians, or even those more advanced technicians who are new to a certain technology- the scripts and tools created may not have the ease-of-use that it really should have.

What better way to make your PowerShell scripts reach the masses -and its full potential- than to wrap it into an easy to use GUI Application? Of course if you’re deep into PowerShell scripts and tools, you’re likely one to shy away from user interfaces and live in the terminal, as I am. But we don’t always write scripts for ourselves, and those we share them with don’t always have the same skills as the author of the script. Today I’d like to help you bridge that gap in your team, and allow everyone to use scripts as simply as they should be to.

Requirements

To make GUI creation as simple as possible, I’ve adapted to using Visual Studio to design my forms, and convert them to PowerShell .ps1 script files. Visual Studio Community is free, and is available for download here.

Once we’ve designed our interface, we’re going to convert it to a .ps1 file, and begin creating the logic and PowerShell functionality. I came across a simple module, ConvertForm, that allows you to easily convert a Visual Studio designer file (x.Designer.cs) into a PowerShell .ps1 file.

ConvertForm is available for download at the link above, or to install directly from PowerShell, you can use the following commands:

$PSGalleryPublishUri = 'https://www.myget.org/F/ottomatt/api/v2/package'
$PSGallerySourceUri = 'https://www.myget.org/F/ottomatt/api/v2'

Register-PSRepository -Name OttoMatt -SourceLocation $PSGallerySourceUri -PublishLocation $PSGalleryPublishUri #-InstallationPolicy Trusted
Install-Module ConvertForm -Repository OttoMatt

These are the only requirements needed to create a running PowerShell GUI Application. Optionally, to take it a step further, when we’re completed we can compile our .ps1 into an executable .exe file using PS2EXE, or PS2EXE-GUI. This will really help finalize the application, and give it some versatility when our technicians begin to use it.

Getting Started

To jump right into it, we’re going to start with designing our User Interface. You should give this some thought before diving in, since the layout is important. Think about which inputs of your script will vary, which are static, what data should be displayed, etc. It’s easier to design the form right the first time, than to go back and move controls around to fit in neglected features.

Open Visual Studio, and Create a New Project
Create a Windows Forms App (.NET Framework)

I’m not going to go too in-depth about how to design forms in Visual Studio, there are plenty of tutorials and videos that you can refer to if you’re just getting started. What I do want to do is outline my process for making the design as easy to interact with as possible once we get into scripting it.

Naming Convention

I’ve found it’s much easier to work with these forms in PowerShell if everything is named with a naming convention that makes sense. Personally, I make all control names begin with what kind of control they are, and what the data will be. For example, a textbox for a name will begin with “TB” for textbox, and “name” to represent it’s data.

I’m going to create a winform with as many controls as I’ve learned to use within it. This way, we can go through each control, and learn how to interface with them in PowerShell.

Winform Design

When we get started in Visual Studio, we should have a window similar to the one above.

Don’t forget to give your main form a name, and a title. I’ve used FM to represent the form, and Main since this will be the main form.

To begin, I’m going to create a menu strip. We’ll have one item on the menu strip, and in that item will be two options.

In this case, I’ve named the menu strip MSmain. The “connection” item has been named MSIconnection, to represent the menu strip item. Then the sub-items are MSICo365, and MSICvmware. This represents Menu Strip->Item->Connection->o365/vmware.

To save real estate and demonstrate the Tab Control, we’re going to set up tabs for each group of controls we’ll be working with.

TCmain (Tab Control Main), TPboxesbuttons (Tab Page > buttons and boxes), etc.

We’re going to fast forward and add a few controls to this “Boxes and Buttons” tab. When we’ve got some controls to play with, I’ll go through the conversion process, and we’ll get the form working in Powershell with the Menu Strip, Tabs, and Boxes/Buttons we’ve added.

Here’s what I’ve added:

  • 2 Group Boxes
    • GBcheckboxes, GBradio
  • 3 Check Boxes
    • CBoption1, CBoption2, CBselectall
  • 2 Radio buttons
    • RBoption1, RBoption2
  • 2 Labels
    • LBcheckboxselected, LBradioselected
  • 2 Read-Only Text Boxes
    • TBcheckboxselected, TBradioselected
  • 2 Buttons
    • BTcheckboxsubmit, BTradiosubmit

I’ve included the names so you can see the naming convention in action.

Tip: You can set the tab index for each control in Visual Studio before converting it to a .ps1. I’ve found this the easiest place to do it, as opposed to doing it in PowerShell. Just click on each control down the form, and increment the tab index. Using the Tab Index properly helps your application flow, and gives it a much more polished, professional feel.

This should be enough of a design to get started with. We’ll revisit the other tabs when we get the form functioning.

Converting design.cs to .ps1

In Visual Studio, your Project should be located in the following folder by default: c:\users\username\source\repos\projectname\projectname

In my example, the project is simply called “Example”. The Design file we need to locate will be found at: c:\users\dom\source\repos\example\example\Form1.Designer.cs

Tip: If you have trouble finding the design file, you can right-click the Form1.cs [Design] tab in Visual Studio, and select “Copy Full Path”, and it will copy to your clipboard.

At this point, we should have the ConvertForm module installed, and we can open up PowerShell. We need the path to the designer file, and the folder path to our destination .ps1 file. My command for that looks like this:

$Source = "C:\Users\Dom\source\repos\Example\Example\Form1.Designer.cs"
$Destination = "C:\Users\Dom\Scripts\"
Convert-Form -Path $Source -Destination $Destination -Encoding ascii -force

We should see a progress bar while this gets converted, and once completed, we should have a file called Form1.ps1 in our destination folder. Let’s open this up and start scripting!

Working with Form Logic

You should now be able to open your form1.ps1 file in your PowerShell script editor. To cut down on unneeded code, there are a few lines I typically remove from this generated .ps1 file. The first is right at the beginning. I usually remove these lines from the top:

function Get-ScriptDirectory
{ #Return the directory name of this script
  $Invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $Invocation.MyCommand.Path
}

$ScriptPath = Get-ScriptDirectory

I also remove these lines from the bottom:

function OnFormClosing_FMmain{ 
	# $this parameter is equal to the sender (object)
	# $_ is equal to the parameter e (eventarg)

	# The CloseReason property indicates a reason for the closure :
	#   if (($_).CloseReason -eq [System.Windows.Forms.CloseReason]::UserClosing)

	#Sets the value indicating that the event should be canceled.
	($_).Cancel= $False
}

$FMmain.Add_FormClosing( { OnFormClosing_FMmain} )

$FMmain.Add_Shown({$FMmain.Activate()})
$ModalResult=$FMmain.ShowDialog()
# Release the Form
$FMmain.Dispose()

I replace that block at the end with a simple:

$FMmain.ShowDialog()

The removed code is just a fancy way to close and dispose of the forms. Feel free to leave it in if you’d like, but for simplicity’s sake, I take it out.

There may also be a couple functions meant to work with unhandled exceptions. I remove these as well. When I converted this example, I saw this function had been added, so I removed it:

function OnCheckedChanged_CBoption2 {
	[void][System.Windows.Forms.MessageBox]::Show("The event handler CBoption2.Add_CheckedChanged is not implemented.")
}

$CBoption2.Add_CheckedChanged( { OnCheckedChanged_CBoption2 } )

Now we’re ready to work with the actual form. If you run the script now, you’ll see you can already click through the tabs, click the menu strip and checkboxes. But none if it does anything just yet.

Checkboxes, and Textboxes

I’ll start with the “Select All” checkbox. I’ll find the section of code for the $CBselectall variable, and begin coding beneath that. I do this to keep the buttons functionality close the the rest of its defining properties. Here’s the $CBselectall code, and the code I added below it.

#
# CBselectall
#
$CBselectall.AutoSize = $true
$CBselectall.Location = New-Object System.Drawing.Point(7, 66)
$CBselectall.Name = "CBselectall"
$CBselectall.Size = New-Object System.Drawing.Size(70, 17)
$CBselectall.TabIndex = 2
$CBselectall.Text = "Select All"
$CBselectall.UseVisualStyleBackColor = $true

# I added this:
$CBselectall.add_checkstatechanged({
	if ($CBselectall.Checked -eq $true){
		$CBoption1.Checked = $true
		$CBoption2.Checked = $true
	}
	else{
		$CBoption1.Checked = $false
		$CBoption2.Checked = $false
	}
})

So what this does is add an action when the state of the check box is changed. It says, “When the Select All box changes, see if it is checked or not. If it is checked, check Option 1 and 2. If it has been unchecked, remove checks from Option 1 and 2.”

Next, let’s make the text-box display our selection when we click submit. This is a nice example of how to make various controls interact with each other.

Since the action is going to happen when we click “Submit”, I’m going to add this code beneath the $BTcheckboxsubmit code.

#
# BTcheckboxsubmit
#
$BTcheckboxsubmit.Location = New-Object System.Drawing.Point(259, 70)
$BTcheckboxsubmit.Name = "BTcheckboxsubmit"
$BTcheckboxsubmit.Size = New-Object System.Drawing.Size(75, 23)
$BTcheckboxsubmit.TabIndex = 5
$BTcheckboxsubmit.Text = "Submit"
$BTcheckboxsubmit.UseVisualStyleBackColor = $true

# I added this:
$BTcheckboxsubmit.add_click({
	$SelectedBoxes = @()
	if ($CBoption1.Checked -eq $true){
		$SelectedBoxes += "Option 1"
	}
	if ($CBoption2.Checked -eq $true){
		$SelectedBoxes += "Option 2"
	}
	$TBcheckboxselected.Text = ($SelectedBoxes -join (", "))
})

The code above adds an action whenever this “Submit” button is clicked. When it’s clicked, it creates an array called $SelectedBoxes. If Option 1 is checked, it adds “Option 1” to the $SelectedBoxes array. If Option 2 is selected, its adds “Option 2” to the array. After those checks are completed, the $SelectedBoxes array is joined with a comma, and added to the Text Box ($TBcheckboxselected) as text. This results in “Option 1, Option 2” if both are selected.

Radio Buttons

If you were able to get the check boxes working, the radio buttons should be no problem. One thing to know about radio buttons, is they must be in the same group box to function as intended. If they are, no logic is needed to de-select one when the other is checked. Because only one can be selected at a time, our code is a little more simple. We don’t need to create an array for this, and can simply say the text for $TBradioSelected is whichever button is selected.

#
# BTradiosubmit
#
$BTradiosubmit.Location = New-Object System.Drawing.Point(259, 76)
$BTradiosubmit.Name = "BTradiosubmit"
$BTradiosubmit.Size = New-Object System.Drawing.Size(75, 23)
$BTradiosubmit.TabIndex = 8
$BTradiosubmit.Text = "Submit"
$BTradiosubmit.UseVisualStyleBackColor = $true

# I added this:
$BTradiosubmit.add_click({
	if ($RBoption1.Checked -eq $true){
		$TBradioselected.Text = "Option 1"
	}
	if ($RBoption2.Checked -eq $true){
		$TBradioselected.Text = "Option 2"
	}
})

Menu Strip Items

The last thing I want to tackle in Part 1 is using menu strip items. I’ve found these most useful for connections or imports- things that need to be done, but don’t have a place in the real estate of the form itself. This is also where we’re going to dive into more PowerShell-related functions.

Menu strip items are simple, in that we really only need to specify an action if it is clicked. I have two options under the “Connection” item: Office365 and VMware.

For Office365, we’re going to pull up the O365 login prompt. There are a number of different ways to connect to Office365 via PowerShell, but I’m just going to use the Connect-MSOLService command from the MSOnline module.

We’ll begin by adding the following action when the MSICo365 button is clicked:

#
# MSICo365
#
$MSICo365.Name = "MSICo365"
$MSICo365.Size = New-Object System.Drawing.Size(124, 22)
$MSICo365.Text = "Office365"

# I added this:
$MSICo365.add_click({
	Connect-MsolService
	If ((Get-MsolDomain) -ne $null){
		$MSICo365.Checked = $true
	}
})

We could simply have “Connect-MsolService” as the only action for this button, but I also wanted to demonstrate the check box on these items. The code above will open the O365 login window, and once logged in, it will run a simple command to verify connectivity. If it succeeds, it will add a check next to the item. I use this to verify if you are connected.

Click on Office365 to connect
You’ll be prompted to sign in.
Once connected, you can click “Connection” to verify the sign in was successful.

Another simple use for these options is to gather general logon credentials using PowerShells Get-Credential command. I’ll assume VMware uses your normal user account to get connected, so all we need to do is get those credentials and store them in a variable. Our code for that will look like this:

#
# MSICvmware
#
$MSICvmware.Name = "MSICvmware"
$MSICvmware.Size = New-Object System.Drawing.Size(124, 22)
$MSICvmware.Text = "VMware"

# I added this:
$MSICvmware.add_click({
	$Credentials = Get-Credential
})
Clicking the VMware button will now open a credential dialog, and save it to the $credential variable to be used later in the script.

Conclusion

That’s all it takes to get started writing PowerShell Applications with winforms. The examples in this post can be greatly expanded to facilitate larger scripts and complex functions.

In the next post, I’m going to cover how to use fields, and populate data in DataGridViews. Thanks for reading, and let me know if you have any questions- I’d be happy to help. You can send an email to dom@domruggeri.com, or post a comment below. Thanks again!

Advertisement

16 thoughts on “[Tutorial] Creating Extensive PowerShell GUI Applications – PART 1

  1. Kevin Rehnberg July 8, 2019 / 8:35 am

    Very nice, I have built a couple of GUI´s with Powrshell but never done it with Visual Studio. This seeems a lot easier and I will give it a try.
    Positioning everything with x and y pixel positions are very time consuming and takes a lot ot trial and error to get things where you want them doing things manually.

    Looking forward to the coming parts.

    Like

  2. Blanke July 12, 2019 / 3:23 pm

    Very nice! I don’t try to nihilate your work, but if you want to spent some money, you can use Sapien Powershellstudio too. Same features, less “program hopping”. But nevertheless, nice to see how it’s done handmade.

    Like

    • Dom Ruggeri July 12, 2019 / 7:58 pm

      Hey Blanke, thanks for reading-
      Yes, I’ve worked with PowerShell Studio before, and as cool and simple as it was, the code that it created afterward was too hard to follow for troubleshooting. (In my opinion). There were many different files created with proprietary functions for carrying out tasks, and I didn’t like the notion of creating scripts that I couldn’t read and understand. I’m not completely writing it off, I’m sure I’ll revisit it again some day. For me, it just abstracted all the PowerShell I know. And there is a lot of program hopping in this tutorial, but in a more practical sense, you would design the form once, convert it, then do the rest in your editor, which is a lot less hops.

      Like

  3. Fred Sahonic September 22, 2020 / 2:24 pm

    Your title is misleading. I’m looking to do this in powershell not something else I don’t know and then converting to powershell.

    Like

    • Dom Ruggeri September 22, 2020 / 2:33 pm

      We only make the form in Visual Studio, no coding is done there. I also tried to cover some of the basics of creating a form in Visual Studio, so you don’t have to know how to use it going in. But, if you try it out you may find it pretty simple, it’s all drag-drop, and resizing.

      Coding this directly in PowerShell wouldn’t be practical, as we’re working on making a visual app. There are a number of functions, assembly types, and declarations that are converted to PowerShell for us when we start in Visual Studio- which overall makes the learning curve much lower.

      Feel free to let me know if you get stuck somewhere, I’d be happy to help out. It really is a simple process, and if you know a bit of PowerShell, anything else needed should be covered in the guide.

      Like

      • Steve October 27, 2021 / 8:57 am

        Please could you cover how to multi-thread the Windows Forms app so that the form does not become unresponsive when a long task is happening as typically PowerShell runs in STA.

        Like

    • Joa July 16, 2021 / 10:36 am

      haters will always cry

      Like

  4. MilenKo June 15, 2021 / 6:05 pm

    Hello. I’ve followed your example to try for fun and convert a Windows Form to PowerShell script. I know for sure and I am comfortable working with WPF, however for the testing sake I’ve followed your example to the bone and ended up with quite a few errors during conversion and later on I am not able to fire up/execute the Windows Form I’ve built for the test.

    Here is the error I am getting during conversion:

    New-ResourcesFile : Error (-1) during the generation of the resource file. Check the log file C:\Test\WindowsFormsTestApp\CONVERTED\Form1.resources.log At C:\Program Files\WindowsPowerShell\Modules\ConvertForm\2.0.0\ConvertForm.psm1:1033 char:5 + New-ResourcesFile -Source $rsxSource -Destination $rsxDestination … + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,New-ResourcesFile

    Any ideas or suggestions what could have gone wrong?

    Like

  5. Gregor Fireside November 2, 2021 / 1:31 am

    Great work here! Thanks for taking the time to go through this all for us who are just starting to learn the art of the GUI in PowerShell. Looking forward to the next section!

    Like

  6. MilenKo December 1, 2021 / 2:52 pm

    Hello. I’ve done a few WinForm conversions previously, and it worked like a charm (thanks, btw, for the detailed info!), however this time I hit a rock when I used some images in my app. The conversion went OK or at least I am able to see my form code correctly, however the error is not very descriptive at all and much of help… It will be greatly appreciated if you can share how can I recover this… I also thought of one thing, probably it will be best if I embed the image code in the PS Script as otherwise I will have to download it or keep together with the PS script?

    Like

Leave a Reply to MilenKo Cancel 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 )

Facebook photo

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

Connecting to %s