Christof VG

You don't need to come out of your comfort zone, if automation is in it!

PowerShell GUI with externally managed data

Read time: 15 minutes

Introduction

A while ago, I got a question from a customer to create a PowerShell GUI script to be used by their end-users. The customer is a global company, with multiple offices in various countries. This script had following requirements:

  • data, displayed in the application, should be filtered by the selected country/location
  • the data to display should be maintained by someone without programming skills

The suggestions I made were also accepted:

  • Source control should be used for the code
  • Automated build and release (CI/CD) should be used

In this blog post I want to show you how I created an PowerShell script with a GUI with one release pipeline, and another pipeline with the data, used in the script.

Tools

I created the application using the following tools:

  • Visual Studio: I used this tool, only to create the XAML code for the GUI
  • Visual Studio Code: For all PowerShell development
  • Azure DevOps: As source control and for build and release pipelines

Supporting code

All code can be found on my github repository. Unfortunately, GitHub doesn’t support projects containing multiple repositories. Therefore, I created a folder structure to separate all code. But it will give you a good view about how I created this code and these pipelines.

Input data

Data layout

An important requirement was to filter the data, based on a selection of the country and the location of the end-user. To do so, I created a data array with an object per country/location. This object contains an array for the printers, shares and phone numbers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"version": "1.0.0",
"data": [
{
"Country": "Belgium",
"Location": "Antwerp",
"Printers": [
{
"DisplayName": "HP LaserJet M1522 series",
"Name": "HP LaserJet M1522 series",
"PrinterServer": "grooveprint.groovesoundz.be"
},
{
"DisplayName": "HP OfficeJet Pro X",
"Name": "HP OfficeJet Pro X",
"PrinterServer": "grooveprint.groovesoundz.be"
}
],
"Drives": [
{
"DisplayName": "Music",
"DriveLetter": "M",
"Path": "\\\\grooveshare.groovesoundz.be\\Music"
},
{
"DisplayName": "Video",
"DriveLetter": "V",
"Path": "\\\\grooveshare.groovesoundz.be\\Video"
}
],
"PhoneNumbers": [
{
"Name": "Service Desk",
"Number": "+32 123 45 67"
}
]
}
]
}

Versioning to the rescue!

On top of the JSON file, I added a version. This version will be showed in the application later on. When the end-user calls the Service Desk for an issue with the app, it can be easily determined if the end-user has the latest version of the data.

Important
The version is 1.0.0 in the file. No worries! This is updated in the automated build, changing the version to the correct build version.

Automated build

The build process has 2 tasks:

  • Updating the version of the JSON file using an inline PowerShell script
  • Publishing the JSON file as an artefact
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Convert the JSON file to a PS object

try {
$JsonData = Get-Content -Path "$(System.DefaultWorkingDirectory)\data.json" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
}
catch {
Write-Host "##VSO[task.logissue type=error]$($Error[0].Exception.Message)"
Write-Host "##vso[task.complete result=Failed;]Error"
}

# Update the version in the JSON file

[version]$Version = $JsonData.version

$JsonData.version = "$($Version.Major).$($Version.Minor).$(Build.BuildNumber)"

# Update the JSON file with the new version

Set-Content -Path "$(System.DefaultWorkingDirectory)\data.json" -Value ($JsonData | ConvertTo-Json -Depth 5) -Force

Automated release

The only step in the release process uploads the file to a blob storage in an Azure Storage Account.

PowerShell GUI

Separating PowerShell code and XAML

Creating the PowerShell GUI itself is out of scope of this blog post. If you like to know more about creating PowerShell GUI scripts, I recommend reading the “Learning GUI toolmaking series” by Stephen Owen.

Most articles describing PowerShell GUI scripts put the XAML code directly in the code of the script. Not only does it make your code longer, there is not a nice separation between the PowerShell code and the GUI code. When using Get-Content of the XAML file, code remains cleaner and the Visual Studio solution can exist in your repository as a separate directory.

1
$inputxml = Get-Content '..\Endpoint Manager\MainWindow.xaml' -Raw

Retrieve the data from the Azure Storage Account

Since we uploaded the JSON file to an Azure Storage Account, we need to download it to be used in the script. You can store it wherever you want. I used AppData, which avoids issues with rights for the end-user and it is a logical location for the data.

1
2
3
4
Invoke-WebRequest -Uri "<URL to the storage account - with key if needed>" `
-OutFile "$DataPath\data.json" `
-UseBasicParsing `
-ErrorAction SilentlyContinue

Important
I used ‘SilentlyContinue’ as error action. The reason for this is because this way, the application will use the latest available version of the data if no internet connection is available without crashing.

Displaying the support information

The support information for the end-user contains all information that the Service Desk might need to support the end-user. The most important information in this context is the version information of the data file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Get the version from the JSON file

$Version = (Get-Content -Path $JsonFilePath -Raw | ConvertFrom-Json).version

# Display the support information, containing the file version

$WPFtxtSysinfo.Text = @"
Support information:

Computer brand: $($ComputerInfo.CsManufacturer)
Computer model: $($ComputerInfo.CsModel)

Windows product: $($ComputerInfo.WindowsProductName)
Windows version: $($ComputerInfo.WindowsVersion)
Windows build version: $($ComputerInfo.OsVersion)
Windows architecture: $($ComputerInfo.OsArchitecture)

Workgroup: $($ComputerInfo.CsWorkGroup)

Physical Memory: $($ComputerInfo.CsTotalPhysicalMemory)

Using config file: $JsonFilePath
Config file version: $Version
"@

Displaying data, filtered by the country/location

When the country and location are selected, all filtered data can be displayed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$WPFcmbLocation.add_SelectionChanged({

# Fill printers, based on the location

$WPFlstPrinters.Items.Clear()
$WPFlstDrives.Items.Clear()

$Country = $WPFcmbCountry.SelectedItem
$Location = $WPFcmbLocation.SelectedItem
$Printers = ($Data | Where-Object {$_.Country -eq $Country -and $_.Location -eq $Location}).Printers

foreach ($Printer in $Printers) {
$WPFlstPrinters.Items.Add("$($Printer.DisplayName)")
}

# Fill network drives, based on the location

$Drives = ($Data | Where-Object {$_.Country -eq $Country -and $_.Location -eq $Location}).Drives

foreach ($Drive in $Drives) {
$WPFlstDrives.Items.Add("$($Drive.DisplayName)")
}

# Set support number variable

$Script:SupportNumber = (($Data | Where-Object {$_.Country -eq $Country -and $_.Location -eq $Location}).PhoneNumbers | Where-Object {$_.Name -eq "Service Desk"}).Number

$WPFtxtHostInfo.Text=@"
Computer name: $($ComputerInfo.CsName)
Support telephone: $($Script:SupportNumber)
"@

})

Result

This is the result I got…

System information:

Printers:

Shares:

Conclusion

It was actually not that hard to create the GUI application with a pipeline for the application itself and for the JSON data file. Making changes is very easy with the support of Azure DevOps. Data can be entered now by people who manage data without any programming skills.