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

34 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}

  2. Hi Naz,

    Is there any way to run this GUI in MDT without using SCCM?
    I kinda need this for MDT only.
    Thanks.

    • 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.

      • Hi! MDT works.
        copy extract the zip file to DeploymentShare\Script\Choose_Disk_WPF_GUI
        task “Choose Disk to Install OS”, command line:
        %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -STA -NoProfile -ExecutionPolicy Bypass -File %SCRIPTROOT%\Script\Choose_Disk_WPF_GUI\ChooseDiskWPF.ps1

  3. 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!

  4. Hi, is there a problem with the ChooseDiskWPF.ps1 script file?
    # Load the XAML form and create the PowerShell Variables
    LoadForm -XamlPath? MyScriptDir \ ChooseDiskXAML.xaml?

    # Create empty array of hard disk numbers
    $ Global: ArrayOfDiskNumbers = @ ()

    ? MyScriptDir \ ChooseDiskXAML.xaml? Is this statement open? No. Modifying “$ MyScriptDir \ ChooseDiskXAML.xaml?” Is also not possible. Later, I copied all the script language from the web page and overwritten it to run the gui normally.

    • This should be a character set issue, you can manually modify it to double quotes. In addition, I would like to ask, how to write the ps command using serviceUI in the task sequence?

  5. BIG THX
    it also works with mdt, the only thing what i changed was the variable “OSDDiskIndex” to “ChooseDiskIndex”, so im able to format all other disks too.

  6. Works like a charm in MDT 8456. Great work Naz!!
    * Added both steps before Format and Partition Disk
    * Added the scripts in a subfolder in MDT Scripts folder. %SCRIPTROOT%\Custom\ChooseDiskWPF\ChooseDiskWPF.ps1″

    Thank you Stefan for the variable “ChooseDiskIndex” for MDT. ;-)

    • For me, deployment fails for system with multiple disks. It fails with disk selection as well.
      Could you recommend something for troubleshooting that?
      The important point is to use a variable for target disk – i had this option in my MDT.

  7. Hello,
    Why does the GUI choose disk1, but it will still be installed on disk 0 when installing the system? Do partitions and formatting and application operating systems need to set variables?

  8. Selam,
    Ben 4 adet sanal disk ile VM kurdum ve Hata kodu aldım:
    The task sequence execution engine failed execution of a task sequence. The operating system reported error 2147942561: The specified path is invalid.
    Sebebi nedir?

    • Yukarıda söylediklerinizi harfiyen yaptım ama yine hata alıyorum.
      2 adet disk olunca çalışıyor. 1 ve ya 3 4 vs olunca hata veriyor.

  9. So, waste several hours building a GUI and creating task sequences for something that is done out of the box with WDS…and done better at that (create partitions, format them, resize them, etc.). Microsoft’s idea of progress I guess…

  10. Powershell report:

    New-Object : Retrieving the COM class factory for component with CLSID {00000000-0000-0000-0000-000000000000} failed due to the following error:
    80040154 Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)).
    At I:\Sources\Packages\ChooseDiskWPFGUIv2\ChooseDiskWPFGUI\1_ChooseDiskWPF.ps1:19 char:17
    + $TSProgressUI = New-Object -COMObject Microsoft.SMS.TSProgressUI
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : ResourceUnavailable: (:) [New-Object], COMException
    + FullyQualifiedErrorId : NoCOMClassIdentified,Microsoft.PowerShell.Commands.NewObjectCommand

    You cannot call a method on a null-valued expression.
    At I:\Sources\Packages\ChooseDiskWPFGUIv2\ChooseDiskWPFGUI\1_ChooseDiskWPF.ps1:20 char:1
    + $TSProgressUI.CloseProgressDialog()
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    • I have the same issue. It seems “Microsoft. SMS. TSProgressUI” did not work at x64 environment. Are there have other ways to execute the “Microsoft. SMS. TSProgressUI” ?

    • SCCM Client Report:

      The task sequence execution engine failed executing the action (Choose Disk to Install OS) in the group (Choose Disk) with the error code 2147942561
      Action output: … ore\path.cpp,104)
      RecursiveCreatePath(sPath.substr(0, nPos), psa), HRESULT=800700a1 (K:\dbs\sh\cmgm\0405_083130\cmd\22\src\Framework\Core\CCMCore\path.cpp,104)
      RecursiveCreatePath( sNormalizedPath, psa ), HRESULT=800700a1 (K:\dbs\sh\cmgm\0405_083130\cmd\22\src\Framework\Core\CCMCore\path.cpp,159)
      DownloadContentLocally (pszSource, sSourceDirectory, dwFlags, hUserToken, mapNetworkAccess), HRESULT=800700a1 (K:\dbs\sh\cmgm\0807_174415\cmd\12\src\Framework\TSCore\resolvesource.cpp,4002)
      TS::Utility::ResolveSource (pszPkgID, sPath, 0, hUserToken, mapNetworkAccess), HRESULT=800700a1 (K:\dbs\sh\cmgm\0405_083130\cmd\15\src\client\OsDeployment\InstallSoftware\runcommandline.cpp,305)
      Failed to resolve the source for SMS PKGID=MRK0003B, hr=0x800700a1
      cmd.Execute(pszPkgID, sProgramName.c_str(), sOutputVariableName.c_str(), dwCmdLineExitCode), HRESULT=800700a1 (K:\dbs\sh\cmgm\0405_083130\cmd\15\src\client\OsDeployment\InstallSoftware\main.cpp,395)
      Install Software failed to run command line, hr=0x800700a1. The operating system reported error 2147942561: The specified path is invalid.

    • I got the same error, my environment were Latest ADK and ADK PE 2004, I don’t know how to trouble shooting it.

  11. I learned how to use powershell scripts in task sequences.
    My problem is a little different, I have 120G ssd, 500G hdd.
    I think I’ll try using different partitioning strategies for different sized installation disks.
    For example:
    120G+1T, 120G single partition, 1T single partition.
    120G, 50G+60G two partitions
    500G, 100G+400G two partitions

    Great job.
    Thank you so much.

  12. $mindisk= (Get-Disk | Where-Object -FilterScript {$_.Bustype -ne ‘USB’}) | Select-Object Number, Size | Sort-Object Size | Select-Object -First 1 | %{$_.Number}
    $TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
    $TSEnv.Value(“OSDDiskIndex”) = $mindisk

    Who will help me modify the script for installing the system on a smaller disk? The test failed.

    • $mindisk= (Get-Disk | Where-Object -FilterScript {$_.Bustype -ne ‘USB’}) | Select-Object Number, Size | Sort-Object Size | Select-Object -First 1
      $TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
      $TSEnv.Value(“OSDDiskIndex”) = $mindisk.Number}

      This test passed
      It’s still me. Just now, it was also a question I raised. I tried it and it passed.

      • Only after passing this task, the installation system still reported an error.

        FAILURE(5616):15250:Verify BCDBootEx

        Litetouch deployment failed,Return Code = -2147467259 0x80004005

        Failed to run the action : install operating system.

        unknown error (error: 0000015f0;source:unknown)

        operation aborted(Error:800040004;source:Windows)

        Task Sequence Engine failed! Code: enExcutionFail

        Task sequence execution failed with error code 80004005

        GetTsRegValue() is unsuccessful . 0x80070002

        error task sequence manager failed to execute task sequence.code 0x80004005

  13. FAILURE(5616):15250:Verify BCDBootEx

    This is caused by disk partitioning, deleting all partitions on the disk can solve the problem.
    Of course, it’s me asking again and I’m answering.

Leave a comment