Getting Data from the Intune Data Warehouse with PowerShell

The Intune Data Warehouse is a great addition to the Microsoft Intune service allowing visibility of historical data for reporting, data and trend analysis for your Microsoft MDM environment. It comes with an OData feed that allows you to connect to the data with PowerBI, Microsoft’s reporting and data visualization service.

The Data Warehouse RESTful API (currently in Beta) can be used to get data from the warehouse using a REST client. I decided to explore how to do this with PowerShell so I can run some ad-hoc queries and analyse trends in the data.

To get data from the Intune Data Warehouse we need to do three main things:

  1. Create a native App in Azure and give it access to the Intune Data Warehouse
  2. Authenticate with Azure using OAuth 2.0 and get an access token
  3. Invoke the RESTful web service using http

Create a Native App in Azure

In your Azure portal, go to Azure Active Directory > App registrations. Click New application registration.

Give it a name, make sure it is a Native app (do not use Web app / API) and use the redirect URI https://login.live.com/oauth20_desktop.srf .

appreg

Click Create.

Once created, make a note of the Application ID as we will need this later.

Now, in the App in the Settings blade, click Required permissions > Add > Select an API and select Microsoft Intune API.

In the Add API access blade click Select permissions and grant the delegated permission Get data warehouse information from Microsoft Intune.

perms

Save your changes.

Authenticate with Azure

To authenticate with Azure I wrote the following function in PowerShell:

What the code does…

To authenticate with Azure we need to use the Active Directory Authentication Library from Microsoft. This library is actually included in resources like the ConfigMgr client and the AzureRM PowerShell module, but these do not have the latest versions of the library and the methods it contains have changed over time. So I prefer to use the latest version of the library (3.17.2 at the time of writing) which is available as a NuGet package.

The function will download NuGet and use it to download the latest version of the ADAL library to your user profile area. Once we have the library we will add it in PowerShell. Then we will acquire an access token.

The access token expires after an hour so once created, the token will probably be good for your current session. If it expires, simply run the function again. The access token will be saved to a variable in the script scope and will be used by the function that queries the data warehouse.

How to use it…

Make sure the following parameters contain the required values. I recommend that you store those in the function directly so you don’t have to add them every time.

Simply run the function:


New-IntuneDataWarehouseAccessToken

If you have not previously authenticated with Azure in your current session you will be prompted to sign in to your Azure account:

auth

The first time you use the native app you created, you will also be prompted for permission:

Prompt

Invoke the Web Service using the OData feed

Now that we have an access token, we can invoke the web service using http. I wrote the following PowerShell function to do that:

What the code does…

The function uses the custom OData feed URL for your tenant, creates an http client to invoke the web service and gets data for the data entity (ie collection, or table) that you specify. The results are then returned to the PowerShell console.

You can read more about the data model for the warehouse and get a reference for the various data entities and their relationships on the Microsoft Docs site.

How to use it…

Make sure the following parameter is set in the function:

  • WarehouseUrl

This Url is the custom feed URL for your tenant and you can find it from the Intune blade in Azure. On the Overview blade, on the right you find Other tasks and underneath Set up Intune Data Warehouse.

url

To list the data entities that are available to query use the ListDataEntities switch:


Get-IntuneDataWarehouseData -ListDataEntities

entities

To return the data from a specific data entity, use the DataEntity parameter. This example returns data from the devices table.


Get-IntuneDataWarehouseData -DataEntity devices

devices

Working with the Data

The API supports a few query parameters in the OData protocol v4.0, so rather than returning all the results in the data entity, you can narrow them down. However, I have noticed that the query parameters do not always work as expected when they are combined, at least in the beta version of the API.

For example, you can use the Filter parameter to return only matching results. This query finds a specific device:


Get-IntuneDataWarehouseData -DataEntity devices -Filter "deviceName eq 'SW-IT-LT-AZURE1'"

You can select only specific properties to be returned in the results using the Select parameter:


Get-IntuneDataWarehouseData -DataEntity devices -Select 'deviceName,serialNumber,lastContact'

Select the top 5 results:


Get-IntuneDataWarehouseData -DataEntity devices -Top 5

Skip 10 results and return the rest:


Get-IntuneDataWarehouseData -DataEntity devices -Skip 10

Sort results by a particular property:


Get-IntuneDataWarehouseData -DataEntity devices -OrderBy 'lastContact'

You can learn more about how to use query parameters in the OData protocol from the Microsoft Developer site for the Graph API.

These query parameters are certainly helpful, but for relational queries where you want to reference more than one table or entity, the API comes short and more complex Powershell code is required.

For example, here I am searching for the ethernet MAC address from the most recent hardware inventory for a device, and even with query parameters applied I still need to do some filtering and sorting in Powershell. Remember that the data warehouse only contains snapshots of historic data, so for current data you can use the Graph API instead, but this is just for an example.


$DeviceName = "SW-IT-LT-AZURE1"
Get-IntuneDataWarehouseData -DataEntity 'mdmDeviceInventoryHistories' -Select 'deviceKey,datekey,ethernetMac' |
    Where {$_.devicekey -eq ((Get-IntuneDataWarehouseData -DataEntity 'devices' -Filter "deviceName eq '$DeviceName'").devicekey)} |
    Sort datekey -Descending |
    Select -First 1 |
    Select -ExpandProperty ethernetMac 

This code takes a while to run however because it’s querying the data warehouse more than once to get the data. Another way to do this would be to first load the required device entities into memory, then I can query them more quickly and run other queries from this cached data.

The following code will load just those two entities into a hash table:


$DataEntities = "mdmDeviceInventoryHistories","devices"
$DataHash = @{}
foreach ($DataEntity in $DataEntities)
{
    Write-host "Loading $DataEntity"
    [void]$DataHash.Add($DataEntity,(Get-IntuneDataWarehouseData -DataEntity $DataEntity))
}

Then I can run the following code to get the ethernet MAC address and it returns the result instantly:


$DeviceName = "SW-IT-LT-AZURE1"
$DataHash['mdmDeviceInventoryHistories'] |
    Where {$_.devicekey -eq (($DataHash['devices'] | where {$_.deviceName -eq $DeviceName}).devicekey)} |
    Sort datekey -Descending |
    Select -First 1 |
    Select -ExpandProperty ethernetMac 

You could load the entire data warehouse into memory using the following code, then you can simply work with the resultant hashtable:


$DataEntities = Get-IntuneDataWarehouseData -ListDataEntities
$DataHash = @{}
foreach ($DataEntity in $DataEntities)
{
    Write-host "Loading $DataEntity"
    [void]$DataHash.Add($DataEntity,(Get-IntuneDataWarehouseData -DataEntity $DataEntity))
}

The benefit of a data warehouse of course is that you can review snapshots of data over a period of time and analyse the data for trends or identify when things changed. The following example is using the data hashtable and is reporting the device inventory history of a specific device over time. The Intune data warehouse keeps up to 90 days of historic data. In particular, I want to see how much the free space on disk is changing over time.


$DeviceName = "SW-IT-LT-158"
$Results = $DataHash['mdmDeviceInventoryHistories'] |
    Where {$_.devicekey -eq (($DataHash['devices'] | where {$_.deviceName -eq $DeviceName}).devicekey)} |
    Sort datekey -Descending |
    Select dateKey, deviceName,
        softwareVersion,
        @{e={$([math]::Round(($_.storageFree / 1GB),2))};l="storageFree (GB)"},
        @{e={$([math]::Round(($_.storageTotal / 1GB),2))};l="storageTotal (GB)"}
foreach ($Result in $results){
    $Result | Add-Member -MemberType NoteProperty -Name date -Value (($DataHash['dates'] | Where {$_.dateKey -eq $Result.dateKey}).fullDate | Get-Date -Format "dd MMM yyyy")
}
$Results | Select deviceName,date,softwareVersion,'storageFree (GB)','storageTotal (GB)' | ft

You can readily see that it’s necessary to manipulate the data quite a bit to get the results I want to see, for example in order to do something equivalent to a ‘join’ in SQL I am using Where-Object, and in order to add the data from another table to my results I am using Add-Member. I am also converting the values of the storage data into GB and formatting the date using the UK short date code.

trend1

The results are returned in an array object, but for data like this it can also be useful to use a datatable as you would for SQL data for example.  Then you can add / remove columns, change column order, set the datatype for a column, change headers etc.

This code does exactly the same thing as the last example, but using a datatable for the results.


$DeviceName = "sw-it-lt-158"
$Datatable = New-Object System.Data.DataTable
[void]$Datatable.Columns.AddRange(@('deviceName','date','softwareVersion','storageFree (GB)','storageTotal (GB)'))
$Results = $DataHash['mdmDeviceInventoryHistories'] |
    Where {$_.devicekey -eq (($DataHash['devices'] | where {$_.deviceName -eq $DeviceName}).devicekey)} |
    Sort datekey -Descending |
    Select dateKey,
        deviceName,
        softwareVersion,
        @{e={$([math]::Round(($_.storageFree / 1GB),2))};l="storageFree (GB)"},
        @{e={$([math]::Round(($_.storageTotal / 1GB),2))};l="storageTotal (GB)"}
foreach ($Result in $results){
    [datetime]$Date = ($DataHash['dates'] | Where {$_.dateKey -eq $Result.dateKey}).fullDate
    [void]$DataTable.Rows.Add($Result.deviceName,$Date.ToShortDateString(),$Result.softwareVersion,$Result.'storageFree (GB)', $Result.'storageTotal (GB)')
}
$Datatable | ft

Reviewing the results I can see that the available disk space is decreasing slightly over time. It would be nice to see that data represented graphically, and of course this is where the integration with PowerBI will shine, but we can also generate graphical charts in Powershell, so let’s give that a go.

Here is a function I wrote that will generate a spline chart using the .Net chart controls and display it in a WPF window. It takes a single series of data and you need to provide a title, a data object as an array, the X and Y axis names (which must match the header names in the data object).

To generate the chart, I will use the results from my previous example (not the datatable but the array), sort them by date, select the last 20 data snapshots, select the X and Y axis data into a new object and provide this to the chart function:


$Data = $Results | Sort dateKey | Select date,'storageFree (GB)' | Select -Last 20
New-SingleSeriesSplineChart -Title "Trend of Available Free Storage on SW-IT-LT-158" -Data $Data -AxisX "date" -AxisY "storageFree (GB)"

Now I have a nice graphical view 🙂

SplineChart

I have focused just on devices in this blog, but there is lots of data available in the Intune Data Warehouse including users, policies, compliance, configurations, MAM data etc, all of which can provide valuable insights into your MDM estate and whether you use PowerShell, PowerBI, Excel or whichever tool, the ability to view and analyse historic data is a welcome improvement to the ever-evolving Intune service.

Improving the User Experience in a ConfigMgr OS Upgrade Task Sequence

Update 24th Nov 2017

  • Fixed the issue where the Upgrade Successful notification does not display for non-admin users. Thanks to a tip from Carl (see comments) I used a somewhat ancient mechanism called ActiveSetup that is still available in Windows 10.
  • The custom background displayed during the online phase of the upgrade now displays on all screens if multiple monitors are being used. Thanks to Ronni Pedersen for the kick 🙂
  • These changes have added a couple more scripts to the download, but the task sequence remains unchanged, so simply update your notifications package in ConfigMgr.

When upgrading to Windows 10 from a ‘down-level’ OS, or to a new version of Windows 10, using installation media, you get a nice UI that guides you through the installation process.

WindowsSetup2

Upgrading using an OS upgrade task sequence in ConfigMgr however, is a comparatively cold experience with no UI except for the TS Progress UI – assuming you enable that. For an IT admin of course, we don’t necessarily care about having a nice UI, we just care that it works and we have log files to check if it doesn’t. But for an end user that can be a different story. It may be a little disconcerting to some that their system is being upgraded yet the upgrade process is providing little feedback about what is happening. Once you get past the online phase of the upgrade however, the experience is more streamlined.

In an OS upgrade task sequence, Windows Setup will be running silently in SYSTEM context so it will not display anything to the logged-on user. Everything is handled by the task sequence. If the task sequence fails, the user might feel panicked and wonder if they have lost any of their data or applications. There is nothing to reassure them otherwise.

We may not be able to reproduce the nice Windows Installer UX, but we could at least add a few custom notifications at different points in the TS to provide some feedback to the end user and improve the overall experience from their perspective.

I experimented with this a bit using my New-WPFMessageBox PowerShell function and the following is what I came up with.

At the start of the upgrade task sequence, I like to check the currently-installed Windows version because – strange but true – the Windows Setup process will not prevent you from ‘upgrading’ to a version you are already running! How’s that for a time-waster?! Of course, you would try to avoid that with correct collection targeting in ConfigMgr, but just as an insurance I check that the system is not already running that version, and if it is, display the following notification to the user, then exit the TS.

AlreadyUpgraded

Next, during the online phase of the Upgrade Operating System step, I display a custom background. This is just to discourage the user from working or rebooting the computer and provides some extra assurance that something is actually happening. This is actually a WPF window that fills the screen, not a desktop wallpaper.

OSUpgrade

I also run the compatibility scan first and if that fails, I notify the user with the error code and description that they can contact IT support with:

CompatScanFail

The same if the upgrade fails, or if a rollback is performed, although no descriptions here as there are many possible result codes.

OSUpgradeFail

OSUpgradeFailRollback

Finally, when the OS upgrade successfully completes, the first user who logs in will see the following notification giving them some hyperlinks to what is new in the upgraded OS:

UpgradeComplete

Using my New-WPFMessageBox function you can customise these notifications as you please.

To make it simple, I have included here an export of an OS upgrade task sequence that you can import into your environment as a basis or an example of how to add such notifications. Here’s a screenshot:

TaskSequence

I’ve also made available all the PowerShell scripts I used as a download. Simply create a standard package in ConfigMgr containing the all the scripts in the same directory and distribute the content (no program required). Update the imported task sequence to reference this package for each of the Run PowerShell script steps, and also reference your OS Upgrade package in the relevant steps.

Some important things to note:

  • The notifications display in the context and session of the logged-on user. This is accomplished by calling the notification scripts via another script – Invoke-PSScriptAsUser.ps1 – that creates a PowerShell process in the user’s context.
  • Where a notification is displayed, I also first hide the TS Progress UI using the TSDisableProgressUI variable, which is available since ConfigMgr Current Branch 1706. This is because the notification will display behind the TS Progress UI, although if there are no further steps to complete after the notification is displayed it doesn’t matter too much because the TS Progress UI will not display for long anyway. The task sequence will not wait for the user to respond to the notification before it continues processing any remaining steps.
  • Where the compatibility scan or OS upgrade fails, the step is set to continue on error so that we can handle the error ourselves. After displaying the error notification, we manually fail the TS using the _SMSTSOSUpgradeActionReturnCode TS variable value as the error code.
  • Where the compatibility scan or OS upgrade fails, we write out the return code to a file so that the custom notification, which runs in the user context, can read in the value. This is because the task sequence variables are only available to query in the SYSTEM context – the user context cannot read them.
  • In handling a failure I set the SMSTSErrorDialogTimeout TS variable to 1 second so that the TS fails quickly and the user is left with our custom error notification instead of the default TS one.
  • The final notification that the upgrade was successful displays for the first user that logs in after the TS has completed. This is because the OS Upgrade TS simply ends at the Windows lock screen where we cannot display anything. Before the TS ends, we copy the notification script to a temp location and set the RunOnce registry key to call it.
  • Pay attention to the step conditions for the groups in the task sequence, as this controls the logical flow of the sequence.
  • Make sure to “Ignore dependency” when importing the task sequence

Pre-caching Content

Another important activity that should be done before making an OS Upgrade task sequence available is to pre-cache as much content as possible on the target systems. Unless the content is already in the ConfigMgr client cache when the TS runs, it’s gonna need to download that content which, for an OS Upgrade TS, is a sizeable amount of data and could add significant time to the execution of the task sequence making for a poorer experience for the end user.

Since ConfigMgr 1702, we have had the ability to pre-download content for a task sequence, and this was improved a bit in 1706, but in my own experience I have not found it to do quite what it says on the tin. Specifically, this line in the documentation – When the client receives the deployment policy, it will start to pre-cache the content. – appears not to be true (at the time of writing with 1706). Even when you have correctly set the OS Architecture and language on the OS Upgrade package, and set the required conditions on the Upgrade Operating System step, no content is actually cached on the client until the date the deployment becomes available. That is, you can target a system with a deployment that has an available date in the future, and theoretically it should start caching content as soon as a machine policy refresh occurs. But in practice, it does not cache any content until the available date of the deployment is reached, then shortly after it will start to download the content. If the user decides to upgrade as soon as the deployment becomes available, they will need to wait for the content to download first. If anyone has a different experience with this, please let me know!

Until that is fixed, we can still pre-cache most of the content by creating a hidden task sequence that uses the Download Package Content step. Make sure to use the Configuration Manager client cache as the location.

TaskSequence2

Check the option to Suppress task sequence notifications on the TS properties, and deploy the TS to the target systems before you deploy the OS Upgrade TS.

SuppressNotifications

Download

Download the PowerShell Scripts and exported Task Sequence here.

 

 

 

Add Custom Notifications to a ConfigMgr Task Sequence

One feature I would really like to see added to a Configuration Manager task sequence is the ability to natively provide notification messages to the logged-on user. Previously, to accomplish this, I have used simple pop-up notifications like the Wscript Shell Popup method in a PowerShell script, together with the handy ServiceUI utility in MDT to display the notification in the logged-on users’ session. This has worked well enough for simple messages, and has been useful in several scenarios. For example, see my blog post about prompting for input during a task sequence.

Recently I wrote a PowerShell function to display my own custom notifications using WPF, called New-WPFMessageBox. This allows for much greater customisation of the message box, including adding your own WPF content. So I decided to revisit displaying notifications during a task sequence using this new function instead. In this post I will show you how to add a “Restart Required” notification to run at the end of a task sequence. This can be used to advise the user that a restart needs to take place after the installation of some software for example, and give them the option to restart immediately, or restart later.

RestartRequired

Instead of using the ServiceUI utility – which works well, but it still runs in SYSTEM context even though it will allow you to display in the logged-on users’ session – I decided on a different method that allows you to truly run a process in the users’ context. Thanks to a tip from Roger Zander I found some C# sharp code by a guy named Justin Murray that can be used in PowerShell to make this possible.

Invoke-PSScriptAsUser

Create a new PowerShell script containing the following code. In the $Source variable, copy and paste the C# code from https://github.com/murrayju/CreateProcessAsUser/blob/master/ProcessExtensions/ProcessExtensions.cs. I have renamed the namespace (line 4 in the C# code) from namespace murrayju.ProcessExtensions to namespace Runasuser.


Param($File)

$Source = @"

"@

# Load the custom type
Add-Type -ReferencedAssemblies 'System', 'System.Runtime.InteropServices' -TypeDefinition $Source -Language CSharp -ErrorAction Stop

# Run PS as user to display the message box
[Runasuser.ProcessExtensions]::StartProcessAsCurrentUser("$env:windir\System32\WindowsPowerShell\v1.0\Powershell.exe"," -ExecutionPolicy Bypass -WindowStyle Hidden -File $PSScriptRoot\$File")

Save this script as Invoke-PSScriptAsUser.ps1

Display-RestartNotification

Create a new PowerShell script containing the following code. At the top paste in my New-WPFMessageBox function from https://gist.github.com/SMSAgentSoftware/0c0eee98a673b6ac34f5215ea6841beb. You can, of course, customise the notification as you wish.


# Paste here New-WPFMessageBox function from https://gist.github.com/SMSAgentSoftware/0c0eee98a673b6ac34f5215ea6841beb

$Params = @{
    Content = "You must restart your computer before using Software X."
    Title = "Computer Restart Required!"
    TitleFontSize = 20
    TitleFontWeight = "Bold"
    TitleBackground = "OrangeRed"
    ButtonType = "None"
    CustomButtons = "RESTART NOW","RESTART LATER"
    Sound = 'Windows Notify'
}

New-WPFMessageBox @Params
If ($WPFMessageBoxOutput -eq "RESTART NOW")
{
    Restart-Computer
}

The function saves the content of the button you click to the variable $WPFMessageBoxOutput, so you can use this to perform certain actions depending on which button the user clicks, in this case simply restarting the computer. This variable is only available in the script scope however.

Save this script as Display-RestartNotification.ps1.

Create a Package

Now create a standard package in ConfigMgr containing both of these scripts in the same directory, and distribute the content. No program is required for the package.

Configure Task Sequence

In your task sequence, add a Run Powershell Script step. Reference the package you created and enter the script name and parameters:

Script name: Invoke-PSScriptAsUser.ps1

Parameters: -File Display-RestartNotification.ps1

TS

When the task sequence executes, it will run the Invoke-PSScriptAsUser.ps1 in SYSTEM context, which will in turn run PowerShell in the logged-on users’ context and run the Display-RestartNotification.ps1 script, which displays the notification to the user.

The task sequence will not wait for the user to respond to the message; it will simply finish up in the background and the notification will remain on screen until the user responds to it.

If you enabled the option to Show task sequence progress then the notification will display behind the task sequence progress UI. Since this is the last step in the sequence it doesn’t matter, but if you have other steps running after the notification, you should hide the task sequence progress UI at that point. Since ConfigMgr 1706 we have the TSDisableProgressUI task sequence variable that can do that for us, so simply place a step before the notification step disabling the progress UI:

tsui

The ability to run a process in the user context during a task sequence is quite useful, not just for displaying notifications, but for running any code or process that must run in the user context, for example setting HKCU registry keys, or triggering a baseline evaluation that has user-based settings.

Friday fun: Create a WPF Clock Widget with PowerShell

The guys over at Arction have kindly made available a free Gauge control for WPF. I decided to download it and create a clock using PowerShell. The result is New-WPFClock.

Clock

To use the function, first download the free Gauge control. You’ll find the Arction.WPF.Gauges.dll in the Libs\Wpf folder. In the script on line 26, enter the location of this dll on your system.

Then simply call New-WPFClock.

The function has a couple of optional parameters:

  • Color – set the colour of the clock face
  • Height – set the height
  • Width – set the width
  • FontSize – set the size of the numbers
  • AlwaysOnTop – keep the clock on top of other windows

To close the clock, simply right-click it.

Enjoy!

Get ConfigMgr Client Versions with PowerShell

When upgrading your ConfigMgr site, or installing an update that creates a new ConfigMgr client package, it can be helpful to monitor the rollout of the new client version in your environment.

I put together this PowerShell function which uses my New-WPFMessageBox function to graphically display the count and percentage of client versions in the ConfigMgr site. The data comes from a SQL query, so you’ll need minimum db_datareader access to your ConfigMgr database with your logged-in account, as well as the New-WPFMessageBox function.

By default, it shows only active systems, but you can include inactive systems by checking the box.

img1

img2

A Customisable WPF MessageBox for PowerShell

Update 2017-08-25 | Changed the way the output from a button click is handled, saving it to a variable instead.

The humble VBScript message box has been around for a while now and is still a useful way of providing a simple pop-up notification in Windows. I’ve used it in my PowerShell scripts as well as my UI tools and even in my ConfigMgr task sequences for providing feedback to the end user.

WscriptMsgBox

Both WinForms and WPF also have a message box but they have similar style and functionality to the VBScript version.  In this age of modern design languages like Metro and Material Design however, this message box is starting to look a little dated. So I decided to create my own customizable message box in WPF as a PowerShell function that can be used in my scripts and UI tools.

Enter New-WPFMessageBox. My goal was to create a good-looking, fully-functional message box window that is highly customizable in the WPF format, and can even be used as a window host to display custom WPF content. The message box has a basic default design, but if you’re the creative type then you can customize away until you get an appearance you like.

Examples

Let’s have a look at some ways we can use this function.

To create a simple message box, all you need to do is pass a string to the Content parameter:


New-WPFMessageBox -Content "I'm a WPF Object!"

img2

The Content parameter is the only mandatory parameter, and can take either a simple text string or your own custom WPF content (which we’ll look at later).

To add a header, simply use the Title parameter:


New-WPFMessageBox -Content "I'm a WPF Object!" -Title "Custom Message Box"

img1

The title bar is a color block, so it can be used to create a nice contrast with the main content, for example. Use the TitleBackground parameter to change the color. This parameter is a dynamic parameter and will allow you to select from the list of available colors in the .Net palette.

img3

 

New-WPFMessageBox -Content "I'm a WPF Object!" -Title "Custom Message Box" -TitleBackground CornflowerBlue

img4

You can change the fonts, font weights and font sizes, as well as the background and foreground colors to create some nice combinations. The FontFamily parameter is also dynamic and allows you to select from fonts available in .Net.

The Content… parameters allow you to change the main content section, and the Title… parameters allow you to change the title.

When using several parameters, it can be easier to ‘splat’ them like so:


$Params = @{
    Content = "I'm a WPF Object!"
    Title = "Custom Message Box"
    TitleBackground = "CornflowerBlue"
    TitleFontSize = 28
    TitleFontWeight = "Bold"
    TitleTextForeground = "SpringGreen"
    ContentFontSize = 18
    ContentFontWeight = "Medium"
    ContentTextForeground = "SpringGreen"
}

New-WPFMessageBox @Params

img5

Use ContentBackground to change the main content background:

img6

You can change the button color using ButtonTextForeground:

img7

The default button is the OK button, but you can also use other buttons or button combinations such as Yes/No, Retry/Cancel, Abort/Retry/Ignore etc. Simply select an option using the ButtonType parameter.


$Params = @{
    Content = "The file 'Important Document.docx' could not be deleted."
    Title = "File Deletion Error"
    TitleBackground = "LightGray"
    TitleFontSize = 28
    TitleFontWeight = "Bold"
    TitleTextForeground = "Red"
    ContentFontSize = 18
    ContentFontWeight = "Medium"
}

New-WPFMessageBox @Params -ButtonType Abort-Retry-Ignore

img8

When a button is clicked, the text of the button is saved to a variable named WPFMessageBoxOutput so you can handle this in your code and perform different actions depending on what was clicked, eg:


New-WPFMessageBox @Params -ButtonType Abort-Retry-Ignore
If ($WPFMessageBoxOutput -eq "Abort")
{
    Exit
}
ElseIf ($WPFMessageBoxOutput -eq "Retry")
{

}
Else
{

}

You can also add your own custom buttons. Set the ButtonType to None, and pass the text for your buttons to the CustomButtons parameter:


$Params = @{
    Content = "The purchase order has been created.  Do you want to go ahead?"
    Title = "Purchase Order"
    TitleFontSize = 20
    TitleBackground = 'Green'
    TitleTextForeground = 'White'
    ButtonType = 'None'
    CustomButtons = "Yes, go ahead!","No, don't do it!", "Save it for later"
}
New-WPFMessageBox @Params

img9

You can create Information, Warning or Error message boxes using your own style. If you define the parameters once in your script, then you can simply call them everytime you want to display that message type.

A simple informational message:


$InfoParams = @{
    Title = "INFORMATION"
    TitleFontSize = 20
    TitleBackground = 'LightSkyBlue'
    TitleTextForeground = 'Black'
}
New-WPFMessageBox @InfoParams -Content "The server has been rebooted at 08:35 on 16th January 2017"

img10

A warning message:


$WarningParams = @{
    Title = "WARNING"
    TitleFontSize = 20
    TitleBackground = 'Orange'
    TitleTextForeground = 'Black'
}
New-WPFMessageBox @WarningParams -Content "The file could not be opened."

img11

An error message:


$ErrorMsgParams = @{
    Title = "ERROR!"
    TitleBackground = "Red"
    TitleTextForeground = "WhiteSmoke"
    TitleFontWeight = "UltraBold"
    TitleFontSize = 20
    Sound = 'Windows Exclamation'
}
New-WPFMessageBox @ErrorMsgParams -Content "There was a problem connecting to the Exchange Server.
Please try again later."

In this one I’ve added the Sound parameter. This is a dynamic parameter allowing you to select from available Windows sounds in your local media library. The sound will be played when the message box is displayed.

img12

Maybe you prefer a little more drama in your error message box. How about this:


$ErrorMsgParams = @{
    TitleBackground = "Red"
    TitleTextForeground = "Yellow"
    TitleFontWeight = "UltraBold"
    TitleFontSize = 28
    ContentBackground = 'Red'
    ContentFontSize = 18
    ContentTextForeground = 'White'
    ButtonTextForeground = 'White'
    Sound = 'Windows Hardware Fail'
}
$ComputerName = "DoesntExist"
Try
{
    New-PSSession -ComputerName $ComputerName -ErrorAction Stop
}
Catch
{
    New-WPFMessageBox @ErrorMsgParams -Content "$_" -Title "PSSession Error!"
}

In this code, I am using a try / catch block to catch an error, then using the message box to display it.

img13

Or we can create a Windows 10 BSOD-style error:


$Params = @{
    FontFamily = 'Verdana'
    Title = ":("
    TitleFontSize = 80
    TitleTextForeground = 'White'
    TitleBackground = 'SteelBlue'
    ButtonType = 'OK'
    ContentFontSize = 16
    ContentTextForeground = 'White'
    ContentBackground = 'SteelBlue'
    ButtonTextForeground = 'White'
    BorderThickness = 0
}
New-WPFMessageBox @Params -Content "The script ran into a problem that it couldn't handle, and now it needs to exit.

0x80050002"

img14

If you prefer square corners and no window shadow effect for a more basic look, try this. Here I have set the CornerRadius, ShadowDepth and BlurRadius to 0, as well as setting the BorderThickness and BorderBrush parameters to create a simple border.


$Params = @{
    Title = "QUESTION"
    TitleBackground ='Navy'
    TitleTextForeground = 'White'
    ButtonType = 'Yes-No'
    CornerRadius = 0
    ShadowDepth = 0
    BlurRadius = 0
    BorderThickness = 1
    BorderBrush = 'Navy'

}
New-WPFMessageBox @Params -Content "The server is not online. Do you wish to proceed?"

img15

You can create some interesting effects with those parameters, such as a more circular window:


$Params = @{
    Title = "QUESTION"
    TitleBackground ='Navy'
    TitleTextForeground = 'White'
    ButtonType = 'Yes-No'
    CornerRadius = 80
    ShadowDepth = 5
    BlurRadius = 5
    BorderThickness = 1
    BorderBrush = 'Navy'

}
New-WPFMessageBox @Params -Content "The server is not online.
Do you wish to proceed?"

img16

Timeout is a useful parameter which allows you to automatically close the message box after a number of seconds. You might want to use this in a script where you choose to continue if no response was received after a period of time. You can also use this to simply display a message without any buttons – just for information, and no response is required.


$Content = "Your mission - which we have chosen to accept for you - is to eliminate Windows XP in the enterprise.
Good luck!

This message will self-destruct in 30 seconds"

$Params = @{
    Content = $Content
    Title = "MISSION:POSSIBLE"
    TitleFontSize = 30
    TitleTextForeground = 'Red'
    TitleFontWeight = 'Bold'
    TitleBackground = 'Silver'
    FontFamily = 'Lucida Console'
    ButtonType = 'None'
    Timeout = 30
}

New-WPFMessageBox @Params

img30

I’ve also added a couple of parameters that allow you to assign code to the Loaded or Closed events on the message box window. This means you can run code when the message box opens, or after it has closed. Simply pass a code block to the OnLoaded or OnClosed parameters. Bear in mind that any code assigned to the Loaded event should not be long running as it will block the thread until it has completed, and the message box will only display afterwards.

In this example, I have used an async method from the SpeechSynthesizer class so that the text will be spoken at the same time the window opens because it is not running in the same thread.


$Content = "We could not access the server. Do you wish to continue?"

$Code = {
    Add-Type -AssemblyName System.speech
    (New-Object System.Speech.Synthesis.SpeechSynthesizer).SpeakAsync($Content)
    }

$Params = @{
    Content = "$Content"
    Title = "ERROR"
    ContentBackground = "WhiteSmoke"
    FontFamily = "Tahoma"
    TitleFontWeight = "Heavy"
    TitleBackground = "Red"
    TitleTextForeground = "YellowGreen"
    Sound = 'Windows Message Nudge'
    ButtonType = 'Cancel-TryAgain-Continue'
    OnLoaded = $Code
}

New-WPFMessageBox @Params

img31

Adding Custom WPF Content

As well as a simple text string, the Content parameter can accept a WPF UI element. When you pass a text string, the function simply creates a textblock element and adds the text. But if you know a little about creating WPF objects in PowerShell, you can create your own elements and add them as content to the message box. This opens up a lot of possibilities and transforms the message box into a kind of easy-to-create window host for any WPF content.

For example, instead of some text, how about displaying a picture instead?


$Source = "C:\Users\tjones\Pictures\minion.jpg"
$Image = New-Object System.Windows.Controls.Image
$Image.Source = $Source
$Image.Height = [System.Drawing.Image]::FromFile($Source).Height / 2
$Image.Width = [System.Drawing.Image]::FromFile($Source).Width / 2

New-WPFMessageBox -Content $Image -Title "Minions Rock!" -TitleBackground LightSeaGreen -TitleTextForeground Black

To set the height and width I have simply halved the dimensions of the source image file.

img17

Maybe you want to add a text description of the picture too. In that case, create a textblock and add both the image and textblock to a layout control like a stackpanel, and pass the stackpanel as content.


$Source = "C:\Users\tjones\Pictures\minion.jpg"
$Image = New-Object System.Windows.Controls.Image
$Image.Source = $Source
$Image.Height = [System.Drawing.Image]::FromFile($Source).Height / 2
$Image.Width = [System.Drawing.Image]::FromFile($Source).Width / 2

$TextBlock = New-Object System.Windows.Controls.TextBlock
$TextBlock.Text = "My friend Robert"
$TextBlock.FontSize = "28"
$TextBlock.HorizontalAlignment = "Center"

$StackPanel = New-Object System.Windows.Controls.StackPanel
$StackPanel.AddChild($Image)
$StackPanel.AddChild($TextBlock)

New-WPFMessageBox -Content $StackPanel -Title "Minions Rock!" -TitleBackground LightSeaGreen -TitleTextForeground Black -ContentBackground LightSeaGreen

img18

How about playing a video?


$MediaPlayer = New-Object System.Windows.Controls.MediaElement
$MediaPlayer.Height = "360"
$MediaPlayer.Width = "640"
$MediaPlayer.Source = "http://video.ch9.ms/ch9/5e56/5148ed00-c3cc-4d1b-a0e0-3f9cbfbb5e56/EmpoerBusinesstodoGreatThings.mp4"

New-WPFMessageBox -Content $MediaPlayer -Title "Windows 10 In The Enterprise"

$MediaPlayer.LoadedBehavior = "Manual"
$MediaPlayer.Stop()

img19

We could also add some buttons to control playback. I’ve added the buttons to a dockpanel, added both the video player and the dockpanel to a stackpanel, as use the stackpanel as content.


# Create a Media Element
$MediaPlayer = New-Object System.Windows.Controls.MediaElement
$MediaPlayer.Height = "360"
$MediaPlayer.Width = "640"
$MediaPlayer.LoadedBehavior = "Manual"
$MediaPlayer.Source = "http://video.ch9.ms/ch9/5e56/5148ed00-c3cc-4d1b-a0e0-3f9cbfbb5e56/EmpoerBusinesstodoGreatThings.mp4"

# Add a start button
$StartButton = New-Object System.Windows.Controls.Button
$StartButton.Content = "Start"
$StartButton.FontSize = 22
$StartButton.Width = "NaN"
$StartButton.Height = "NaN"
$StartButton.VerticalContentAlignment = "Center"
$StartButton.HorizontalContentAlignment = "Center"
$StartButton.HorizontalAlignment = "Center"
$StartButton.VerticalAlignment = "Center"
$StartButton.Background = "Transparent"
$StartButton.Margin = "0,5,15,0"
$StartButton.Padding = 10
$StartButton.Cursor = "Hand"
$StartButton.Add_Click({
    $MediaPlayer.Play()
})

# Add a stop button
$StopButton = New-Object System.Windows.Controls.Button
$StopButton.Content = "Stop"
$StopButton.FontSize = 22
$StopButton.Width = "NaN"
$StopButton.Height = "NaN"
$StopButton.VerticalContentAlignment = "Center"
$StopButton.HorizontalContentAlignment = "Center"
$StopButton.HorizontalAlignment = "Center"
$StopButton.VerticalAlignment = "Center"
$StopButton.Background = "Transparent"
$StopButton.Margin = "15,5,0,0"
$StopButton.Padding = 10
$StopButton.Cursor = "Hand"
$StopButton.Add_Click({
    $MediaPlayer.Stop()
})

# Add a pause button
$PauseButton = New-Object System.Windows.Controls.Button
$PauseButton.Content = "Pause"
$PauseButton.FontSize = 22
$PauseButton.Width = "NaN"
$PauseButton.Height = "NaN"
$PauseButton.VerticalContentAlignment = "Center"
$PauseButton.HorizontalContentAlignment = "Center"
$PauseButton.HorizontalAlignment = "Center"
$PauseButton.VerticalAlignment = "Center"
$PauseButton.Background = "Transparent"
$PauseButton.Margin = "15,5,15,0"
$PauseButton.Padding = 10
$PauseButton.Cursor = "Hand"
$PauseButton.Add_Click({
    $MediaPlayer.Pause()
})

# Add buttons to a dock panel
$DockPanel = New-object System.Windows.Controls.DockPanel
$DockPanel.LastChildFill = $False
$DockPanel.HorizontalAlignment = "Center"
$DockPanel.Width = "NaN"
$DockPanel.AddChild($StartButton)
$DockPanel.AddChild($PauseButton)
$DockPanel.AddChild($StopButton)

# Add dock panel and media player to a stackpanel
$StackPanel = New-object System.Windows.Controls.StackPanel
$StackPanel.AddChild($MediaPlayer)
$StackPanel.AddChild($DockPanel)

New-WPFMessageBox -Content $StackPanel -Title "Windows 10 In The Enterprise"

img20

Although I haven’t included any icons or images for message types like warning or error etc, you could still add your own using custom content:


$Image = New-Object System.Windows.Controls.Image
$Image.Source = "http://www.asistosgb.com/wp-content/uploads/2017/05/attent.png"
$Image.Height = 50
$Image.Width = 50
$Image.Margin = 5

$TextBlock = New-Object System.Windows.Controls.TextBlock
$TextBlock.Text = "The file could not be deleted at this time!"
$TextBlock.Padding = 10
$TextBlock.FontFamily = "Verdana"
$TextBlock.FontSize = 16
$TextBlock.VerticalAlignment = "Center"

$StackPanel = New-Object System.Windows.Controls.StackPanel
$StackPanel.Orientation = "Horizontal"
$StackPanel.AddChild($Image)
$StackPanel.AddChild($TextBlock)

New-WPFMessageBox -Content $StackPanel -Title "WARNING" -TitleFontSize 28 -TitleBackground Orange 

img21

How about including an expander for an error message box, with the error details available for viewing when expanded?


$ComputerName = "RandomPC"
Try
{
    New-PSSession -ComputerName $ComputerName -ErrorAction Stop
}
Catch
{

    # Create a text box
    $TextBox = New-Object System.Windows.Controls.TextBox
    $TextBox.Text = "Could not create a remote session to '$ComputerName'!"
    $TextBox.Padding = 5
    $TextBox.Margin = 5
    $TextBox.BorderThickness = 0
    $TextBox.FontSize = 16
    $TextBox.Width = "NaN"
    $TextBox.IsReadOnly = $True

    # Create an exander
    $Expander = New-Object System.Windows.Controls.Expander
    $Expander.Header = "Error details"
    $Expander.FontSize = 14
    $Expander.Padding = 5
    $Expander.Margin = "5,5,5,0"

    # Bind the expander width to the text box width, so the message box width does not change when expanded
    $Binding = New-Object System.Windows.Data.Binding
    $Binding.Path = [System.Windows.Controls.TextBox]::ActualWidthProperty
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    $Binding.Source = $TextBox
    [void]$Expander.SetBinding([System.Windows.Controls.Expander]::WidthProperty,$Binding)

    # Create a textbox for the expander
    $ExpanderTextBox = New-Object System.Windows.Controls.TextBox
    $ExpanderTextBox.Text = "$_"
    $ExpanderTextBox.Padding = 5
    $ExpanderTextBox.BorderThickness = 0
    $ExpanderTextBox.FontSize = 16
    $ExpanderTextBox.TextWrapping = "Wrap"
    $ExpanderTextBox.IsReadOnly = $True
    $Expander.Content = $ExpanderTextBox

    # Assemble controls into a stackpanel
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.AddChild($TextBox)
    $StackPanel.AddChild($Expander)

    # Using no rounded corners as they do not stay true when the window resizes
    New-WPFMessageBox -Content $StackPanel -Title "PSSession Error" -TitleBackground Red -TitleFontSize 20 -Sound 'Windows Unlock' -CornerRadius 0
}

img22

img23

You can also ask the user for input and return the results to a variable in the containing script. For example, here I am simply asking the user to select an option from a drop-down list. The selection will be saved to a variable that can be used later in the script.

Location


Add-Type -AssemblyName PresentationFramework

# Define the location list
$Array = @(
    "London"
    "Paris"
    "New York"
    "Tokyo"
)

# Create a stackpanel container
$StackPanel = New-Object System.Windows.Controls.StackPanel

# Create a combobox
$ComboBox = New-Object System.Windows.Controls.ComboBox
$ComboBox.ItemsSource = $Array
$ComboBox.Margin = "10,10,10,0"
$ComboBox.Background = "White"
$ComboBox.FontSize = 16

# Create a textblock
$TextBlock = New-Object System.Windows.Controls.TextBlock
$TextBlock.Text = "Select your location from the list:"
$TextBlock.Margin = 10
$TextBlock.FontSize = 16

# Assemble the stackpanel
$TextBlock, $ComboBox | foreach {
    $StackPanel.AddChild($PSItem)
}

$Params = @{
    Content = $StackPanel
    Title = "Location Selector"
    TitleFontSize = 22
    TitleFontWeight = "Bold"
    TitleBackground = "CadetBlue"
}

# Display the message
New-WPFMessageBox @Params

# Set the variable from the selected value
$Location = $ComboBox.SelectedValue

If your script returns data in table format, how about using a WPF datagrid to display it in a nicer way? You can populate a datagrid from an array, but I recommend converting to a datatable object as it’s a type that fully supports the features of the datagrid.
In this example, I’m simply displaying services on my system from the Get-Service cmdlet.


# Get Services
$Fields = @(
    'Status'
    'DisplayName'
    'ServiceName'
)
$Services = Get-Service | Select $Fields

# Add Services to a datatable
$Datatable = New-Object System.Data.DataTable
[void]$Datatable.Columns.AddRange($Fields)
foreach ($Service in $Services)
{
    $Array = @()
    Foreach ($Field in $Fields)
    {
        $array += $Service.$Field
    }
    [void]$Datatable.Rows.Add($array)
}

# Create a datagrid object and populate with datatable
$DataGrid = New-Object System.Windows.Controls.DataGrid
$DataGrid.ItemsSource = $Datatable.DefaultView
$DataGrid.MaxHeight = 500
$DataGrid.MaxWidth = 500
$DataGrid.CanUserAddRows = $False
$DataGrid.IsReadOnly = $True
$DataGrid.GridLinesVisibility = "None"

$Params = @{
    Content = $DataGrid
    Title = "Services on $Env:COMPUTERNAME"
    ContentBackground = "WhiteSmoke"
    FontFamily = "Tahoma"
    TitleFontWeight = "Heavy"
    TitleBackground = "LightSteelBlue"
    TitleTextForeground = "Black"
    Sound = 'Windows Message Nudge'
    ContentTextForeground = "DarkSlateGray"
}
New-WPFMessageBox @Params

img24

How about using a TreeView object to group the services by status?


$TreeView = New-Object System.Windows.Controls.TreeView
$TreeView.MaxHeight = 500
$TreeView.Height = 400
$TreeView.Width = 400
$TreeView.BorderThickness = 0
"Running","Stopped" | foreach {
    $Item = New-Object System.Windows.Controls.TreeViewItem
    $Item.Padding = 5
    $Item.FontSize = 16
    $Item.Header = $_
    [void]$TreeView.Items.Add($Item)
}
$TreeView.Items[0].ItemsSource = Get-Service | Where {$_.Status -eq "Running"} | Select -ExpandProperty DisplayName
$TreeView.Items[1].ItemsSource = Get-Service | Where {$_.Status -eq "Stopped"} | Select -ExpandProperty DisplayName

New-WPFMessageBox -Content $TreeView -Title "Services by Status" -TitleBackground "LightSteelBlue"

img25

If you want to use the message box function in your own WPF UI tool, then you can use the WindowHost parameter to pass the host window object. This will make the host window owner of the message box window, and means your message box will always display centered in the host window, no matter the size or location of the window.

In this example I have created a simple host window with a big “Click Me” button. When clicked, a message box will display.


$Window = New-object System.Windows.Window
$Window.WindowStartupLocation = "CenterScreen"
$Window.SizeToContent="WidthAndHeight"
$Button = New-Object System.Windows.Controls.Button
$Button.Content = "Click Me!"
$Button.FontSize = 28
$Button.HorizontalAlignment = "Center"
$Button.VerticalAlignment = "Center"
$Button.Height = 300
$Button.Width = "400"
$Button.Background = "WhiteSmoke"
$Button.BorderThickness = 0
$Window.AddChild($Button)

$Button.Add_Click({
    New-WPFMessageBox -Content "This Content box is owned by the parent window and will be positioned centrally in the window." -Title "WPF Content Box" -TitleBackground Coral -WindowHost $Window
})

$Window.ShowDialog()

img26

img27

My final example is a bit more advanced, but it demonstrates how the message box could be used in a more dynamic way to display output from code running in a background job.

I’ve created a stackpanel from XAML code containing a textbox and a button.  These are all added to a synchronized hash table which gets passed, together with a code block, to a new PowerShell instance which runs in the background. The code block simply pings a list of computers in succession and updates the textbox in real time with the result of each ping.

The background job is triggered by the “Begin” button.


# XAML code
[XML]$Xaml = @"
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <TextBox ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto" Name="TextBox" Width="400" Height="200" Text="" BorderThickness="0" FontSize="18" TextWrapping="Wrap" FontFamily="Arial" IsReadOnly="True" Padding="5"/>
    <Button Name="Button" Content="Begin" Background="White" FontSize="24" HorizontalAlignment="Center" Cursor="Hand" />
</StackPanel>
"@

# Create the WPF object from the XAML code
$ChildElement = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $xaml))

# Create a synchronised hashtable and add elements to it
$UI = [System.Collections.Hashtable]::Synchronized(@{})
$UI.ChildElement = $ChildElement
$UI.TextBox = $ChildElement.FindName('TextBox')
$UI.Button = $ChildElement.FindName('Button')

# Define the code to run in a background job
$Code = {
    Param($UI)

    # Disable the button during run
    $UI.TextBox.Dispatcher.Invoke({
        $UI.Button.IsEnabled=$False
        $UI.Button.Foreground="Gray"
    })

    # Ping IP addresses
    10..20 | foreach {
        $Index = $_
        $IPaddress = "10.25.24.$Index"
        If (Test-Connection -ComputerName $IPaddress -Count 1 -Quiet)
        {
            $UI.TextBox.Dispatcher.Invoke({
                $UI.TextBox.Text = $UI.TextBox.Text + "`n" + "$IPAddress is online"
                $UI.TextBox.ScrollToEnd()
            })
        }
        Else
        {
            $UI.TextBox.Dispatcher.Invoke({
                $UI.TextBox.Text = $UI.TextBox.Text + "`n" + "$IPAddress could not be contacted"
                $UI.TextBox.ScrollToEnd()
            })
        }
    }

    # Re-enable button
    $UI.TextBox.Dispatcher.Invoke({
        $UI.Button.IsEnabled=$True
        $UI.Button.Foreground="Black"
    })
}

# Define the code to run when the button is clicked
$UI.Button.Add_Click({

    # Spin up a powershell instance and run the code
    $PowerShell = [powershell]::Create()
    $PowerShell.AddScript($Code)
    $PowerShell.AddArgument($UI)
    $PowerShell.BeginInvoke()

})

New-WPFMessageBox -Title "Test-Connection Log Window" -Content $ChildElement

Ping

You could also run the code automatically when the message box opens by using the OnLoaded parameter, or by subscribing to the Loaded event on one of the UI elements instead of the button click, eg:


$UI.TextBox.Add_Loaded({

    # Spin up a powershell instance and run the code
    $PowerShell = [powershell]::Create()
    $PowerShell.AddScript($Code)
    $PowerShell.AddArgument($UI)
    $PowerShell.BeginInvoke()

})

# or...

$OnLoadedCode = {

    # Spin up a powershell instance and run the code
    $PowerShell = [powershell]::Create()
    $PowerShell.AddScript($Code)
    $PowerShell.AddArgument($UI)
    $PowerShell.BeginInvoke()

}

New-WPFMessageBox -Title "Test-Connection Log Window" -Content $ChildElement -OnLoaded $OnLoadedCode<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

And I’m done. Try it out!

Btw, here’s the code 🙂

New tool: ConfigMgr PXE Boot Log

Today I’m releasing a new tool for ConfigMgr admins and IT support staff!  This tool displays PXE boot events in an easy-to-view format and provides a history of PXE boot attempts on a distribution point during a selected time period. The tool can also display any records that exist in ConfigMgr associated with a device based on its SMBIOS GUID. The ‘log’ entries returned come from the status messages sent by the distribution point and not from the SMSPXE.log.

PXEBoot

The tool can help in troubleshooting PXE boot failures such as devices not being targeted with a deployment, duplicate records in the ConfigMgr database, or mismatched unique identifiers because of hardware changes.

Requirements

  • Minimum PowerShell 5
  • Minimum .Net Framework 4.5
  • Minimum read-only access to the Configuration Manager database (db_datareader role)

Installation

The tool can be downloaded and installed from the Technet Gallery.

Configuration

  • For first time use, click the “More Options” menu (3 dots, top-right) and choose Settings
  • Enter your Configuration Manager SQL Server and Database
  • Optionally select to view dates and times in your local timezone instead of the default UTC.
  • Click Save.

Using the Tool

  • Select a PXE-Enabled distribution point from the drop-down list.
  • Select a time period to view results
  • Click Retrieve Log
  • Double-click any log entry to retrieve the associated record/s for the device in ConfigMgr

Source Code

Source code is available on GitHub