### PowerShell Classes and Class Lists

I found that I could use classes in PowerShell similar to how I use them in C#. I instantly wanted to play with this and I thought I would share this as well.

To create a class in PowerShell, it’s as simple as:

#Person class
class PersonClass{
[String]$Name [Int]$Age
}

This allows a “Person” to be created that has the attributed of a name and an age. Simple stuff.

Say I wanted to have a bunch of these “Person”s in a list, a “People” list if you will. Then I could do something like this:

#Creating a list to hold the people using the PersonClass
$People = New-Object 'System.Collections.Generic.List[PSObject]' #Creating a new person$newPerson = [PersonClass]::new()
$newPerson.Name = "Roy Orbison"$newPerson.Age = "24"

#Adding the new person to the people list
$People.Add($newPerson)

What if I wanted to add something like a “Pets” attribute onto the person? Well, I could create a new class to hold a framework for each pet and create a new list attribute in the PersonClass. Here is my PetClass:

#Pet class
class PetClass{
[String]$Name [Int]$Age
[String]$Color } And here is how I add it to my PersonClass so that I can have a list of pets for each user: #Person class class PersonClass{<br> [String]$Name
[Int]$Age [PetClass[]]$Pets
}

Now its really simple to create a list of people with a list of any pets that they might have. Stitching this all together, it looks like this:

#Person class
class PersonClass{
[String]$Name [Int]$Age
[PetClass[]]$Pets } #Pet class class PetClass{ [String]$Name
[Int]$Age [String]$Color
}

#Creating a list to hold the people using the PersonClass
$People = New-Object 'System.Collections.Generic.List[PSObject]' #Creating a new person$newPerson = [PersonClass]::new()
$newPerson.Name = "Roy Orbison"$newPerson.Age = "24"

#Adding pets to the new person
for ($i = 0;$i -le 5; $i++){$newPet = [PetClass]::new()
$newPet.Name =$i
$newPet.Age =$i + 2
$newPet.Color = "Brown" #Adding the pet to the new person$newPerson.Pets += $newPet } #Adding the new person to the people list$People.Add($newPerson) Above you can see that I have created a new person called “Roy Orbison” with an age of “24” and I have added five pets. The pet names and age aren’t really accurate but it’s good enough for this demonstration. Continuing from this, I could add as many users as I want or even create new classes to add extra framework information for existing classes. Searching this information isn’t as straight forward in PowerShell as it is in C# but it’s still quite easy. You can see how I get a list of all the pets that Roy Orbison has below:$People | Where-Object {$_.Name -eq "Roy Orbison"} | Select-Object -ExpandProperty Pets Upon finishing this, I realised that it would have been much more appropriate to do the users and albums, instead of pets. But I’m far too lazy to change what I already have… Enjoy! ### Don’t use$input in PowerShell Functions

Just a short post and a gentle reminder to check conventions before pulling my hair out over a simple issue.

### In PowerShell, this doesn’t work…

function Test-Function([string]$input){ Write-Host$input
}

After messing for about thirty minutes, I finally found this knowledge out and renamed my parameter. I think you’ll like the name I continued to use… 😊

function Test-Function([string]$stupidFuckingInput){ Write-Host$stupidFuckingInput
}

Enjoy!

### Office Click-To-Run and XML Files

So, it used to be that we would install Office using a batch script that would invoke a setup.exe, assign a specific /configure flag and manually assign a specific XML file that contained the product that we wanted to install. This was bulky. It got too bulky when we needed to install 32-bit and 64-bit versions.

TIME FOR A CHANGE!

This is when I started thinking: “wow I really hate batch. I’m really glad I’m not the one that had to write this old script. Lets PowerShell this shit!”

First I needed a template XML file to modify, So this is what that looks like:

<Configuration>
<Language ID="MatchOS" />
</Product>
</Configuration>

This is the file that we will edit to say which product we want installing also if we want 64-bit or 32-bit.

Next, I needed to create a PowerShell script that would take a user’s input, edit the XML file accordingly and start the setup.exe with this flag. I also needed the bit-version that they wanted.

I started by defining the variables I would need for the script:

#Variables used for the installation
$bitVersion = ""$officeProduct = ""
$pathToOffice = "\\path\to\office\folder"$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path$pathToOffice -ChildPath $xmlFile Then I created a function I would use to update the XML file. I needed two parameters, the product that they wanted installing and the bit version they wanted: #Updates the XML file based on the input function Update-XMLFile([string]$product, [string]$bit){ try{ #Loading the XML document [xml]$xmlDoc = Get-Content -Path $pathToXMLFile #Edit the document$xmlDoc.Configuration.Add.OfficeClientEdition = $bit$xmlDoc.Configuration.Add.Product.ID = $product #Save the document$xmlDoc.Save($pathToXMLFile) }catch{$errorMessage = $_.Exception.Message Write-Host$errorMessage -ForegroundColor Red
Read-Host "The script encountered the above error - will now exit"
}
}

I then created another function to start the installation. This also required two parameters, the bit version and the XML file name

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
try{
.\setup.exe /configure $bit\$xmlName
}catch{
$errorMessage =$_.Exception.Message
Write-Host $errorMessage Read-Host "The script encountered the above error - will now exit" } } My final function was a verification test. Since we want to only use 64-bit for future installations, I had to make sure that whoever was using the script knew this and would be competent enough to do a little bit of math: #Function to check the user wants 32 bit function Get-Verification(){$output = $false Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red Write-Host "All new installs should use 64-bit instead" Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"$firstNumber = Get-Random -Minimum 1 -Maximum 11
$secondNumber = Get-Random -Minimum 1 -Maximum 11$sumToCheck = $firstNumber +$secondNumber

$verificationInput = Read-Host "$($firstNumber) +$($secondNumber) = ?" if ($verificationInput -eq $sumToCheck){ Write-Host "Fine! 32-bit will be installed..."$output = $true }else{ Write-Host "Finally! 64-bit will be installed"$output = $false } return$output
}

Now that all my functions were defined, I could start with the actual meat of the script. This included cleaning the screen, asking the user some questions, launching the 32-bit verification is needed, updating the XML file using a switch statement and finally kicking off the installation. Heres what that looked like:

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

Write-Host "Do you want" -NoNewline
Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
Write-Host "or" -NoNewline
Write-Host " 32-bit " -NoNewline -ForegroundColor Green
Write-Host "? (64 or 32): " -NoNewline
$bitVersionInput = (Read-Host).ToUpper() }while((64 ,32) -notcontains$bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){ if (Get-Verification){$bitVersion = $bitVersionInput }else{$bitVersionInput = "64"
}
}

#Update the bitVersion variable
$bitVersion =$bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
$officeProductInput = (Read-Host "Enter a number").ToUpper() }while((1,2,3,4,5,6,7,8, "C") -notcontains$officeProductInput)

#endregion

#Update the product variable
$officeProduct =$officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){ #Business Retail 1 { Update-XMLFile -product "O365BusinessRetail" -bit$bitVersion}
#ProPlus
2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion} #Visio Std Volume 3 { Update-XMLFile -product "VisioStd2019Volume" -bit$bitVersion}
#Visio Pro Volume
4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion} #Visio Pro Retail 5 { Update-XMLFile -product "VisioPro2019Retail" -bit$bitVersion}
#Project Std Volume
6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion} #Project Pro Volume 7 { Update-XMLFile -product "ProjectPro2019Volume" -bit$bitVersion}
#Project Pro Retail
8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion} #Cancel "C" {Exit} default {Exit} } #endregion #Start the installation Write-Host "Installing..." -ForegroundColor Green Start-Installation -bit$bitVersion -xmlName $xmlFile Write-Host "This window can be closed" Read-Host ## Done! If you’re wondering what the script looks like as a whole, wonder no longer: #Variables used for the installation$bitVersion = ""
$officeProduct = ""$pathToOffice = "\\sandpdc\software\Office"
$xmlFile = "OfficeXML.xml"$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath$xmlFile

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

try{
[xml]$xmlDoc = Get-Content -Path$pathToXMLFile

#Edit the document
$xmlDoc.Configuration.Add.OfficeClientEdition =$bit
$xmlDoc.Configuration.Add.Product.ID =$product

#Save the document
$xmlDoc.Save($pathToXMLFile)
}catch{
$errorMessage =$_.Exception.Message
Write-Host $errorMessage -ForegroundColor Red Read-Host "The script encountered the above error - will now exit" } } #Function to start the installation function Start-Installation([string]$bit, [string]$xmlName){ try{ .\setup.exe /configure$bit\$xmlName }catch{$errorMessage = $_.Exception.Message Write-Host$errorMessage
Read-Host "The script encountered the above error - will now exit"
}
}

#Function to check the user wants 32 bit
function Get-Verification(){
$output =$false

Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
Write-Host "All new installs should use 64-bit instead"
Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

$firstNumber = Get-Random -Minimum 1 -Maximum 11$secondNumber = Get-Random -Minimum 1 -Maximum 11

$sumToCheck =$firstNumber + $secondNumber$verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

if ($verificationInput -eq$sumToCheck){
Write-Host "Fine! 32-bit will be installed..."
$output =$true
}else{
Write-Host "Finally! 64-bit will be installed"
$output =$false
}
return $output } #Clear the screen Clear-Host #region Checking if the user wants 64 bit or 32 bit do{ Write-Host "Do you want" -NoNewline Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow Write-Host "or" -NoNewline Write-Host " 32-bit " -NoNewline -ForegroundColor Green Write-Host "? (64 or 32): " -NoNewline$bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput) #endregion #Check the user definitely wants 32 bit if ($bitVersionInput -eq "32"){
if (Get-Verification){
$bitVersion =$bitVersionInput
}else{
$bitVersionInput = "64" } } #Update the bitVersion variable$bitVersion = $bitVersionInput #region Asking what product to install #Ask the user what product they want to install Write-Host @" Please select one product from the below list "@ Write-Host @" 1) Business Retail 2) ProPlus Retail "@ -ForegroundColor Cyan Write-Host @" 3) Visio Std Volume 4) Visio Pro Volume 5) Visio Pro Retail "@ -ForegroundColor Green Write-Host @" 6) Project Std Volume 7) Project Pro Volume 8) Project Pro Retail "@ -ForegroundColor Gray Write-Host @" C) Cancel "@ -ForegroundColor Red do{$officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput) #endregion #Update the product variable$officeProduct = $officeProductInput #region Switch the input to see what it is and perform the required operation switch($officeProduct){

1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion} #ProPlus 2 { Update-XMLFile -product "O365ProPlusRetail" -bit$bitVersion}
#Visio Std Volume
3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion} #Visio Pro Volume 4 { Update-XMLFile -product "VisioPro2019Volume" -bit$bitVersion}
#Visio Pro Retail
5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion} #Project Std Volume 6 { Update-XMLFile -product "ProjectStd2019Volume" -bit$bitVersion}
#Project Pro Volume
7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion} #Project Pro Retail 8 { Update-XMLFile -product "ProjectPro2019Retail" -bit$bitVersion}
#Cancel
"C" {Exit}
default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName$xmlFile
Write-Host "This window can be closed"

### LAPS 3

Okay, this is probably my final update to the whole LAPS thing. I have created two iterations in the past but neither were really groundbreaking or my own design. Not that this update is groundbreaking either though. This is a further update to the below post:

LAPS WinForm 2

I wanted to completely redo my LAPS form (again) to make it my own design, responsive and ultimately better. This is what the final form looks like. It is completely responsive and resizeable:

I will include the source code here but the best place to download this would be from my TechNet gallery.

There are a couple of things you need to change in the form to make it work:

• Adding your domain controller and domain root to the variables at the top of the script

Heres the code:

#Enter your domain and domain controller below :)
$script:domainController = "DOMAIN CONTROLLER HERE" #E.G domaincontroller.domain.lan$script:domainRoot = "DOMAIN ROOT HERE" #E.G domain.lan

Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration

#ICON FOR FORM
[string]$base64=@' BASE64 DATA HERE '@ #CREATING THE IMAGE FROM BASE64 DATA$bitmap = New-Object System.Windows.Media.Imaging.BitMapImage
$bitmap.BeginInit()$bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64)$bitmap.EndInit()
$bitmap.Freeze() #LAPS WINDOW XML [xml]$LAPSXaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="LAPS UI" Height="400" Width="400" MinHeight="400" MinWidth="400" WindowStartupLocation="CenterScreen">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto" MinWidth="75"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" MinHeight="7"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="ComputerName:" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Column="1" FontSize="14"/>
<TextBox Name="Computer_Textbox" VerticalContentAlignment="Center" HorizontalAlignment="Stretch" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Stretch" Margin="3" Grid.Column="1" FontSize="14"/>
<Button Name="Search_Button" Content="Search" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="1" VerticalAlignment="Stretch" Margin="0,3,5,3"/>
<Label Content="Password" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="2" VerticalAlignment="Stretch" FontSize="14"/>
<Button Name="Copy_Button" Content="Copy" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="3" Margin="0,3,5,3" VerticalAlignment="Stretch"/>
<Label Content="Password Expires" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="4" VerticalAlignment="Stretch" FontSize="14"/>
<Label Content="New Expiration" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="6" VerticalAlignment="Stretch" FontSize="14"/>
<DatePicker Name="Date_Picker" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="3" FontSize="14"/>
<Button Name="Set_Button" Content="Set" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="0,5,5,5"/>
<GridSplitter IsEnabled="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="8" Grid.Column="1" Margin="5,2,5,2" Grid.ColumnSpan="2"/>
<TextBox Name="Output_Textbox" VerticalScrollBarVisibility="Auto" IsReadOnly="True" HorizontalAlignment="Stretch" Grid.Row="9" TextWrapping="Wrap" Margin="1,5,1,1" VerticalAlignment="Stretch" Grid.ColumnSpan="3" FontSize="12"/>
</Grid>
</Window>
"@

$LAPSReader=(New-Object System.Xml.XmlNodeReader$LAPSXaml)
$LAPSWindow=[Windows.Markup.XamlReader]::Load($LAPSReader)
$LAPSWindow.Icon =$bitmap

#ASSIGNING CONTROLS
$Computer_Textbox =$LAPSWindow.FindName("Computer_Textbox")
$Search_Button =$LAPSWindow.FindName("Search_Button")
$Password_Textbox =$LAPSWindow.FindName("Password_Textbox")
$Copy_Button =$LAPSWindow.FindName("Copy_Button")
$Password_Ex_Textbox =$LAPSWindow.FindName("Password_Ex_Textbox")
$Date_Picker =$LAPSWindow.FindName("Date_Picker")
$Set_Button =$LAPSWindow.FindName("Set_Button")
$Output_Textbox =$LAPSWindow.FindName("Output_Textbox")

#FUNCTION TO SET OUTPUT TEXTBOX
function set-output-textbox{
param(
[string]$value, [bool]$date
)
if ($date){$Output_Textbox.Text = ("[$(Get-Date)] -$value rn")
}else{
$Output_Textbox.Text =$value
}
}

#FUNCTION TO UPDATE OUTPUT TEXTBOX
function update-output-textbox{
param(
[string]$value, [bool]$date
)
if ($date){$Output_Textbox.AppendText("[$(Get-Date)] -$value rn")
}else{
$Output_Textbox.AppendText("$value rn")
}
$Output_Textbox.ScrollToEnd() } #FUNCTION TO UPDATE FORM function update-form{ [System.Windows.Forms.Application]::DoEvents() } #FUNCTION TO UPDATE PASSWORD TEXTBOX function update-password-textbox($value){
$Password_Textbox.Text =$value
}

#FUNCTION TO UPDATE PASSWORD EX TEXTBOX
function update-passwordex-texbox($value){$Password_Ex_Textbox.Text = $value } #FUNCTION TO SET CONTROLS function set-controls{ param( [bool]$switcher,
[bool]$setswitcher )$Search_Button.IsEnabled = $switcher$Set_Button.IsEnabled = $setswitcher$Date_Picker.IsEnabled = $setswitcher } #DECIDE IF COPY BUTTON SHOULD BE ENABLED$Copy_Button.IsEnabled = $false$Password_Textbox.Add_TextChanged({
if ($Password_Textbox.Text.Length -gt 0){$Copy_Button.IsEnabled = $true }else{$Copy_Button.IsEnabled = $false } }) #MAKING COMPUTER NAME UPPERCASE ON FOCUS LOST$Computer_Textbox.Add_LostFocus({
$Computer_Textbox.Text =$Computer_Textbox.Text.ToUpper()
})

#COPY BUTTON LOGIC
$Copy_Button.Add_Click({ Set-Clipboard -Value$Password_Textbox.Text
})

#COMPUTER TEXTBOX KEYDOWN LOGIC
$Computer_Textbox.Add_KeyDown({ if ($args.Key -eq 'Enter'){
$Search_Button.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs$([System.Windows.Controls.Button]::ClickEvent)))
}
})

set-controls -switcher $true -setswitcher$false

$Output_Textbox.HorizontalContentAlignment="Center"$Output_Textbox.VerticalContentAlignment="Center"
set-output-textbox -date $false -value "Welcome to version 3 of this form! It is now responsive and a lot cleaner in the background. Nothing you ever had to worry about though :)" #SEARCH BUTTON LOGIC$Search_Button.Add_Click({

#DISABLING CONTROLS ON BUTTON PRESS
$Output_Textbox.HorizontalContentAlignment="Left"$Output_Textbox.VerticalContentAlignment="Top"
set-controls -switcher $false -setswitcher$false
update-password-textbox -value $null update-passwordex-texbox -value$null
$Date_Picker.Text =$null

if ($Computer_Textbox.Text.Length -le 0){ #OUTPUT IF EMPTY SEARCH AND ENABLING CONTROLS set-output-textbox -date$true -value "Input cannot be empty"
set-controls -switcher $true -setswitcher$false
}else{
set-output-textbox -date $true -value "Please Wait" #PUTTING INPUT INTO VARIABLE$script:computerName = $Computer_Textbox.Text #CREATING A SYNCHRONISED HASHTABLE$script:syncHash = [hashtable]::Synchronized(@{})

#CREATING SEARCH RUNSPACE
$searchRunspace = [runspacefactory]::CreateRunspace()$searchRunspace.ApartmentState = "STA"
$searchRunspace.ThreadOptions = "ReuseThread"$searchRunspace.Open()
$searchRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$searchRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
$searchRunspace.SessionStateProxy.SetVariable("domainController",$domainController)

#POWERSHELL TO BE RAN IN RUNSPACE
$searchPowerShell = [powershell]::Create().AddScript({$syncHash.searchADComputer = Get-ADComputer -Identity $computerName$syncHash.searchInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock { Get-AdmPwdPassword -ComputerName$args[0] } -ArgumentList $computerName | Select-Object Password, ExpirationTimeStamp }) #ASSIGNING RUNSPACE TO POWERSHELL$searchPowerShell.Runspace = $searchRunspace #STARTING THE RUNSPACE AND POWERSHELL$searchObject = $searchPowerShell.BeginInvoke() #REFRESHING UNTIL POWERSHELL IS COMPLETE do{ Start-Sleep -Milliseconds 100 update-form }while (!$searchObject.IsCompleted)

#ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE
$searchPowerShell.EndInvoke($searchObject)
$searchPowerShell.Dispose() if ($syncHash.searchADComputer){
#COMPUTER IS FOUND ON DOMAIN
if ($syncHash.searchInvoke){ #INVOKE SUCCESSFUL$admpwdPassword = $syncHash.searchInvoke.password$admpwdPasswordExpiration = $syncHash.searchInvoke.ExpirationTimeStamp$admpwdPasswordExpirationFormatted = $admpwdPasswordExpiration.ToString("dd/MM/yyyy hh:mm:ss") #UPDATING FIELDS update-output-textbox -date$true -value "Information retrieved"
update-password-textbox -value $admpwdPassword update-passwordex-texbox -value$admpwdPasswordExpirationFormatted
set-controls -switcher $true -setswitcher$true
}else{
#INVOKE FAILED
update-output-textbox -date $true -value "Failded to retrieve password information" update-password-textbox -value$null
update-passwordex-texbox -value $null set-controls -switcher$true -setswitcher $false } }else{ #COMPUTER NOT FOUND ON DOMAIN update-output-textbox -date$true -value "Host not found on domain"
update-password-textbox -value $null update-passwordex-texbox -value$null
set-controls -switcher $true -setswitcher$false
}
}
})

#SET EXPIRATION BUTTON LOGIC
$Set_Button.Add_Click({ #DISABLING CONTROLS ON BUTTON PRESS set-controls -switcher$false -setswitcher $false if ($Date_Picker.Text.Length -le 0){
#OUTPUT IF EMPTY DATE AND ENABLING CONTROLS
update-output-textbox -date $true -value "No date selected" set-controls -switcher$true -setswitcher $true }else{ #GETTING NEW DATES FOR EXPIRATION$newExpirationString = $Date_Picker.SelectedDate.ToString("MM/dd/yyyy")$script:newExpirationDate = [datetime]::ParseExact($newExpirationString, 'MM/dd/yyyy',$null)

#OUTPUTTING FRIENDLY EXPIRATION TO OUTPUT TEXTBOX
update-output-textbox -date $true -value "Setting expiration to$newExpirationString..."

#CREATING SEARCH RUNSPACE
$setRunspace = [runspacefactory]::CreateRunspace()$setRunspace.ApartmentState = "STA"
$setRunspace.ThreadOptions = "ReuseThread"$setRunspace.Open()
$setRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$setRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
$setRunspace.SessionStateProxy.SetVariable("domainController",$domainController)
$setRunspace.SessionStateProxy.SetVariable("newExpirationDate",$newExpirationDate)

#POWERSHELL TO BE RAN IN RUNSPACE
$setPowerShell = [powershell]::Create().AddScript({ try{$syncHash.setInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock {Reset-AdmPwdPassword -ComputerName$args[0] -WhenEffective $args[1] } -ArgumentList$computerName, $newExpirationDate -ErrorAction Stop try{ Invoke-GPUpdate -Computer$computerName -ErrorAction Stop
$syncHash.setGPUpdate =$true
}catch{
#GP UPDATE FAILED
$syncHash.setGPUpdate =$null
}
}catch{
#CHANGING EXPIRATION FAILED
$syncHash.setInvoke =$null
}
})

#ASSIGNING RUNSPACE TO POWERSHELL
$setPowerShell.Runspace =$setRunspace
#STARTING THE RUNSPACE AND POWERSHELL
$setObject =$setPowerShell.BeginInvoke()

#REFRESHING UNTIL POWERSHELL IS COMPLETE
do{
Start-Sleep -Milliseconds 100
update-form
}while (!$setObject.IsCompleted) #ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE$setPowerShell.EndInvoke($setObject)$setPowerShell.Dispose()

if ($syncHash.setInvoke){ update-output-textbox -date$true -value "Successfully reset password expiration date"
#CHECKING GP UPDATE SUCCESS
if ($syncHash.setGPUpdate){ update-output-textbox -date$true -value "Succesfully ran GP update"
}else{
update-output-textbox -date $true -value "Failed to run GP update, this is probably due to permissions" } }else{ update-output-textbox -date$true -value "Failed to reset password expiration date"
}

#RESETTING CONTROLS
set-controls -switcher $true -setswitcher$true
}
})

#CHECK FOR AD MODULE AND TEST IF ON LOCAL DOMAIN/NETWORK
if ( Test-Connection $domainRoot -Count 1 -Quiet){ #DOMAIN IS ACCESSIBLE if (Get-Module -List ActiveDirectory ){ #AD MODULE INSTALLED #FORM WILL BE DISPLAYED WITHOUT ANY MODIFICATIONS }else{ #AD MODULE NOT INSTALLED set-output-textbox -date$false -value "Install the AD module and restart"
set-controls -switcher $false -setswitcher$false
$Computer_Textbox.IsEnabled =$false
}
}else{
#DOMAIN ISN'T ACCESSIBLE
set-output-textbox -date $false -value "$domainRoot is not accessible"
set-controls -switcher $false -setswitcher$false
$Computer_Textbox.IsEnabled =$false
}

#REMOVING PROCESS ON FORM CLOSE
$LAPSWindow.Add_Closing({ try{$syncHash.Clear() | Out-Null
}catch{}

Stop-Process -Name "LAPS" -ErrorAction SilentlyContinue
})

#DISPLAY FORM WHILST TESTING
$app = [Windows.Application]::new()$app.run($LAPSWindow) Enjoy! ### Building Cleaner, Responsive WPF Forms In my first two posts on this subject, I was just getting started with learning responsive WPF form building. I’m here today to show you a better way to build a responsive WPF using runspaces that will do the exact same thing as my previous uploads showed. Just better. This time, I won’t be putting the form into its own runspace. As I learnt you didn’t need to from JRV over on the TechNet forums. This has some benefits that I will very helpfully, briefly and probably incorrectly list below: • Don’t have to use syncHash when updating the form • One less runspace for the form • Having a form in its own runspace creates additional overhead and possible errors So to display the form I would use something like the below: [xml]$xml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
<Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
</Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader$xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label =$Window.FindName("Label")
$Button =$Window.FindName("Button")

$Window.ShowDialog() | Out-Null Which will give us the below form: But what if I want the button press to make the label number increase? I would use something like this on the button press: [xml]$xml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
<Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
</Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader$xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label =$Window.FindName("Label")
$Button =$Window.FindName("Button")

$Button.Add_Click({$counter = 1

do{
Start-Sleep -Milliseconds 5
$label.content =$counter
[System.Windows.Forms.Application]::DoEvents()
$counter += 1 }while ($counter -le 5000)

})

$Window.ShowDialog() | Out-Null This produces a form which increases the label up to 5000 when the button is pressed. You can see this below: But what if I want to actually run something in a runspace. For example, test the connection to google.com? Then I would use the below code: [xml]$xml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
<Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
</Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader$xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label =$Window.FindName("Label")
$Button =$Window.FindName("Button")

$Button.Add_Click({$syncHash = [hashtable]::Synchronized(@{})
$Runspace = [runspacefactory]::CreateRunspace()$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

$powershell = [powershell]::Create().AddScript({$connection = Test-Connection -ComputerName google.com -Count 5
$syncHash.output = [math]::Round(($connection.ResponseTime | Measure-Object -Average).Average)
})

$powershell.Runspace =$Runspace

$Object =$powershell.BeginInvoke()

do {
Start-Sleep -Milliseconds 50
[System.Windows.Forms.Application]::DoEvents()
}while(!$Object.IsCompleted)$powershell.EndInvoke($Object)$powershell.Dispose()

$label.Content =$syncHash.output
})

$Window.ShowDialog() | Out-Null All the above examples will stay responsive whilst the action is performed. There are a couple of different methods to do this as you can see above, for anything that takes some time to complete, a runspace is needed. But when you are updating the form quickly, like the counter, then no runspace is needed. Enjoy! ### Launching a Remote Console, the Smart Way Today, I kind of stumbled across a way of connecting to a remote host quite a bit more sophisticated. Basically, the difference is that it will check to see if the connection was successful or not. This still works off the basic premise of opening a new PowerShell process and using the -ArgumentList parameter to instantly connect to a remote system. This is the command I use (THIS IS JUST FOR CLEARER EXPLANATION, USE THE ONE-LINER BELOW): Start-Process PowerShell -ArgumentList "-noexit -command Clear-Host; try{ Enter-PSSession -ComputerName$COMPUTERNAMEHERE -ErrorAction Stop;
Write-Host 'You should now be connected to the remote host, check below...' -ForeGroundColor Green

}catch{
Write-Host 'Could not perform action - most likely that access is denied for the invoke' -ForeGroundColor Red

}
"

Or as a one-liner:

Start-Process PowerShell -ArgumentList "-noexit -command Clear-Host; try{ Enter-PSSession -ComputerName $COMPUTERNAMEHERE -ErrorAction Stop; Write-Host 'You should now be connected to the remote host, check below...' -ForeGroundColor Green }catch{Write-Host 'Could not perform action - most likely that access is denied for the invoke' -ForeGroundColor Red } " This is what the command does • Starts a new powershell console • clears the host • tries to the connection • writes output to console Enjoy! ### Handling Textbox Keydown Events Welcome to another instalment of “how much can I confused myself today…” Here, I will you how to recognise a keydown event on a textbox and also how to “identify” which key was pressed. This was useful to me because I wanted a button to be pressed when the user pressed the enter key whilst typing in a textbox. Similar to when you type a question into Google and press enter instead of pressing the search button. First, I found what control I want the event to handle and added a raiseevent onto the button I wanted pressing. You can see this below:$syncHash.Textbox.Add_KeyDown({
if ($args[1].key -eq 'Enter'){$syncHash.Button.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) } }) So in this scenario, when the user wants to search they can just press enter in the textbox and the button will be pressed. You can also do this for the entire form. Meaning that if you have multiple textboxes and want an enter in any of them to press a button, you can just put the handler onto the entire form. You can see this below:$syncHash.Window.Add_KeyDown({
if ($args[1].key -eq 'Enter'){$syncHash.Button.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs -ArgumentList $([System.Windows.Controls.Button]::ClickEvent))) } }) Enjoy! ### Adding an BASE64 Icon to a WPF GUI Nice and simple one today. I’m going to show you how to add an icon to a WPF GUI in PowerShell using BASE64 data. I won’t be putting my BASE64 data into this post since its a MASSIVELY long string of characters but it should look something like this ” iVBORw0KG…” First, we need to create a new variable to hold the data and then use the bitmapimage object to convert the data into a usable icon. You can see this below: [string]$script:base64=@"
iVBORw0KGgo...
"@

$script:bitmap = New-Object System.Windows.Media.Imaging.BitMapImage$bitmap.BeginInit()
$bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64)
$bitmap.EndInit()$bitmap.Freeze()

After this we can simply assign the new icon to the form using the code below:

$window.Icon =$bitmap

Enjoy!

### Responsive PowerShell WPF Form Introduction #2

Following on from my last post, I’m going to show you how to update a textbox using a button on the same form. I will be adding the following code starting on line 38:

#BUTTON LOGIC
$syncHash.Button.Add_Click({$syncHash.Window.Dispatcher.Invoke(
[action]{
$syncHash.TextBox.AppendText("This is a test") } ) }) This is fairly basic in what it does. It just adds “This is a test” to the textbox. Say if I want the button to run a task and then update the textbox with the results, but the results took a long time to come, the form would freeze. This is because whatever command you run in the same runspace as the GUI, takes controls and stops the GUI being responsive. So, what I’m going to do is ping google 5 times, get the average from all of those and then update the textbox without the GUI becoming unresponsive. To do this, I’m going to create a new runspace and add the code I want to run. You can see this below: #BUTTON LOGIC$syncHash.Button.Add_Click({
#ASSIGNING HOST VARIABLE
$syncHash.host =$Host
#CREATING NEW RUNSPACE
$pingrunspace = [runspacefactory]::CreateRunspace()$pingrunspace.ApartmentState = "STA"
$pingrunspace.ThreadOptions = "ReuseThread"$pingrunspace.Open()
#PUTTING THE SYNCHASH VARIABLE INSIDE THE NEW RUNSPACE
$pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

#THIS IS THE CODE THAT WILL BE EXECUTED IN THE NEW RUNSPACE
$code = { #CONNECTION TO GOOGLE AND CALCULATING AVERAGE IN NEW RUNSPACE$connection = Test-Connection -ComputerName google.co.uk -Count 5
$average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
#UPDATING THE TEXTBOX WITH CONNECTION AVERAGE IN NEW RUNSPACE
$syncHash.Window.Dispatcher.Invoke( [action]{$syncHash.TextBox.AppendText($average) } ) } #ADDING AND RUNNING THE CODE IN THE NEW RUNSPACE$PSInstance = [powershell]::Create().AddScript($code)$PSinstance.Runspace = $pingrunspace$job = $PSinstance.BeginInvoke() }) This will run the code in a separate runspace to the GUI and allow you to interact with it whilst the commands complete in the background. Just in case you want the entire this, this is what the whole file looks like 🙂 #CREATE HASHTABLE AND RUNSPACE FOR GUI$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({ [xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Name="Window" Height="400" Width="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
<TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
</Grid>
</Window>
"@

$reader=(New-Object System.Xml.XmlNodeReader$xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load($reader )

#EXTRACT THE CONTROLS FROM THE GUI
$syncHash.TextBox =$syncHash.window.FindName("Textbox")
$syncHash.Button =$syncHash.Window.FindName("Button")

#BUTTON LOGIC
$syncHash.Button.Add_Click({$syncHash.host = $Host$pingrunspace = [runspacefactory]::CreateRunspace()
$pingrunspace.ApartmentState = "STA"$pingrunspace.ThreadOptions = "ReuseThread"
$pingrunspace.Open()$pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)$code = {

$connection = Test-Connection -ComputerName google.co.uk -Count 5$average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)$syncHash.Window.Dispatcher.Invoke(
[action]{
$syncHash.TextBox.AppendText($average)
}
)

}

$PSInstance = [powershell]::Create().AddScript($code)
$PSinstance.Runspace =$pingrunspace
$job =$PSinstance.BeginInvoke()
})

#FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
$syncHash.Window.ShowDialog() | Out-Null$syncHash.Error = $Error$Runspace.Close()
$Runspace.Dispose() }) #LOAD RUNSPACE WITH GUI IN$psCmd.Runspace = $newRunspace$data = $psCmd.BeginInvoke() Enjoy! ### Responsive PowerShell WPF Form Introduction #1 Hooooly jebus chwist! This took a LONG time for me to get my head around and an even longer time to implement and get working (still breaking it every minute!). I used this website and this website to help me learn the basics. Today, I’m going to show you how to create a responsive WPF from using PowerShell. This utilises runspaces and a synchronised hashta… never mind the technical stuff! This is the code that I used: #CREATE HASHTABLE AND RUNSPACE FOR GUI$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({ [xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Name="Window" Height="400" Width="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
<TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
</Grid>
</Window>
"@

$reader=(New-Object System.Xml.XmlNodeReader$xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load($reader )

#EXTRACT THE CONTROLS FROM THE GUI
$syncHash.TextBox =$syncHash.window.FindName("Textbox")
$syncHash.Button =$syncHash.Window.FindName("Button")

#FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
$syncHash.Window.ShowDialog() | Out-Null$syncHash.Error = $Error$Runspace.Close()
$Runspace.Dispose() }) #LOAD RUNSPACE WITH GUI IN$psCmd.Runspace = $newRunspace$data = $psCmd.BeginInvoke() Using this, you can then use the same command prompt used to launch the script to change the form. E.g. to change the text in the textbox we would use:$syncHash.Window.Dispatcher.Invoke(
[action]{\$syncHash.TextBox.Text = "Updated text here"}
)

In another post, I’ll show you how to update the textbox using a button on the same form. Exciting stuff, right?