Building an OSD Front End using C#: Part 5 – Computer Name Form Design

Now that we have the basic layout of the computer name tab built but we don’t have any of the computer naming logic that’s our next step.

When it comes to the layout, at the same level as the button from our last example or right below the closing </Grid.RowDefinitions> tag we’re going to start building our computer name input form. We’ll start by adding a <DockPanel> control.

<DockPanel Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"&gt;

</DockPanel&gt;

A dock panel is a control which will allow us to dock various controls inside it and get them to stretch to the full width. In this case, I wanted to have our group box controls stretch to fill the width of the tab so we don’t have weird ratios of empty space as we move between tabs or between different group box controls on the same tab. As with the button we assign the dock panel to a row but in this case we assign to Grid.Row="0" which is the very first row in the grid. The horizontal and vertical alignments are set to stretch so the dock panel fills all the space available.

Now, let’s build our computer name section of the form.

<GroupBox DockPanel.Dock="Top" Header="Computer Name" VerticalAlignment="Top" Margin="5,5,5,5"&gt;
    <Grid HorizontalAlignment="Center"&gt;
        <Grid.RowDefinitions&gt;
            <RowDefinition Height="Auto" /&gt;
        </Grid.RowDefinitions&gt;
        <Grid.ColumnDefinitions&gt;
            <ColumnDefinition Width="Auto" /&gt;
            <ColumnDefinition Width="Auto" /&gt;
        </Grid.ColumnDefinitions&gt;
        <Label Grid.Row="0" Grid.Column="0" Content="Enter Computer Name:" HorizontalAlignment="Left" Margin="10,10,10,10" VerticalAlignment="Top" /&gt;
        <TextBox Grid.Row="0" Grid.Column="1" x:Name="TextBoxComputerName" Width="175" HorizontalAlignment="Left" Margin="10,10,10,10" VerticalAlignment="Top" CharacterCasing="Upper" TextChanged="TextBoxComputerName_TextChanged" /&gt;
    </Grid&gt;
</GroupBox&gt;

We’re using a new control called a group box. It lets us put groups of content together into a single control when they’re related. In this case it’s to contain a label and a text box which prompts for a computer name to be inputted.

You’ll notice that we again use a grid and this time we have both rows and columns defined to help us align everything and make it look neat and orderly.

There is a TextChanged event trigger that we’ve added to the text box. We can ignore that for now and we’ll get to that later on in the behind the form code section.

The next part of the computer name entry form is the table where we show any validation rules and if they pass or fail:

<GroupBox DockPanel.Dock="Top" Header="Computer Name Validation" VerticalAlignment="Top" Margin="5,5,5,5"&gt;
    <Grid HorizontalAlignment="Left" x:Name="GridComputerNameRules"&gt;
        <Grid.ColumnDefinitions&gt;
            <ColumnDefinition Width="Auto" /&gt;
            <ColumnDefinition Width="Auto" /&gt;
        </Grid.ColumnDefinitions&gt;
        <Grid.RowDefinitions&gt;
            <RowDefinition Height="Auto" /&gt;
            <RowDefinition Height="Auto" /&gt;
            <RowDefinition Height="Auto" /&gt;
            <RowDefinition Height="Auto" /&gt;
        </Grid.RowDefinitions&gt;
        <Label Grid.Column="0" Grid.Row="0" Content="REQ - Length &gt;= 5:" x:Name="LabelRuleGreaterThan" /&gt;
        <Label Grid.Column="1" Grid.Row="0" Content="False" x:Name="LabelRuleGreaterThanStatus" Foreground="{StaticResource InvalidItemBrush}" /&gt;
        <Label Grid.Column="0" Grid.Row="1" Content="REQ - Length <= 15:" x:Name="LabelRuleLessThan" /&gt;
        <Label Grid.Column="1" Grid.Row="1" Content="False" x:Name="LabelRuleLessThanStatus" Foreground="{StaticResource InvalidItemBrush}" /&gt;
        <Label Grid.Column="0" Grid.Row="2" Content="OPT - Starts with UMN:" x:Name="LabelRuleStartsWith" /&gt;
        <Label Grid.Column="1" Grid.Row="2" Content="False" x:Name="LabelRuleStartsWithStatus" Foreground="{StaticResource InvalidItemBrush}" /&gt;
        <Label Grid.Column="0" Grid.Row="3" Content="OPT - Ends with blah:" x:Name="LabelRuleEndsWith" /&gt;
        <Label Grid.Column="1" Grid.Row="3" Content="False" x:Name="LabelRuleEndsWithStatus" Foreground="{StaticResource InvalidItemBrush}" /&gt;
    </Grid&gt;
</GroupBox&gt;

Here we see a lot of things we’ve built into the form already just in a slightly different way. You will notice we’re leveraging a {StaticResource} for InvalidItemBrush. This lets us define the color of an invalid item and then simply reference it and change it by changing our theme file. If you didn’t use the UMN theme file you may need to configure this in a xaml file.

Now we can move behind the scenes to look at how this works. However, before we move behind the form we’ll need to spend a little time on how we’re going to handle settings and configuration. In the next post we’ll go over the AppSettings.json file and how we parse the json file and use those settings in the application.

User Data Backup in OSD: Windows 7 to 10 Migration

Currently, the University of Minnesota is working on our Windows 7 to 10 migration.  One of the key risks identified is that user data could be lost during the transition.  Our current process involves our desktop support technicians manually backing up user data, then running a wipe and reload task sequence followed by a manual restore of user data.  This manually intensive process is also not very consistent.

We have decided to take a two-pronged approach to dealing with user data in such a way as to keep our data loss risk as low as possible.  We will be using USMT in our task sequence to backup and restore user data along with a "just in case" complete disk image using MDT to create a WIM of the hard drive.

Using USMT

User State Migration Toolkit is fairly easy to get up and running with in any task sequence.

Before beginning to leverage USMT you’ll need to set up a state migration point. We needed to reconfigure how our boundaries were setup to leverage IP boundaries to make sure the migration point was available to the proper devices. I’d recommend reading through the Microsoft documentation on state migration as it’s a wealth of knowledge about setting up a state migration point.

Once you have your state migration point setup and working you’ll need to create a task sequence that is able to leverage USMT. We decided to do USMT in WinPE because it made everything much simpler and ran faster. There are some quirks to things that migrate offline vs online and they’re also worth considering as you build your task sequence.

Since we’re doing a re-image with the task sequence if we start in windows we want to make sure Bitlocker is suspended prior to rebooting into WinPE. We do this using the built in Disable Bitlocker step in SCCM task sequences and have it target the current operating system drive. It’s also set to continue on error to avoid some issues we had on test devices and haven’t had issues in production with this configuration.

Capturing User State

After disabling Bitlocker from Windows restart the device into WinPE and the first USMT step is to Request State Store which will look at what state store’s are assigned to the device and fail if none are found. This step should fail on error. We request the store to Capture state from the computer and try 5 times with a 60 second delay and to fail over to the network access account if the computer account fails.

Now that we have a state store we can capture our user state using the Capture User State step. You’ll need to point it to the built in User State Migration Tool for Windows package. If you don’t have this or need to generate a new package there is a built in template for it in the package wizard.

We use four files, MigApp.xml and MigUser.xml are both the standard built in configuration files provided by USMT and should already be in your USMT package. We’ve also built two custom USMT files. See below for their contents and a brief description of what they’re doing. Along with the files we’ve enabled verbose logging, skipping files that use EFS and to copy the files using offline/win-pe mode. If you’re not using offline mode you’ll want to not check this box and/or check the VSS box depending on your own scenarios and based on your own testing.

OITCM-CustomeExclude.xml as shown below is designed to not include the computer certificate in the backup (the SkipMachineCerts) section which caused issues early on in testing with some devices.

We’ve also excluded specific user data locations where we didn’t want installers and/or other files coming through. We also decided to not include any startup or start menu shortcuts as this brought over all sorts of shortcuts that were broken on the device after re-imaging.

<?xml version="1.0" encoding="UTF-8"?&gt;
<migration urlid="http://www.microsoft.com/migration/1.0/migxmlext/customExclude"&gt;
   <component type="Documents" context="System"&gt;
      <displayName&gt;SkipMachineCerts</displayName&gt;
      <role role="Data"&gt;
         <rules&gt;
            <include&gt;
               <objectSet&gt;
                  <pattern type="Registry"&gt;HKLM\SOFTWARE\Microsoft\SystemCertificates\My\Certificates\*[*]</pattern&gt;
               </objectSet&gt;
            </include&gt;
			<unconditionalExclude&gt;
               <objectSet&gt;
                  <pattern type="Registry"&gt;HKLM\SOFTWARE\Microsoft\SystemCertificates\My\Certificates\*[*]</pattern&gt;
               </objectSet&gt;
            </unconditionalExclude&gt;
         </rules&gt;
      </role&gt;
   </component&gt;
   
   <!-- This component EXCLUDES the following User specific stuff--&gt;
   <component type="System" context="user"&gt;
       <displayName _locID="miguser.User_Exclusions"&gt;User Exclusions</displayName&gt;
       <role role="Data"&gt;
           <rules&gt;
               <unconditionalExclude&gt;
                   <objectSet&gt;

                   <!-- exclude blank links on the desktop--&gt;    
                   <pattern type="File"&gt;%CSIDL_DESKTOP%\* [*.msi]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOP%\* [*.exe]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOP%\* [*.lnk]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOP%\* [*.lnk2]</pattern&gt;

                   <pattern type="File"&gt;%CSIDL_DESKTOPDIRECTORY%\* [*.msi]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOPDIRECTORY%\* [*.exe]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOPDIRECTORY%\* [*.lnk]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DESKTOPDIRECTORY%\* [*.lnk2]</pattern&gt;



                   <pattern type="File"&gt;%CSIDL_DEFAULT_DESKTOP%\* [*.msi]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DEFAULT_DESKTOP%\* [*.exe]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DEFAULT_DESKTOP%\* [*.lnk]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DEFAULT_DESKTOP%\* [*.lnk2]</pattern&gt;


                   <pattern type="File"&gt;%CSIDL_DEFAULT_PROGRAMS%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DEFAULT_STARTMENU%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_DEFAULT_STARTUP%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_PROGRAMS%\* [*]</pattern&gt;   		                 
                   <pattern type="File"&gt;%CSIDL_STARTMENU%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_STARTUP%\* [*]</pattern&gt;

                   </objectSet&gt;
               </unconditionalExclude&gt;
           </rules&gt;
       </role&gt;
   </component&gt;

   <!-- This component EXCLUDES the following shared User specific stuff--&gt;
   <component type="System" context="System"&gt;
       <displayName _locID="miguser.Shared_User_Exclusions"&gt;Shared User Exclusions</displayName&gt;
       <role role="Data"&gt;
           <rules&gt;
               <unconditionalExclude&gt;
                   <objectSet&gt;


                   <pattern type="File"&gt;%CSIDL_COMMON_DESKTOPDIRECTORY%\* [*.msi]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_COMMON_DESKTOPDIRECTORY%\* [*.exe]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_COMMON_DESKTOPDIRECTORY%\* [*.lnk]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_COMMON_DESKTOPDIRECTORY%\* [*.lnk2]</pattern&gt;



                   <pattern type="File"&gt;%CSIDL_COMMON_PROGRAMS%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_COMMON_STARTMENU%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_COMMON_STARTUP%\* [*]</pattern&gt;

                   <pattern type="File"&gt;%CSIDL_PROGRAMS%\* [*]</pattern&gt;   		                 
                   <pattern type="File"&gt;%CSIDL_STARTMENU%\* [*]</pattern&gt;
                   <pattern type="File"&gt;%CSIDL_STARTUP%\* [*]</pattern&gt;

                   </objectSet&gt;
               </unconditionalExclude&gt;
           </rules&gt;
       </role&gt;
   </component&gt;
</migration&gt;

In our OITCM-CustomMigrations.xml file as shown below we’ve set up custom migrations for several highly used applications within our environment. I’ve also left in a very specific example of how to migrate a programdata folder as that was also helpful for us. We have Firefox, Thunderbird, Chrome, Perceptive Content, Downloads, and Custom File Extensions added into the migration.

<?xml version="1.0" encoding="UTF-8"?&gt;
<migration urlid="http://www.microsoft.com/migration/1.0/migxmlext/oitcmcustom"&gt;
    <!-- Start Firefox Migration --&gt;
    <component type="Documents" context="User"&gt;
        <displayName&gt;Mozilla Migration</displayName&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File","%CSIDL_APPDATA%\Mozilla")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%CSIDL_APPDATA%\Mozilla\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
            </rules&gt;
        </role&gt;
    </component&gt;

    <component type="Documents" context="User"&gt;
        <displayName&gt;Thunderbird Migration</displayName&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File","%CSIDL_APPDATA%\Thunderbird")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%CSIDL_APPDATA%\Thunderbird\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End Firefox Migration --&gt;

    <!-- Start Chrome Migration --&gt;
    <component type="Documents" context="User"&gt;
        <displayName&gt;Google Chrome Migration</displayName&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File","%CSIDL_LOCAL_APPDATA%\Google\Chrome")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%CSIDL_LOCAL_APPDATA%\Google\Chrome\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End Chrome Migration --&gt;

    <!-- Start ImageNow/Perceptive Content Migration --&gt;
    <component type="Documents" context="System"&gt;
        <displayName&gt;Perceptive Content Migration</displayName&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File","%CSIDL_COMMON_APPDATA%\ImageNow")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%CSIDL_COMMON_APPDATA%\ImageNow\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End ImageNow/Perceptive Content Migration --&gt;

    <!-- Start ImageNow/Perceptive Content Backup Migration --&gt;
    <component type="Documents" context="System"&gt;
        <displayName&gt;Perceptive Content Backup Migration</displayName&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File", "%CSIDL_COMMON_APPDATA%\ImageNow722-Archive")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%CSIDL_COMMON_APPDATA%\ImageNow722-Archive\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End ImageNow/Perceptive Content Backup Migration --&gt;

    <!-- Start Downloads Migration --&gt;
    <component type="Documents" context="User"&gt;
        <displayName&gt;My Downloads</displayName&gt;
        <paths&gt;
            <path type="File"&gt;%FOLDERID_DOWNLOADS%</path&gt;
        </paths&gt;
        <role role="Data"&gt;
            <detects&gt;
                <detect&gt;
                    <condition&gt;MigXmlHelper.DoesObjectExist("File","%FOLDERID_DOWNLOADS%")</condition&gt;
                </detect&gt;
            </detects&gt;
            <rules&gt;
                <include filter='MigXmlHelper.IgnoreIrrelevantLinks()'&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%FOLDERID_DOWNLOADS%\* [*]</pattern&gt;
                    </objectSet&gt;
                </include&gt;
                <merge script="MigXmlHelper.DestinationPriority()"&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%FOLDERID_DOWNLOADS% [desktop.ini]</pattern&gt;
                    </objectSet&gt;
                </merge&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End Downloads Migration --&gt;

    <!-- Start known file extensions --&gt;
    <component type="Documents" context="System"&gt;
        <displayName&gt;Custom File Extensions</displayName&gt;
        <role role="Data"&gt;
            <rules&gt;
                <include&gt;
                    <objectSet&gt;
                        <script&gt;MigXmlHelper.GenerateDrivePatterns ("* [*.pdf]", "Fixed")</script&gt;
                    </objectSet&gt;
                </include&gt;
                <exclude&gt;
                    <objectSet&gt;
                        <pattern type="File"&gt;%PROFILESFOLDER%\* [*]</pattern&gt;
                        <pattern type="File"&gt;%CSIDL_WINDOWS%\* [*]</pattern&gt;
                        <pattern type="File"&gt;%CSIDL_PROGRAM_FILES%\* [*]</pattern&gt;
						<pattern type="File"&gt;%CSIDL_COMMON_APPDATA%\Johnson Controls\* [*]</pattern&gt;
                        <!--We are trying to remove system files from other windows installation on the same machine--&gt;
                        <!--This is the best guess we can come up with, in case of these folder name localized, we might not be
                         to do whatever we have intended here--&gt;
                        <script&gt;MigXmlHelper.GenerateDrivePatterns ("\Program Files\* [*]", "Fixed")</script&gt;
                        <script&gt;MigXmlHelper.GenerateDrivePatterns ("\Winnt\* [*]", "Fixed")</script&gt;
                        <script&gt;MigXmlHelper.GenerateDrivePatterns ("\Windows\* [*]", "Fixed")</script&gt;
                    </objectSet&gt;
                </exclude&gt;
            </rules&gt;
        </role&gt;
    </component&gt;
    <!-- End known file extensions --&gt;
</migration&gt;

Finally, once you’re all ready to go, simply add a Release State Store step and you’re done!

Restoring User State

Restoring user state is much like capturing, we need both a Request State Store step before the restore and a Release State Store added after the restore. When it comes to configuring the Restore User State step, we again point it at our USMT package and then we selected to Restore all captured user profiles with standard options. We didn’t need any additional customization on this section.

WIM Backup

To capture a WIM of the device during the task sequence as a backup you’ll need to get MDT integration if you don’t already have it. There’s good info on this on the Microsoft docs page. You’ll also need an MDT toolkit package and this guide walks you through the basics of setting up your first MDT enabled task sequence.

WIM Capture

Firstly, add a Use Microsoft Deployment Toolkit Package step if it’s not already in your task sequence. It should point to the MDT package you created either through the task sequence wizard or the package wizard.

The second step is to add a Gather step which will pull settings off the device which are then used by MDT for its various features. We set it up to just Gather only local data (do not process rules) as we didn’t need anything custom or fancy, just some info about the drives. required by one of the MDT scripts we’ll be calling later on.

The third step is to set the task sequence variable ComputerBackupLocation to the location you want the WIM stored. This can/should be a network drive. This is a variable used by the MDT backup script to generate the WIM.

Finally we come to the stage where we can capture the WIM. We’ll need a Run Command Line step with the command cscript.exe "%DeployRoot%\Scripts\ZTIBackup.wsf" which will run the MDT WIM backup script.

And there you have it! That’s how we’re using USMT as well as MDT to capture user state and full disk images during a task sequence.

WIM Restore

If you’re looking to restore the WIM you’ve just captured, below are the steps. I would say this is not a 100% guaranteed restore and there will likely be issues. We’ve had good luck with it internally so far however.

Things to Gather Before Beginning

  • WinPE Boot Media
  • Windows 7 Installation Media
  • External drive or network share with the WIM

Steps

  1. Boot into a WinPE image. The SCCM boot media will suffice for this. You’ll need to use F8 to open a command prompt and then run the diskpart command to build the appropriate partition table.
    • select disk 0 (select the main disk for the system)
    • Clean
    • Create partition primary size=300
    • Format quick fs=ntfs label=”System”
    • Assign letter=”S”
    • Active
    • Create partition primary
    • Format quick fs=ntfs label=”Windows”
    • Assign letter=”C”
    • Exit
  2. Now you’ll need to use the dism tool to apply the WIM image to use the drive.
    • If you’ve stored the WIM on a network share use the net use y: \path\to\share method to mount the network share as a drive.
    • Commands to run:
      • dism /apply-image /imagefile:y:\path\to\image.wim /index:1 /ApplyDir:C:\
      • The index can be found using the dism /Get-ImageInfo /imagefile:y:\path\to\image.wim command. This is the location within the WIM where the OS is stored. You may have additional images for other partitions of the drive.
  3. Now you’ll need to boot into Windows 7 installation media and choose the recovery option.
  4. Choose the option to mark partitions as active.
    • Diskpart commmands:
      • select disk disk
      • select partition partition
      • assign
      • active
  5. Run the following commands:
    • bootrec /fixboot
    • bootrec /scanos
    • bootrec /fixmbr
    • bootrec /rebuildbcd
  6. Restart the computer and re-enter the Windows 7 recovery mode on the install media
  7. Choose the repair Windows 7 option.
  8. Choose the fix startup option.
  9. Restart the computer and login as a local admin account.
  10. Unbind and rebind the computer.

Syntax checking your Powershell code with Pester

If you just want to syntax check your Powershell code with Pester, scroll to the bottom and grab my describe block. If you’d like you’d like to go on a little journey with me, keep reading.

I’m in the process of getting as much of team’s Powershell code through a CI pipeline using Azure DevOps. Due to some issues with accidentally pushing code with syntax and encoding errors to production, one of the first things I wanted to do was just simply validate that the code was valid. I found a post on the PowerShell.org forums from 2014 that inspired me to write this test:

Continue reading “Syntax checking your Powershell code with Pester”

Using VNC For Remote Imaging in SCCM Task Sequences

I want to quickly thank Gary Blok as he was the inspiration for the process we’re currently using in his DaRT & VNC Remote during OSD without Integration post. If you’d like to leverage DaRT in WinPE I would strongly encourage looking through that blog post for additional information.

We made a choice to go pure VNC as it allowed for a consistent process through both the WinPE and Windows phases of imaging. DaRT only works while you’re in WinPE and will not in Windows so if you wanted to have remote capabilities through the whole task sequence you’d still need to build VNC for Windows.

The huge advantage of using a VNC executable in a package is that it’s a transient item that is gone after a reboot. It adds more steps to your task sequence but makes sure there’s nothing left over after the task sequence has run through.

Obtain VNC

The process is fairly simple, first you need to get the latest version of the Ultra VNC Server. From the downloads page make sure to get the bin zip ALL. You’ll also probably want to grab the latest MSI to package and deploy as a viewer for those doing the imaging.

Setup Server

For the server package, grab the winvnc.exe file out of zip for the bit level of your boot media/OS. Likely this is 64 but if you have special circumstances you may need to build a 64 and 32 bit version of the package.

Once you’ve moved the winvnc.exe file to a new directory, run it as an administrator and configure the settings. You’ll want to setup a good password so that it cannot be easily cracked. Also, configure the security settings to conform to the security standards of your environment. Once you have everything configured how you like it click apply and it should generate an UltraVNC.ini file in the same directory. If you’d like to manually configure this file check out more info in the UltraVNC docs.

Setup Powershell Launch File – WinPE

I use a Powershell file to launch the VNC Server as it lets me write a configuration file off to a file server with a super secure password in it. You could leverage the password generator for VNC to make a unique password for each device but that is a step I haven’t gone down yet.

Here is what the Powershell should look like:

$VNCConnection = @"
[connection]
host={HOST_IP}

"@
$TS = New-Object -ComObject Microsoft.SMS.TSEnvironment

$IPAddress = (Get-WmiObject win32_Networkadapterconfiguration | Where-Object{ $_.ipaddress -notlike $null }).IPaddress | Select-Object -First 1
$FileContents = $VNCConnection.Replace("{HOST_IP}", $IPAddress)

$ComputerName = $TS.Value('_SMSTSMachineName')

wpeutil DisableFirewall

cmd.exe /c start winvnc.exe

if($null -ne $ComputerName) {
    New-Item -Path "Z:\" -Name "$ComputerName.vnc" -ItemType File -Value $FileContents -Force
} else {
    New-Item -Path "Z:\" -Name "$IPAddress.vnc" -ItemType File -Value $FileContents -Force
}

Firstly, update $VNCConnection to be the contents of your ini file if you’re looking to leverage the ability to write a VNC shortcut to a file share. You’ll want to set up the host={HOST_IP} like I did as this is how we make sure the host is binding to the correct IP address.

The script will grab the first active network IP address. I have yet to run into any issues with this but I imagine you could if you have multiple network adapters and some don’t allow VNC traffic so your mileage may very on this method.

It also needs to disable the WinPE firewall. I didn’t run into a good way to allow just VNC traffic through so this could easily be a deal breaker for some organizations. If you find a good way to punch a whole through the firewall let me know in the comments or on Twitter!

It starts VNC and then generates the shortcut we previously discussed. I use the built in map network drive function of the task sequence.

Setup Powershell Launch File – WinPE

Next we have to build the Powershell script for launching the server once the task sequence is running from Windows.

$FileContents = @"
[connection]
host={HOST_IP}

@"

$TS = New-Object -ComObject Microsoft.SMS.TSEnvironment

$IPAddress = $TS.Value("IPAddress001")
$FileContents = $FileContents -replace "{HOST_IP}", $IPAddress

$ComputerName = $TS.Value('_SMSTSMachineName')

netsh advfirewall firewall add rule name="VNC OSD 5900" protocol=tcp dir=in localport=5900 action=allow

cmd.exe /c start winvnc.exe

if($null -ne $ComputerName) {
    New-Item -Path "Z:\" -Name "$ComputerName.vnc" -ItemType File -Value $FileContents
} else {
    New-Item -Path "Z:\" -Name "$IPAddress.vnc" -ItemType File -Value $FileContents
}

This script is virtually identical but this time we open a specific firewall port. In this example I set it up for the default port of 5900 but you’ll want to set it to whatever you’ve configured in your settings and once again you’ll want to be sure you have all the settings in your ini in the $FileContents variable.

Adding Steps to Task Sequence

Now that we have the executable, configuration and launch scripts we are ready to build the steps in the task sequence. To make it easier to execute I make a group that I can copy paste where I need it. This could possibly be made a child task sequence as well.

First, connect to the network share using the Connect to Network Folder task sequence step. Give it the user account to map the drive as and define a drive letter. I used Z:\ for simplicity.

Second, create a Run PowerShell Script step. Set the package source for the script to the package of files created earlier and put the script name in the Script name field.

Lastly, disconnect the network drive. This will make sure if you have other steps leveraging the same network resource with different credentials don’t collide with this step.

You’ll need to setup this configuration for both the WinPE script and the Windows script. Make sure to put this block of steps after every reboot or where a reboot occurs naturally in the task sequence and it will pull up VNC and run it from the package. After a reboot it should all be gone.

Cleanup

At the end of the task sequence, make sure you’re removing the firewall rule we created in the script using the following Run Command Line step in the task sequence. I just put the code directly in the command line section of the step.

netsh advfirewall firewall delete rule name="VNC OSD 5900"

You may also want to make sure you have a post action to reboot the computer. This can be done by setting up a Set Task Sequence Variable step and setting the SMSTSPostaction variable to be:

cmd /c shutdown /r /t 300 /f

Building an OSD Front End using C#: Part 4 – Adding Controls and Going Deeper

Up until now we’ve been very focused on getting everything ready. Now we’re ready to actually start building out our form controls and some of the logic behind them.

Let’s get our MainWindow.Xaml setup to start adding tabs, for this we’ll need to add some basic framework controls where we can build everything inside (I’ve cut all the extra stuff out of the Controls:MetroWindow tag):

<Controls:MetroWindow&gt;
    <Grid&gt;
        <TabControl x:Name="TabControlMainWindow" Margin="0,0,0,0" TabStripPlacement="Left" FontFamily="Arial" FontSize="14" FontWeight="Bold" RenderTranformOrigin="0.5,0.5"&gt;

        </TabControl&gt;
    </Grid&gt;
</Controls:MetroWindow&gt;

Everything we write for the main form from here on out will be within the TabControl. Let’s go through a little of what we have here.

The first new control we’ve added is a <Grid> which is needed as the base. We don’t need to add anything to it since it will just be the primary container on our form.

Next we have the main piece of our form which is a <TabControl> control. We need to define several properties of the TabControl to make sure that it functions properly on our form:

  1. x:Name – Name that the control will be called including in code later on.
  2. Margin – We want the TabControl going all the way out to the edge of the form so we make this 0 on all sides.
  3. TabStripPlacement – Moves the tabs from the top of the form to the left side as a vertical list.
  4. FontFamily – This just changes the font used on the tab names.
  5. FontSize – Change the font size for the tab names.
  6. FontWeight – Make the text bold for the tab names.
  7. RenderTransformOrigin – Determines where the center point of the control.

The next thing we’re going to want to add is our first <TabItem> control. Here’s the code for the computer name tab and below the code we’ll discuss what we’re doing.

<TabItem x:Name="tabComputerName" Header="Computer Name"&gt;
    <Grid x:Name="gridComputerName" Margin="0,0,0,0"&gt;
        <Grid.RowDefinitions&gt;
            <RowDefinition Height="*" /&gt;
            <RowDefinition Height="Auto" /&gt;
        </Grid.RowDefinitions&gt;
        <Button Grid.Row="1" x:Name="buttonComputerNameNext" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="5,5,5,5" Content="Next &gt;&gt;" Width="125" Click="NextButtonHandler" /&gt;
    </Grid&gt;
</TabItem&gt;

Inside a <TabControl> you have <TabItem>‘s. Think of a tab item as just a container for all the controls on a given tab. I’ve found the best way for me when building a tab in the front end is to put down a <Grid> control like we had before but this time to define two rows. The first row will expand to fill as much space as it can. The second row will grow to the height it requires but no more.

Now, you’ll see I’ve added a button called buttonComputerNameNext. You’ll also notice it’s in the Grid.Row 1. This is the second row (numbering starts from 0) and we’re putting it in the bottom right hand corner. This is the next button. I’ve also added a click event handler. Below is the code you’ll want to add to the MainWindow.xaml.cs file.

private void NextButtonHandler( object sender, RoutedEventArgs e ) {
    TabControlMainWindow.SelectedIndex++;
    ( TabControlMainWindow.Items[tabControlMainWindow.SelectedIndex] as TabItem ).IsEnabled = true;
}

This simple button handler will increase the SelectedIndex property of the TabControlMainWindow. The selected index is the index of the currently selected tab. This code allows all our next buttons to share the same event handler and will also allow us to enable or disable tabs without needing special code to handle those cases. We can also re-order tabs through the designer without worrying about updating code to change tab orders on the next buttons either.

It also then enables the control which is disabled by default so it cannot be selected. To do that we need to get the item and the selected index and cast it to be a TabItem so we can use it’s properties.

Building an OSD Front End using C#: Part 3 – Styling Our WPF Form

All we have so far is a blank Xaml form without much content. Let’s get some basic styles applied to the form and make it look a little more respectable. I have created a custom accent theme for UMN WPF forms using MahApps.Metro. The code for this is below, if you’d like to use it feel free to download and add it to your project as a Xaml file.

There are some good guides on the web page for MahApps.Metro around how to build a theme file like I’ve included. You’ll notice I basically took their theme template and adapted it to our color scheme.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                    xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
                    mc:Ignorable="options"&gt;
    <Color x:Key="HighlightColor"&gt;#FFFFCC33</Color&gt;
    <Color x:Key="AccentBaseColor"&gt;#FF7A0019</Color&gt;
    <!-- 80% --&gt;
    <Color x:Key="AccentColor"&gt;#FF7A0019</Color&gt;
    <!-- 60% --&gt;
    <Color x:Key="AccentColor2"&gt;#997A0019</Color&gt;
    <!-- 40% --&gt;
    <Color x:Key="AccentColor3"&gt;#667A0019</Color&gt;
    <!-- 20% --&gt;
    <Color x:Key="AccentColor4"&gt;#337A0019</Color&gt;

    <Color x:Key="ValidItem"&gt;#FF27BC1A</Color&gt;
    <Color x:Key="InvalidItem"&gt;#FFBC1A1A</Color&gt;

    <!-- re-set brushes too --&gt;
    <SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentBaseColorBrush" Color="{StaticResource AccentBaseColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentColorBrush" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentColorBrush2" Color="{StaticResource AccentColor2}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentColorBrush3" Color="{StaticResource AccentColor3}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentColorBrush4" Color="{StaticResource AccentColor4}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="ValidItemBrush" Color="{StaticResource ValidItem}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="InvalidItemBrush" Color="{StaticResource InvalidItem}" options:Freeze="True" /&gt;

    <SolidColorBrush x:Key="WindowTitleColorBrush" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;

    <LinearGradientBrush x:Key="ProgressBrush" StartPoint="1.002,0.5" EndPoint="0.001,0.5" options:Freeze="True"&gt;
        <GradientStop Offset="0" Color="{StaticResource HighlightColor}" /&gt;
        <GradientStop Offset="1" Color="{StaticResource AccentColor3}" /&gt;
    </LinearGradientBrush&gt;

    <SolidColorBrush x:Key="CheckmarkFill" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="RightArrowFill" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;

    <Color x:Key="IdealForegroundColor"&gt;White</Color&gt;
    <SolidColorBrush x:Key="IdealForegroundColorBrush" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="IdealForegroundDisabledBrush" Opacity="0.4" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="AccentSelectedColorBrush" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;

    <!--  DataGrid brushes  --&gt;
    <SolidColorBrush x:Key="MetroDataGrid.HighlightBrush" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MetroDataGrid.HighlightTextBrush" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MetroDataGrid.MouseOverHighlightBrush" Color="{StaticResource AccentColor3}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MetroDataGrid.FocusBorderBrush" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MetroDataGrid.InactiveSelectionHighlightBrush" Color="{StaticResource AccentColor2}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MetroDataGrid.InactiveSelectionHighlightTextBrush" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;

    <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchBrush.Win10" Color="{StaticResource AccentColor}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchMouseOverBrush.Win10" Color="{StaticResource AccentColor2}" options:Freeze="True" /&gt;
    <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.ThumbIndicatorCheckedBrush.Win10" Color="{StaticResource IdealForegroundColor}" options:Freeze="True" /&gt;
</ResourceDictionary&gt;

Now we just need to setup our App.Xaml file to include a <Application.Resources> section as seen below:

<Application.Resources&gt;
    <ResourceDictionary&gt;
        <ResourceDictionary.MergedDictionaries&gt;
            <!-- MahApps.Metro resource dictionaries. --&gt;
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" /&gt;
            <ResourceDictionary Source="pack://application:,,,/UMN-OSDFrontend;component/Resources/Icons.xaml" /&gt;
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" /&gt;
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colors.xaml" /&gt;
            <ResourceDictionary Source="pack://application:,,,/UMN-OSDFrontend;component/Styles/CustomAccentUMN.xaml" /&gt;
            <!-- Accent and AppTheme Settings --&gt;
            <!-- Un-comment the following line if you're not using the UMN theme --&gt;
            <!--<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Blue.xaml" /&gt;--&gt;
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseDark.xaml" /&gt;
        </ResourceDictionary.MergedDictionaries&gt;
    </ResourceDictionary&gt;
</Application.Resources&gt;

Now we have a themed window. While we don’t have anything on it yet we’re now fully ready to build on top of this foundation. I encourage you to play around a little with the theme’s and colors while you’re building your front end to get a better idea of what looks good and what works for your purposes.

Next post we’re going to dive deep into the meat of building the form and talk about the first two tabs for computer naming and pre-flight checks.

Building an OSD Front End using C#: Part 1 – Getting Started

This is a blog series walking through how to build your own OSD Front End using C#.  It walks through everything I went through creating the UMN-OSDFrontEnd project which is now open source and available on GitHub.

I’m using Visual Studio.  If you are starting out it’s the easiest (in my opinion) way to get up and running with C#.  There are other ways to build C# projects but I’m going to assume for the purposes of this guide that you have access to Visual Studio.

You’ll want to create a new WPF App (.NET Framework) project.  A key thing is to ensure you change your project’s target framework from the latest version to .NET Framework 4.5.  This will ensure you’re building to be compatible with WinPE.

OSDFrontEndProjectCreation

Next we will need to get all the NuGet packages we’re going to use throughout the project by going to Project -> Manage NuGet Packages then search for these under the browse section:

  • ControlzEx
    • The only reason this needs to be installed is that it’s a requirement of the MahApps.Metro package.
  • MahApps.Metro
    • This gives you access to a metro styled app using WPF.  It makes it way easier to customize the application and make it look good.
  • MahApps.Metro.Resources
    • Requires MahApps.Metro
    • This contains a bunch of useful resources such as icons/images.
  • Mono.Options
    • Gives us an easy way to parse parameters passed to the executable.
  • Newtonsoft.Json
    • Makes it easier to handle json data.