Choose a Disk to Install Windows on using WPF and PowerShell

I recently tweeted a screenshot of a GUI I created using WPF and PowerShell to let engineers choose the disk to install Windows on, intended to be used in a SCCM Task Sequence. I was then asked by (none other than!) David Segura to share this with the rest of the community.

In my last post I wrote about how I found a workaround to a snag I hit upon while using the MahApps.Metro theme. That was almost 10 months ago. That post was meant to be a precursor to introducing this GUI but I got busy with life and my new job so the blog took a back seat. I’m glad that David’s reply has spurred me on to write this post and introduce the GUI. (I also have a few more posts lined up inspired by Gary Blok’s endeavour to break away from MDT and go native ConfigMgr. More on that soon.)

Update 1: I included steps in the Task Sequence to make the GUI appear only if more than one disk is present, as suggested by Marcel Moerings in the comments.

Update 2: I updated the script to exclude USB drives.

Background

SCCM will install the OS on disk 0 by default. In my previous environment two disk configurations were very common so I created this GUI for Engineers to choose the disk to install Windows on. This works by leveraging the “OSDDiskIndex” task sequence variable. If you set this variable to your desired disk number then SCCM will install the OS on that disk.

This is what the GUI looks like when run in full OS:

clip_image003

And this is what it looks like when run in a Task Sequence:

clip_image005

Prerequisites

You will need to add the following components to your Boot Image:

  • Windows PowerShell (WinPE-PowerShell)
  • Windows PowerShell (WinPE-StorageWMI)
  • Microsoft .Net (WinPE Dot3Svc)

How to Implement

Simples :)

  • Download the solution and extract the zip file
  • Create a standard package with the contents of the zip file. Do not create a program.
  • In your task sequence add a Group called “Choose Disk” before the partitioning steps
  • Within the group add a Run Command Line task and name it “Check if there’s more than one Hard Disk”. Enter the following one-liner:
PowerShell.exe -NoProfile -Command "If ((Get-Disk | Where-Object -FilterScript {$_.Bustype -ne 'USB'}).Count -gt 1) {$TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment;$TSEnv.Value('MoreThanOneHD') = $true}"

one liner

  • Add another Run Command Line step and name it “Choose Disk to Install OS”, and choose the package you created. Add the following command line:
%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -STA -NoProfile -ExecutionPolicy Bypass -File .\ChooseDiskWPF.ps1

choose disk

  • In the Options tab for the “”Choose Disk to Install OS” step, Click on Add Condition > Task Sequence Variable > type “MoreThanOneHD” in the variable field, set the condition to “equals” and the value to “TRUE”

condition

Bear in mind that this does not exclude removable drives. This is because I always run a set of pre-flight checks as a first step which weed out any removable drives before this GUI is presented hence looking out for removable drives was not duplicated in this solution.

The Code

Clear-Host 

 

# Assign current script directory to a global variable

$Global:MyScriptDir = [System.IO.Path]::GetDirectoryName($myInvocation.MyCommand.Definition)

 

# Load presentationframework and Dlls for the MahApps.Metro theme

[System.Reflection.Assembly]::LoadWithPartialName(“presentationframework”| Out-Null

[System.Reflection.Assembly]::LoadFrom($Global:MyScriptDir\assembly\System.Windows.Interactivity.dll”| Out-Null

[System.Reflection.Assembly]::LoadFrom($Global:MyScriptDir\assembly\MahApps.Metro.dll”| Out-Null

 

Temporarily close the TS progress UI

$TSProgressUI = New-Object COMObject Microsoft.SMS.TSProgressUI

$TSProgressUI.CloseProgressDialog()

 

# Set console size and title

$host.ui.RawUI.WindowTitle = “Choose hard disk…”

 

Function LoadForm {

    [CmdletBinding()]

    Param(

     [Parameter(Mandatory=$True,Position=1)]

     [string]$XamlPath

    )

  

    # Import the XAML code

    [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]”| ForEach {

        Set-Variable -Name ($_.Name) -Value $xamGUI.FindName($_.Name) -Scope Global

    }

}

 

Function Get-SelectedDiskInfo {

    # Get the selected disk with the model which matches the model selected in the List Box

    $SelectedDisk = Get-Disk | Where-Object $_.Number eq $Global:ArrayOfDiskNumbers[$ListBox.SelectedIndex] }

 

    # Unhide the disk information labels

    $DiskInfoLabel.Visibility = “Visible”

    $DiskNumberLabel.Visibility = “Visible”

    $SizeLabel.Visibility = “Visible”

    $HealthStatusLabel.Visibility = “Visible”

    $PartitionStyleLabel.Visibility = “Visible”

 

    # Populate the labels with the disk information

    $DiskNumber.Content = $($SelectedDisk.Number)

    $HealthStatus.Content = $($SelectedDisk.HealthStatus)$($SelectedDisk.OperationalStatus)

    $PartitionStyle.Content = $SelectedDisk.PartitionStyle

 

    # Work out if the size should be in GB or TB

    If ([math]::Round(($SelectedDisk.Size/1TB),2lt 1) {

        $Size.Content = $([math]::Round(($SelectedDisk.Size/1GB),0)) GB”

    }

    Else {

        $Size.Content = $([math]::Round(($SelectedDisk.Size/1TB),2)) TB”

    }

}

 

# Load the XAML form and create the PowerShell Variables

LoadForm XamlPath $MyScriptDir\ChooseDiskXAML.xaml

 

# Create empty array of hard disk numbers

$Global:ArrayOfDiskNumbers = @()

 

# Populate the listbox with hard disk models and the array with disk numbers

Get-Disk | Where-Object -FilterScript {$_.Bustype -ne ‘USB’} | Sort-Object {$_.Number}| ForEach {

    # Add item to the List Box

    $ListBox.Items.Add($_.Model) | Out-Null

  

    # Add the serial number to the array

    $ArrayOfDiskNumbers += $_.Number

}

 

# EVENT Handlers

$OKButton.add_Click({

    If no disk is selected in the ListBox then do nothing

    If (-not ($ListBox.SelectedItem)) {

        # Do nothing

    }

    Else {

        Else If a disk is selected then get the disk with matching disk number according to the ListBox selection

        $Disk = Get-Disk | Where-Object {$_.Number eq $Global:ArrayOfDiskNumbers[$ListBox.SelectedIndex]}

      

        Set the Task Sequence environment object

        $TSEnv = New-Object COMObject Microsoft.SMS.TSEnvironment

 

        # Populate the OSDDiskIndex variable with the disk number

        $TSEnv.Value(OSDDiskIndex= $Disk.Number

 

        # Close the WPF GUI

        $xamGUI.Close()

    }

})

 

$ListBox.add_SelectionChanged({ 

    # Call function to pull the disk informaiton and populate the details on the form

    Get-SelectedDiskInfo

})

 

# Launch the window

$xamGUI.ShowDialog(| Out-Null

8 thoughts on “Choose a Disk to Install Windows on using WPF and PowerShell

    • Hi Marcel, thank you for your suggestion. I updated the post with instructions on how to add logic to the Task Sequence to only show the GUI if more than one disk is available. I also updated the script to exclude USB drives. Thank you very much for taking the time to comment!

  1. Thanks for this – it’s going to be very helpful!
    I think there’s a typo here:
    $TSEnv.Value(‘MoreThanOneHD $true}
    it should be:
    $TSEnv.Value(‘MoreThanOneHD’)= $true}

    • Hi Mikkel, sorry for the late reply. I didn’t test this with MDT but I don’t see any reason why it shouldn’t. If you copy the script and folder to the Scripts folder in your deployment share then you should be able to run it with a Run Command Line step using:

      %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -STA -NoProfile -ExecutionPolicy Bypass -File %SCRIPTROOT%\ChooseDiskWPF.ps1

      You might need to set the ‘Start in’ field to %SCRIPTROOT%

      Let me know if this works or not.

  2. Thank you so much, this worked beautifully for me. I did have to make a few changes so that it would run in my 64-bit boot image though.

    1) For the “Multiple Disks” and “Choose Disk” steps I had to check “Disable 64-bit file system redirection”, otherwise the “New-Object -COMObject Microsoft.SMS.TSEnvironment” fails.

    2) Also, WinPE didn’t seem to like accessing the DLL’s directly from the network so I had to add 2 lines to the PS script which copies the package contents to a local folder (I figured X:\choosedisk would be a safe spot), then I had to edit the “Assign current script directory to a global variable” section to reference that local folder instead of the package location. Those lines are…

    New-Item X:\choosedisk -ItemType Directory
    xcopy.exe “.\*.*” “X:\choosedisk” /D /E /C /I /Q /H /R /Y /S

    # Assign current script directory to a global variable
    $Global:MyScriptDir = “X:\choosedisk”

    After that everything worked as expected.

    Thanks again!

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s