Get Program Execution History from a ConfigMgr Client with PowerShell

Have you ever been in the situation where something unexpected happens on a users computer and people start pointing their fingers at the ConfigMgr admin and asking “has anyone deployed something with SCCM?” Well, I decided to write a PowerShell script to retrieve the execution history for ConfigMgr programs on a local or remote client. This gives clear visibility of when and which deployments such as applications/programs/task sequences have run on the client and hopefully acquit you (or prove you guilty!)

Program execution history can be found in the registry but it doesn’t contain the name of the associated package, so I joined that data with software distribution data from WMI to give a better view.

You can run the script against the local machine, or a remote machine if you have PS remoting enabled. You can also run it against multiple machines at the same time and combine the data if desired. I recommend to pipe the results to grid view.

Get-CMClientExecutionHistory -Computername PC001,PC002 | Out-GridView

Using Windows 10 Toast Notifications with ConfigMgr Application Deployments

When deploying software with ConfigMgr, the ConfigMgr client can create a simple “New software is available” notification to inform the user that something new is available to install from the Software Center. But this notification is not overly descriptive. You might wish to provide a more detailed notification with a description of the software, why the user should install it, the installation deadline etc. For Windows 10, we can do that simply by disabling the inbuilt notifications on the deployment and creating our own custom toast notifications instead.

The Notification

Consider the examples below.

Here I have created a simple toast notification with the name of the software, what it does, what it is needed for, and a simple instruction to close Outlook before installing. The user can then choose to install it now – and clicking on that button will simply open the Software Center to that application via it’s sharing link. If they click Another time… the notification goes away for now, and if they dismiss it, it will move to the Action Center.

Title Only

In this version, I’ve added a logo instead of a title…

Image Only

…and in this version, I’ve added both.

Title and Image

If the deployment has a deadline, you can state the deadline in the notification as well as tell the user how long they have left before the deadline is reached.

Image with Deadline

Clicking Install now opens that app in the Software Center where the user can go ahead and install it…

Software Center

The big gotcha (for now) is that this only works with Application deployments, and you need to be running ConfigMgr 1706 or later. Please, Microsoft, make sharing links possible for other deployments (packages/programs, task sequences) too!

The client machines also need to be running Windows 10 Anniversary Update or later for the notification to work properly.

The Magic

So how does this work? Well, first we need to disable the inbuilt notifications on the application deployment, so set that to Display in Software Center, and only show notifications for computer restarts in the deployment type on the User Experience tab.

Next, we create a compliance item and compliance baseline which will display the notification. Target the compliance baseline at the same collection/s you are targetting your application.

The compliance item will have a PowerShell discovery script and remediation script. The discovery script will simply detect whether the software has been installed and report compliance if it is. The remediation script contains the code that displays the notification, and will only run if the discovery script does not report compliance, ie the software is not yet installed.

The Code

For the discovery script, create some code that will detect whether the software is installed. For my example, I used the code below which simply checks for the existence of a registry key.


## Discovery script for Veritas Enterprise Vault Outlook Add-in (x64) 12.2.1.1485

$RegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0DBA46D1-5D49-4888-BC50-D3DF38F85126}"
If (Test-Path $RegKey)
{
    "Compliant"
}
Else
{
    "Not compliant"
}

It’s important that the script outputs a value whether it’s compliant or not, so you don’t get issues with the instance not being found.

For the remediation script, I created the following code to display a toast notification:

Code Walkthrough

Let’s walk through the code to explain the variables and what it does.

Variables

Title is the notification title that displays more prominently, the name of the software for example.

SoftwareCenterShortcut is the sharing link from your ConfigMgr application. To get this, you simply deploy the application to a machine, go to the Software Center, open the application and in the top-right click the link and copy and paste the link as the variable value.

AudioSource is the sound that displays when the notification appears. There are various options here, see the reference in the script for more info.

SubtitleText and BodyText contain the main wording in the notification.

HeaderFormat is a choice of either:

  1. TitleOnly – this just displays a title in the notification header
  2. ImageOnly – this just displays an image in the notification header
  3. TitleAndImage – this displays both

Base64Image – if you wish to include an image or a logo, use this optional variable. You need to convert an image file to a base64 string first, and code is included in the script for how to do that. You can output the base64 string to a text file and copy and paste it back into the script in this variable.

The reason for encoding the image is simply to avoid any dependencies on files in network locations, setting directory access or requiring internet access. The script will convert the base64 string back to an image file and save it in the user’s temporary directory.

Deadline is an optional parameter. If your deployment has a deadline, you probably want to include that in the notification. Deadline should be a parseable datetime format.

What the Script Does

The script will register PowerShell in the HKCU registry as an application that can display notifications in the Action Center, if it isn’t registered already.

Next it defines the toast notification in XML format. I chose XML to avoid any dependencies on external modules, and it’s actually quite simple to create a notification that way. The schema for toast notification is all documented by Microsoft and you can find a reference in the script.

Next it manipulates the XML a bit depending on whether you chose to display an image or use a deadline etc.

Finally, the notification is displayed.

Duration

The notification uses the reminder scenario so that it stays visible on the screen until the user takes action with it. If this is undesirable, you can change it to a normal notification with either the standard or longer duration. In this case, you need to be sure that the text in the notification can be read in that time frame.

In the toast template XML definition, change the first line from:

<toast scenario=”reminder”>

to either (default duration 5 seconds)

<toast duration=”short”>

or (around 25 seconds)

<toast duration=”long”>

Creating the Compliance Item and Baseline

When creating the compliance item in SCCM, make sure of the following:

  • Supported platforms – should be Windows 10 only. Actually, I have used some features in toast notifications that are only available in the Anniversary Update and later, so don’t target versions less than.
  • User context – make sure the compliance item has the option Run scripts by using the logged on user credentials checked
  • Compliance rule value – the value returned by the script should equal “Compliant
  • Compliance rule remediation – make sure that Run the specified remediation script when this setting is noncompliant is checked

When creating the deployment for the compliance baseline in SCCM, make sure of the following:

  • Remediate noncompliant rules when supported is checked
  • Allow remediation outside the maintenance window is checked (if that is acceptable in your environment)

Conclusion

This is a handy way to create your own notifications for ConfigMgr application deployments in Windows 10 and is fully customizable per application, within the limits of the toast notification schema. If and when Microsoft make sharing links available for task sequences, or packages and programs too, this would become even more useful, for example, sending a custom notification when a Windows 10 version upgrade is available.

PowerShell DeepDive: WPF, Data Binding and INotifyPropertyChanged

PowerShell can be used to create some nice UI front-ends using the Windows Presentation Framework (WPF). You can create anything from a simple popup window to a full-blown, self-contained application. A major concept in WPF is that of data binding. Data binding allows you to create some separation between the design of the UI and data it operates with. Binding UI element attributes to a data source means that when you need to update data in the UI, you don’t need to edit the object property itself, rather you simply update the data source it is bound to. For that to work automatically, you need to bind to a collection type that implements the INotifyPropertyChanged interface. This interface provides a notification mechanism that can notify the bound element when a value in the datasource has changed. The UI can then automatically update to reflect the change.

Without using data binding you need to create some code to update the UI element and tell it what new data to display. In a single-threaded UI you can simply set the object property directly, and in a multi-threaded UI you will invoke the dispatcher on the object, so that the property can be safely updated without contention from another thread.

Maybe this all sound Greek (no offence if you speak Greek!), so lets do a couple of examples to illustrate.

Increment Me!

Here I created a simple function that will display a WPF UI window. Then I created an event handler so that when the button is clicked, the textbox will update. The textbox contains a number that will be incremented every time you click the button.


Add-Type -AssemblyName PresentationFramework

function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "WPF Window"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create a textbox object
    $TextBox = New-Object System.Windows.Controls.TextBox
    $TextBox.Height = 85
    $TextBox.HorizontalContentAlignment = "Center"
    $TextBox.VerticalContentAlignment = "Center"
    $TextBox.FontSize = 30
    $Hash.TextBox = $TextBox

    # Create a button object
    $Button = New-Object System.Windows.Controls.Button
    $Button.Height = 85
    $Button.HorizontalContentAlignment = "Center"
    $Button.VerticalContentAlignment = "Center"
    $Button.FontSize = 30
    $Button.Content = "Increment Me!"
    $Hash.Button = $Button

    # Assemble the window
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Margin = "5,5,5,5"
    $StackPanel.AddChild($TextBox)
    $StackPanel.AddChild($Button)
    $Window.AddChild($StackPanel)
    $Hash.Window = $Window
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Set the value of the textbox
$Hash.TextBox.Text = [int]0

# Add an event for the button click
$Hash.Button.Add_Click{

    # Get the current value
    [int]$CurrentValue = $Hash.TextBox.Text

    # Increment the value
    $CurrentValue ++

    # Update the text property with the new value
    $Hash.TextBox.Text = $CurrentValue
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

gif

Since the data type for the text property of the textbox is a string, I cannot increment the value directly, I must first cast it into an integer, then return it back to the textbox where it will again be stored as a string. Since the textbox text property is not bound to a data source, I must update the property directly. In this simple example that is not a problem, but it does mean that I cannot work independently of the data.

Creating a data binding

In the following example I have bound the textbox text property to a data source. Data binding in WPF can be done either in XAML or code, but here we are using code to illustrate how that can be done.

First, we will create a datasource object. This will be what we bind to. Since the datasource must notify the UI when the bound value has changed, we must use a collection that implements the INotifyPropertyChanged interface. You can actually create a custom class in C# to do that, as Ryan Ephgrave nicely describes on his blog. This is quite a flexible method, however it does mean you need to create all the properties used in your datasource up front in the class. A ‘native’ way to do that in .Net is to use an observable collection.

In this example, I am going to use an observable collection with a simple System.Object type, so I can add multiple kinds of object to the collection if I want.


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

After creating the observable collection, I then add my initial value for the textbox – 0, and finally set the collection as the datacontext for the WPF window.

When using data binding, one can either declare the binding source directly, or set the datacontext for one of the parent elements. When you set the datacontext at the window level for example, this will be inherited by default by any child elements in the window, such as textboxes, panels etc. For more granular control, you can set the datacontext on the object itself rather than at the top-level. In either case, the bound object will use this datacontext.

Next, I create a binding object and pass to it which object in the datasource I want to bind to. In this case, I only have one item in the collection so I can simply reference it using it’s index number, which is [0]. If the collection contains multiple items, you need to keep a reference of which index your items are using.


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding -ArgumentList "[0]"

Another way to do that would be to set the path property of the binding object:


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding
$Binding.Path = "[0]"

If you prefer not to use a datacontext, you can also declare a binding source in the binding object instead:


# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding
$Binding.Path = "[0]"
$Binding.Source = $DataContext

Now I need to set the binding mode, which is oneway in the case:


$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay

And then finally I create the binding on the WPF element property. For the overload of the SetBinding() method, I need to provide the object I am binding to ($Hash.TextBox), then the property I am binding to (Text), then the binding object itself.


[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

Now I can handle the button click event, but this time instead of updating the “text” property directly, I simply update the datasource it is bound to. In this instance, the datasource is an integer not a string, so I can increment it directly.


# Add an event for the button click
$Hash.Button.Add_Click{
    $DataContext[0] ++
} 

And finally now I run the window and the result is the same: the number is incremented with every click of the button. Only the datasource is being updated, but the text property of the textbox is bound to it and updates automatically when notified by the observable collection.

gif3

Here is the full code (using the same WPF Window function):


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding # -ArgumentList "[0]"
$Binding.Path = "[0]"
$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

# Add an event for the button click
$Hash.Button.Add_Click{
    $DataContext[0] ++
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Multi-threading using the Dispatcher

Using data binding in this way can simplify the way you design your UI, and can also result in better UI performance, especially when using multiple threads because calling the dispatcher from another thread has a higher overhead. Using background threads is essential whenever you run any code that takes long enough that it will cause the UI to freeze and be unresponsive. Offloading that code to a background thread allows the UI to continue running happily in the main thread.

Does data binding work across threads? Yes it does, although when dealing with additional threads you need to consider the thread safety of the collections you are using as your datasource, as not all collections are inherently thread safe. There are some ways to achieve that though, but that’s for another post. Thread safety is important when you have multiple threads that may wish to access the same shared object at the same time.

For now, to illustrate data binding with a multi-threaded UI, lets first create the same WPF window but we will create and use a background thread to update the UI directly using the dispatcher. Each time the button is clicked, a new thread is created and the UI updated from that thread. The dispatcher allows us to queue work items for the window. This is a safe way to update the UI when using multiple threads as the dispatcher will manage each request in turn ensuring we do not suffer contention.

We are using the same WPF window function as previously. When the button is clicked, we read the current value of the texbox into a variable then increment it. We spin up a background thread and call the dispatcher on the window to update the UI. I’ve also included some code to cleanup completed threads using the Get-Runspace cmdlet (this is only available since PowerShell 5)


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Set textbox value
$Hash.TextBox.Text = "0"

# Add an event for the button click
$Hash.Button.Add_Click{

    # Cleanup any completed runspaces
    $FinishedRS = (Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"})
    If ($FinishedRS)
    {
        $FinishedRS.Dispose()
    }

    # Get current textbox value and increment
    [int]$CurrentValue = $Hash.TextBox.Text
    $CurrentValue ++

    # Create and invoke a background thread
    $ScriptBlock = {
        Param($Hash,$CurrentValue)
        $Hash.Window.Dispatcher.Invoke({
            $Hash.TextBox.Text = $CurrentValue
        })
    }
     $PowerShell = [PowerShell]::Create()
    [void]$PowerShell.AddScript($ScriptBlock)
    [void]$PowerShell.AddArgument($Hash)
    [void]$PowerShell.AddArgument($CurrentValue)
    $PowerShell.BeginInvoke()
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

# Cleanup runspaces
(Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"}).Dispose()

The result is that it works the same as before, but the response is somewhat slower. This is due to the additional overhead of creating a background thread and calling the dispatcher.

singlethreaddispatch

Multi-threading using data binding

Now lets do the same thing and use data binding. In this case, I only need to pass the datasource object to the background thread. When I update it in the background thread, the change is also reflected in the main UI thread.


# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datacontext for the textbox and set it
$DataContext = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$Text = [int]0
$DataContext.Add($Text)
$hash.TextBox.DataContext = $DataContext

# Create and set a binding on the textbox object
$Binding = New-Object System.Windows.Data.Binding -ArgumentList "[0]"
$Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
[void][System.Windows.Data.BindingOperations]::SetBinding($Hash.TextBox,[System.Windows.Controls.TextBox]::TextProperty, $Binding)

# Add an event for the button click
$Hash.Button.Add_Click{

    # Cleanup any completed runspaces
    $FinishedRS = (Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"})
    If ($FinishedRS)
    {
        $FinishedRS.Dispose()
    }

    # Create and invoke a background thread
    $ScriptBlock = {
        Param($DataContext)
        $DataContext[0] ++
    }
     $PowerShell = [PowerShell]::Create()
    [void]$PowerShell.AddScript($ScriptBlock)
    [void]$PowerShell.AddArgument($DataContext)
    $PowerShell.BeginInvoke()
} 

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

# Cleanup runspaces
(Get-Runspace | where {$_.RunspaceAvailability -ne "Busy"}).Dispose()

The result this time is that even though there remains a small overhead in creating and managing a background thread, we get a much quicker response. Score!

singlethreaddb

Binding to the UI

Data binding can also be used to bind to items in the UI itself. In the following example, we have two textboxes. The one on the left is bound to a datasource that is incremented everytime the button is clicked. The one on the right is bound to the text property of the textbox on the left. The result is that both textboxes update incrementally, even though only one is bound to a datasource.

uibinding

This time, in the code, I have created a simple function to create a binding. When binding to a UI element, it is necessary to set both the source and path properties of the binding.


function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "WPF Window"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create a button object
    $Button = New-Object System.Windows.Controls.Button
    $Button.Height = 85
    $Button.Width = [System.Double]::NaN # "auto" in XAML
    $Button.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $Button.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $Button.FontSize = 30
    $Button.Content = "Increment Me!"
    $Hash.Button = $Button

    # Create a textbox object
    $TextBox1 = New-Object System.Windows.Controls.TextBox
    $TextBox1.Name = "FirstTextBox"
    $TextBox1.Height = 85
    $TextBox1.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $TextBox1.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $TextBox1.FontSize = 30
    $TextBox1.BorderThickness = 0
    $Hash.TextBox1 = $TextBox1

    # Create a textbox object
    $TextBox2 = New-Object System.Windows.Controls.TextBox
    $TextBox2.Height = 85
    $TextBox2.HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center
    $TextBox2.VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center
    $TextBox2.FontSize = 30
    $TextBox2.BorderThickness = 0
    $Hash.TextBox2 = $TextBox2

    # Assemble the window
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $StackPanel.AddChild($TextBox1)
    $StackPanel.AddChild($TextBox2)

    $MainStackPanel = New-Object System.Windows.Controls.StackPanel
    $MainStackPanel.Margin = "5,5,5,5"
    $MainStackPanel.AddChild($StackPanel)
    $MainStackPanel.AddChild($Button)
    $Window.AddChild($MainStackPanel)
    $Hash.Window = $Window
}

Function Set-Binding {
    Param($Target,$Property,$Path,$Source)

    $Binding = New-Object System.Windows.Data.Binding
    $Binding.Path = $Path
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    If ($Source)
    {
        $Binding.Source = $Source
    }
    [void][System.Windows.Data.BindingOperations]::SetBinding($Target,$Property,$Binding)

    # Another way to do it...
    #[void]$Target.SetBinding($Property,$Binding)
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datasource and set the initial value
$DataSource = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$DataSource.Add([int]0)
$DataSource.Add([int]0)
$Hash.Window.DataContext = $DataSource

# Bind the first text box to the data source
Set-Binding -Target $Hash.TextBox1 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[0]"

# Bind the second text box to the first textbox, text property
Set-Binding -Target $Hash.TextBox2 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Source $Hash.TextBox1 -Path $([System.Windows.Controls.TextBox]::TextProperty)

# Event: Window Loaded
$Hash.Window.Add_Loaded{

    # Set the textbox widths to half the size of the button
    $Hash.TextBox1, $Hash.TextBox2 | foreach {
        $_.Width = $Hash.Button.ActualWidth / 2
    }

}

# Event: Button Clicked
$Hash.Button.Add_Click{

    # Increment the data source value
    $DataSource[0] ++

}

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Learn your times tables!

Finally, my last example is a simple tool to help you learn your multiplication tables (you know you need reminding 😉 ). Here, the 3 text boxes with numbers are all bound to a datasource. When a button is clicked, the value is incremented or decremented in the datasource, and this is reflected in the UI by the binding. The result is then calculated based on the new values, and this is done by handling the TextChanged event on the first 2 text boxes. Again, the UI is never being updated directly, only the datasource is being changed, but the UI also updates because of the bindings.

multipier

The code for this is a bit long because I am creating the entire WPF window the ‘old-school’ way – in code, rather than in XAML. In practice, it is much better to create the UI definition in XAML, and then you will use a combination of XAML and PowerShell code to manage the UI.


function Create-WPFWindow {
    Param($Hash)

    # Create a window object
    $Window = New-Object System.Windows.Window
    $Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
    $Window.Title = "Multiplication Tables"
    $window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
    $Window.ResizeMode = [System.Windows.ResizeMode]::NoResize

    # Create the first value textbox
    $TextBoxProperties = @{Height = 85; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True}
    $TextBox1 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox1.$($_.Name) = $_.Value
    }
    $Hash.TextBox1 = $TextBox1

    # Create the "x" textbox
    $TextBoxProperties = @{Height = 85; Width = 40; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; Text = "x"; IsReadOnly = $True}
    $TextBox2 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox2.$($_.Name) = $_.Value
    }
    $Hash.TextBox2 = $TextBox2

     # Create the second value textbox
    $TextBoxProperties = @{Height = 85; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True }
    $TextBox3 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox3.$($_.Name) = $_.Value
    }
    $Hash.TextBox3 = $TextBox3

    # Create the "=" textbox
    $TextBoxProperties = @{Height = 85; Width = 40; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; Text = "="; IsReadOnly = $True }
    $TextBox4 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox4.$($_.Name) = $_.Value
    }
    $Hash.TextBox4 = $TextBox4

    # Create the calculated value textbox
    $TextBoxProperties = @{Height = 85; Width = 80; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Center; FontSize = 30; BorderThickness = 0; IsReadOnly = $True}
    $TextBox5 = New-Object System.Windows.Controls.TextBox
    $TextBoxProperties.GetEnumerator() | foreach {
        $TextBox5.$($_.Name) = $_.Value
    }
    $Hash.TextBox5 = $TextBox5

    # Create the first "+" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "+"; Margin = "5,0,0,0"}
    $Button1 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button1.$($_.Name) = $_.Value
    }
    $Hash.Button1 = $Button1

    # Create the first "-" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "-"; Margin = "5,0,0,0"}
    $Button2 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button2.$($_.Name) = $_.Value
    }
    $Hash.Button2 = $Button2

    # Create the second "+" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "+"; Margin = "28,0,0,0"}
    $Button3 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button3.$($_.Name) = $_.Value
    }
    $Hash.Button3 = $Button3

    # Create the second "-" button
    $ButtonProperties = @{Height = 30; Width = 30; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "-"; Margin = "5,0,0,0"}
    $Button4 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button4.$($_.Name) = $_.Value
    }
    $Hash.Button4 = $Button4

     # Create the reset button
    $ButtonProperties = @{Height = 30; Width = 60; HorizontalContentAlignment = [System.Windows.HorizontalAlignment]::Center; VerticalContentAlignment = [System.Windows.VerticalAlignment]::Top; FontSize = 20; Content = "Reset"; Margin = "40,0,0,0"}
    $Button5 = New-Object System.Windows.Controls.Button
    $ButtonProperties.GetEnumerator() | foreach {
        $Button5.$($_.Name) = $_.Value
    }
    $Hash.Button5 = $Button5

    # Assemble the first stackpanel
    $StackPanel = New-Object System.Windows.Controls.StackPanel
    $StackPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $TextBox1, $TextBox2, $TextBox3, $TextBox4, $TextBox5 | foreach {
        $StackPanel.AddChild($_)
    }

    # Assemble the second stackpanel
    $StackPanel2 = New-Object System.Windows.Controls.StackPanel
    $StackPanel2.Orientation = [System.Windows.Controls.Orientation]::Horizontal
    $Button1, $Button2, $Button3, $Button4, $Button5 | foreach {
        $StackPanel2.AddChild($_)
    }

    # Assemble the window
    $MainStackPanel = New-Object System.Windows.Controls.StackPanel
    $MainStackPanel.Margin = "5,5,5,5"
    $MainStackPanel.AddChild($StackPanel)
    $MainStackPanel.AddChild($StackPanel2)
    $Window.AddChild($MainStackPanel)
    $Hash.Window = $Window
}

Function Set-Binding {
    Param($Target,$Property,$Path,$Source)

    $Binding = New-Object System.Windows.Data.Binding
    $Binding.Path = $Path
    $Binding.Mode = [System.Windows.Data.BindingMode]::OneWay
    If ($Source)
    {
        $Binding.Source = $Source
    }
    [void][System.Windows.Data.BindingOperations]::SetBinding($Target,$Property,$Binding)

    # Another way to do it...
    #[void]$Target.SetBinding($Property,$Binding)
}

# Create a WPF window and add it to a Hash table
$Hash = @{}
Create-WPFWindow $Hash

# Create a datasource and set the initial values
$DataSource = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$DataSource.Add([int]1)
$DataSource.Add([int]1)
$DataSource.Add([int]1)
$Hash.Window.DataContext = $DataSource

# Bind the value text boxes to the data source
Set-Binding -Target $Hash.TextBox1 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[0]"
Set-Binding -Target $Hash.TextBox3 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[1]"
Set-Binding -Target $Hash.TextBox5 -Property $([System.Windows.Controls.TextBox]::TextProperty) -Path "[2]"

# Events: Button Clicks
$Hash.Button1.Add_Click{
    # Increment
    $DataSource[0] ++
}

$Hash.Button2.Add_Click{
    # Decrement
    $DataSource[0] --
}

$Hash.Button3.Add_Click{
    # Increment
    $DataSource[1] ++
}

$Hash.Button4.Add_Click{
    # Decrement
    $DataSource[1] --
}

$Hash.Button5.Add_Click{
    # Reset Values to 1
    $DataSource[0] = [int]1
    $DataSource[1] = [int]1
}

# Events: TextBox values changed
$Hash.TextBox1, $Hash.TextBox3, $hash.TextBox5 | foreach {
    $_.Add_TextChanged{
        # Calculate
        $DataSource[2] = $DataSource[0] * $DataSource[1]
    }
}

# Show the window
[void]$Hash.Window.Dispatcher.InvokeAsync{$Hash.Window.ShowDialog()}.Wait()

Conclusion

Data binding can get much more complex than this including two-way bindings, priority binding and multiple bindings, but hopefully these examples will whet your appetite to explore it further. When using data binding in your UI you are taking a step towards implementing the MVVM, a kind of best-practice concept for designing UI applications. Whether you really need to use data binding or not depends a lot on the type of UI you are creating and what it does, but certainly consider taking advantage of it to create a more efficient and logically designed PowerShell UI.

Have fun!

Explore WPF Controls with PowerShell

If you’ve ever tried creating a tool or an application with WPF, you know that the built-in controls contain many properties, methods and events, and finding the right one to use can be, well, fun!

As an aid to creating WPF applications, I created this simple tool which explores the various controls and exposes their properties, methods and events. It’s handy as a quick reference, or as a convenient way to get familiar with the various controls.

The tool is a PowerShell script, so simply download the script, right-click and ‘Run with PowerShell’.

More info here: https://smsagent.wordpress.com/tools/wpf-control-explorer/

wpf-controlexplorer

New Free App – ConfigMgr Deployment Reporter

Just released a new free application for ConfigMgr admins – ConfigMgr Deployment Reporter.  I developed this app for use in the organisation I currently work for, and it turned out quite well, so I decided to release a public version to the community!

capture

I developed this app as an alternative (and IMO easier) way to report on ConfigMgr deployments than using the ConfigMgr console. It uses a little different format than the console node allowing you to select which deployment you wish to view data for based on the “feature type” (ie application, package etc) and report on only that deployment.  It also introduces a separation of results between all applicable systems for a deployment, and only those systems which have currently reported status, which allows for a more accurate view of the success of a deployment as it progresses.

The app allows the creation of charts and HTML-format reports to give a nice graphical snapshot of a deployment.

I also added the capability to report per-device for Software Update and Task Sequence deployments.  For Software Updates, this allows you to see which updates from the deployment are applicable to the machine and the status of each update, and for Task Sequences it allows viewing the execution status of each step in the task sequence for the selected device.

As usual, I code purely in PowerShell using WPF for the UI.  This time I added metro styling using the excellent MahApps.Metro project 🙂

Download the app from here.

Detect an Active VPN Adapter During ConfigMgr Deployments

A common requirement with ConfigMgr deployments is to exclude clients that are connected to the corporate network via a VPN, when the total size of the content files for the deployment are too much to be throwing down a slow network link. There is more than one way to do this, but I have seen that not all are reliable and do not work in every case or for every VPN adapter out there.

For example, using PowerShell, you can run either of the following WMI queries to potentially detect an active VPN adapter (your VPN adapter description may be different):

Using Win32_NetworkAdapter


Get-WmiObject -Query "Select * from Win32_NetworkAdapter where Name like '%VPN%' and NetEnabled='True'"

Using Win32_NetworkAdapterConfiguration


Get-WmiObject -Query "Select * from Win32_NetworkAdapterConfiguration where Description like '%VPN%' and IPEnabled='True'"

Since Windows 8 / Server 2012 you can also use the Get-VPNConnection cmdlet:


(Get-VpnConnection -AllUserConnection).where{$_.Name -like "*VPN*" -and $_.ConnectionStatus -eq "Connected"}

Another method is simply:


ipconfig | Select-String 'PPP adapter'

But my preferred method is to check the IPv4 routing table. This is because VPN connections typically use their own subnet, so when connected they will add entries to the IP routing table for that subnet, and will remove them again when disconnected. If you know the subnets used by your VPN connections, you can query for them in WMI:


Get-WmiObject -Query "Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%' 

To use this with Application deployments in ConfigMgr, you can create a Global Condition with a script setting.  This condition could be used either to target or to exclude systems using VPN:

capture

Here is an example script that returns “VPN-Active” or “VPN-InActive” based on whether a VPN subnet is detected:


If (Get-WmiObject -Query "Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%'")
    {Write-host "VPN-Active"}
Else {Write-host "VPN-InActive"}

You can then add this as a requirement to an application:

capture

For task sequences, you can use a WMI query condition:

WMI Query


Select * from Win32_IP4RouteTable where Name like '10.0.99.%' or Name like '10.15.99.%'

 

capture

The only concession is if your VPN subnets ever change, you will need to update them in ConfigMgr.

New Tool: System Explorer for Windows

Today I am pleased to release a new tool for enterprises and home users alike: System Explorer for Windows.  This application can be used to view detailed system and hardware data for a local or remote computer by exposing WMI Win32 classes in an easy-to-use Graphical User Interface.  For enterprises that use System Center Configuration Manager, the application can connect to the SCCM database to allow viewing of hardware inventory data even if the target system is not currently online.

capture
System Explorer for Windows – Client WMI view

Check it out here: https://smsagent.wordpress.com/tools/system-explorer-for-windows/

 

Creating WPF GUI Applications with Pure PowerShell

When creating a GUI application in PowerShell, I usually use Visual Studio, or Blend for Visual Studio, to design a WPF application, then copy and run the XAML code in PowerShell.  Designing in VS is generally easier and quicker and creates less code, but it is also perfectly possible to create a WPF GUI using pure PowerShell, which is more akin to the Windows Forms method of GUI creation.  For more complex applications that’s not the ideal way because it takes longer and creates a lot more code, but for simple applications, or if you are used to designing something in Windows Forms, why not give it a go?  You simply need to create a window from the [System.Windows] .Net namespace, then add some controls from the [System.Windows.Controls] namespace, and you’re away!

Here’s a simple example application that displays the running processes on your machine, the list of services, and a little game “Click the Fruit” as a bonus 😉

capture


# Add required assembly
Add-Type -AssemblyName PresentationFramework

# Create a Window
$Window = New-Object Windows.Window
$Window.Height = "670"
$Window.Width = "700"
$Window.Title = "PowerShell WPF Window"
$window.WindowStartupLocation="CenterScreen"

# Create a grid container with 2 rows, one for the buttons, one for the datagrid
$Grid =  New-Object Windows.Controls.Grid
$Row1 = New-Object Windows.Controls.RowDefinition
$Row2 = New-Object Windows.Controls.RowDefinition
$Row1.Height = "70"
$Row2.Height = "100*"
$grid.RowDefinitions.Add($Row1)
$grid.RowDefinitions.Add($Row2)

# Create a button to get running Processes
$Button_Processes = New-Object Windows.Controls.Button
$Button_Processes.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Processes.Height = "50"
$Button_Processes.Width = "150"
$Button_Processes.Margin = "10,10,10,10"
$Button_Processes.HorizontalAlignment = "Left"
$Button_Processes.VerticalAlignment = "Top"
$Button_Processes.Content = "Get Processes"
$Button_Processes.Background = "Aquamarine"

# Create a button to get services
$Button_Services = New-Object Windows.Controls.Button
$Button_Services.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Services.Height = "50"
$Button_Services.Width = "150"
$Button_Services.Margin = "180,10,10,10"
$Button_Services.HorizontalAlignment = "Left"
$Button_Services.VerticalAlignment = "Top"
$Button_Services.Content = "Get Services"
$Button_Services.Background = "Aquamarine"

# Create a button to play Click the fruit
$Button_Cool = New-Object Windows.Controls.Button
$Button_Cool.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Cool.Height = "50"
$Button_Cool.Width = "150"
$Button_Cool.Margin = "350,10,10,10"
$Button_Cool.HorizontalAlignment = "Left"
$Button_Cool.VerticalAlignment = "Top"
$Button_Cool.Content = "Play 'Click the Fruit'"
$Button_Cool.Background = "Aquamarine"

# Create a datagrid
$DataGrid = New-Object Windows.Controls.DataGrid
$DataGrid.SetValue([Windows.Controls.Grid]::RowProperty,1)
$DataGrid.MinHeight = "100"
$DataGrid.MinWidth = "100"
$DataGrid.Margin = "10,0,10,10"
$DataGrid.HorizontalAlignment = "Stretch"
$DataGrid.VerticalAlignment = "Stretch"
$DataGrid.VerticalScrollBarVisibility = "Auto"
$DataGrid.GridLinesVisibility = "none"
$DataGrid.IsReadOnly = $true

# Add the elements to the relevant parent control
$Grid.AddChild($DataGrid)
$grid.AddChild($Button_Processes)
$grid.AddChild($Button_Services)
$grid.AddChild($Button_Cool)
$window.Content = $Grid

# Add an event on the Get Processes button
$Button_Processes.Add_Click({
    $Processes = Get-Process | Select ProcessName,HandleCount,NonpagedSystemMemorySize,PrivateMemorySize,WorkingSet,UserProcessorTime,Id
    $DataGrid.ItemsSource = $Processes
    })

# Add an event on the Get Services button
$Button_Services.Add_Click({
    $Services = Get-Service | Select Name,ServiceName,Status,StartType
    $DataGrid.ItemsSource = $Services
    })

# Add an event to play Click the fruit
$Button_Cool.Add_Click({

    $Fruit = @{
        Apples = "Green"
        Bananas = "Yellow"
        Oranges = "Orange"
        Plums = "Maroon"
    }

    $Fruit.GetEnumerator() | Foreach {
        # Create a transparent window at a random location on the screen
        $NewWindow = New-Object Windows.Window
        $NewWindow.SizeToContent = "WidthAndHeight"
        $NewWindow.AllowsTransparency = $true
        $NewWindow.WindowStyle = "none"
        $NewWindow.Background = "Transparent"
        $NewWindow.WindowStartupLocation = "Manual"
        $Height = Get-Random -Maximum (([System.Windows.SystemParameters]::PrimaryScreenHeight) - 100)
        $Width = Get-Random -Maximum (([System.Windows.SystemParameters]::PrimaryScreenWidth) - 100)
        $NewWindow.Left = $Width
        $NewWindow.Top = $Height

        # Add a label control for the fruit
        $NewLabel = New-Object Windows.Controls.Label
        $NewLabel.Height = "150"
        $NewLabel.Width = "400"
        $NewLabel.Content = $_.Name
        $NewLabel.FontSize = "100"
        $NewLabel.FontWeight = "Bold"
        $NewLabel.Foreground = $_.Value
        $NewLabel.Background = "Transparent"
        $NewLabel.Opacity = "100"

        # Add an event to close the window when clicked
        $NewWindow.Add_MouseDown({
            $This.Close()
        })

        # Add an event to change the cursor to a hand when the mouse goes over the window
        $NewWindow.Add_MouseEnter({
        $this.Cursor = "Hand"
        })

        # Display the window
        $NewWindow.Content = $NewLabel
        $NewWindow.ShowDialog()
    }
})

# Show the window
if (!$psISE)
{
    # Hide PS console window
    $windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
    $asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
    $null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0) 

    # Run as an application in it's own context
    $app = [Windows.Application]::new()
    $app.Run($Window)
}
Else
{
    [void]$window.ShowDialog()
}

Temporarily Increasing the ConfigMgr Client Cache Size for a Large Application

Recently I had to deploy an application whose content files were larger than the default SCCM client cache size (5120 MB).  This will return an error in the Software Center, such as:

0x87D01201 (The content download cannot be performed because there is not enough available space in cache or the disk is full.)

I didn’t want to permanently increase the cache size, or require that user do it manually, so I investigated some options and came up with a couple of simple PowerShell scripts that can increase or decrease the cache size.  I put these scripts into a standard package and created a program for each script using a command-line like:

powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File .\Increase-CCMCacheSize

You then have various options for how you can run that.  For example, my application was being deployed using a task sequence as there are multiple steps, so I simply right-click the task sequence and on the Advanced tab, I check the option to Run another program first:

capture

This will increase the cache size before the task sequence starts to run, which means it will no longer give an error.

To restore the cache to it’s default size after the application install, I simply add an additional step in the task sequence at the end using the package I created:

capture

I haven’t tested it, but you could do something similar with the standard package model by right-clicking the package program, and setting the Run another program first option. The only issue there is that there is no option to run the script to restore the cache size after, unless you create a kind of dependency chain, ie:

Restore Cache size > (depends on) My Package > (depends on) Increase Cache size

For applications, you could also use the capability for a dependecy chain, but you would need to create the script as an application and use a detection method.

Increase-CCMCacheSize


# Increase SCCM Client cache size to 20000 MB (20GB)
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCM.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -lt 20000)
    {
        write-host "Setting cache size to 20000"
        try
        {
            $CC.TotalSize = 20000
        }
        Catch
        {
            $_
        }
}

Restore-CCMCacheSize


# Restore SCCM Client cache size to default (5120 MB)
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCM.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -gt 5120)
    {
        write-host "Setting cache size to 5120"
        try
        {
            $CC.TotalSize = 5120
        }
        Catch
        {
            $_
        }
}

Detection Method

For an application detection method, you could also use a PowerShell script, something like this:


# Detection method to check the SCCM Client cache size
$CCM = New-Object -com UIResource.UIResourceMGR
$CC = $CCm.GetCacheInfo()
$CacheSize = $CC.TotalSize

if ($CacheSize -eq 20000)
{
    write-host "Compliant"
}
Else
{
    write-host "Not-Compliant"
}

Prompting the End-User during ConfigMgr Application Installs

As a Configuration Manager administrator, from time to time I have to deploy an application where I need to notify the end-user of something before the installation begins. A recent example was a plugin for IE that would fail to install if Internet Explorer was running at the time. I can force-ably kill the running process of course, but that’s not necessarily a nice experience for the user – without warning their browser and any open tabs get closed. So better to notify them first, and give them a chance to close the application themselves and save any work. Rather than email each targeted user and warn them to close Internet Explorer before the plugin installs (which they probably ignore or forget anyway), I wanted the installation process to handle that by some kind of prompt.

I could create a script wrapper for the plugin but that would necessitate running in the user context to display interactively. An easier way is simply to install it using a task sequence with some additional steps that will prompt the user first, kill the process if necessary, then install the plugin. A task sequence also gives me better logging.

The problem with a task sequence is that it runs in the system context, so I cannot interact with the end user who is effectively working in a different session. This can be solved however by using the ServiceUI.exe that comes with MDT. Sometime ago I wrote a post about how to prompt for input during a task sequence, but in this case I don’t want input, I simply want to use a message box.  I also want something reusable – so I don’t have to create a new package for each custom prompt.

I have a nice PowerShell function that will create a message box for me using the Wscript.shell “popup” method, so I added this function to a script, where I have also defined the message parameters I want to use at the bottom.


function New-PopupMessage {
# Return values for reference (https://msdn.microsoft.com/en-us/library/x83z1d9f(v=vs.84).aspx)

# Decimal value    Description  
# -----------------------------
# -1               The user did not click a button before nSecondsToWait seconds elapsed.
# 1                OK button
# 2                Cancel button
# 3                Abort button
# 4                Retry button
# 5                Ignore button
# 6                Yes button
# 7                No button
# 10               Try Again button
# 11               Continue button

# Define Parameters
[CmdletBinding()]
    [OutputType([int])]
    Param
    (
        # The popup message
        [Parameter(Mandatory=$true,Position=0)]
        [string]$Message,

        # The number of seconds to wait before closing the popup.  Default is 0, which leaves the popup open until a button is clicked.
        [Parameter(Mandatory=$false,Position=1)]
        [int]$SecondsToWait = 0,

        # The window title
        [Parameter(Mandatory=$true,Position=2)]
        [string]$Title,

        # The buttons to add
        [Parameter(Mandatory=$true,Position=3)]
        [ValidateSet('Ok','Ok-Cancel','Abort-Retry-Ignore','Yes-No-Cancel','Yes-No','Retry-Cancel','Cancel-TryAgain-Continue')]
        [array]$ButtonType,

        # The icon type
        [Parameter(Mandatory=$true,Position=4)]
        [ValidateSet('Stop','Question','Exclamation','Information')]
        $IconType
    )

# Convert button types
switch($ButtonType)
    {
        "Ok" { $Button = 0 }
        "Ok-Cancel" { $Button = 1 }
        "Abort-Retry-Ignore" { $Button = 2 }
        "Yes-No-Cancel" { $Button = 3 }
        "Yes-No" { $Button = 4 }
        "Retry-Cancel" { $Button = 5 }
        "Cancel-TryAgain-Continue" { $Button = 6 }
    }

# Convert Icon types
Switch($IconType)
    {
        "Stop" { $Icon = 16 }
        "Question" { $Icon = 32 }
        "Exclamation" { $Icon = 48 }
        "Information" { $Icon = 64 }
    }

# Create the popup
(New-Object -ComObject Wscript.Shell).popup($Message,$SecondsToWait,$Title,$Button + $Icon)
}

# Close the Task Sequence Progress UI temporarily (if it is running) so the popup is not hidden behind
try
    {
        $TSProgressUI = New-Object -COMObject Microsoft.SMS.TSProgressUI
        $TSProgressUI.CloseProgressDialog()
    }
Catch {}

# Define the parameters.  View the function parameters above for other options.
$Params = @(
    "The software 'Custom IE Plugin' is being installed to your computer. Please close Internet Explorer then click OK to continue." # Popup message
    0                           # Seconds to wait till the popup window is closed
    "Contoso IT: Custom IE Plugin" # title
    "Ok"                        # Button type
    "Exclamation"               # Icon type
    )

# Run the function
New-PopupMessage @Params

I place this script in a network share that everyone can access, and then simply call it during the task sequence using ServiceUI.exe.

How to Do It

Firstly, I need to create a package in SCCM containing the ServiceUI.exe for x86 and x64 architectures.  This package has no program, but simply contains the exe files, which I have renamed per architecture.  You can find the ServiceUI.exe in the following locations in your MDT install:

C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x64, or
C:\Program Files\Microsoft Deployment Toolkit\Templates\Distribution\Tools\x86

Capture

Once I have created and distributed the package, I create a new task sequence and add two “Run command line” steps at the beginning where I will prompt the user, one for x86 OS and one for x64.

Capture

The following things are needed in this step:

  • Use the package you created that contains the ServiceUI executables
  • Call ServiceUI using a process that the end user is running.  This enables ServiceUI to detect the session of the end user and interact with it.  If you are using a task sequence deployment with the option “Show task sequence progress” enabled, then you can use the tsprogressui.exe process, however if you are hiding the task sequence progress from the user, then this process will not exist, so you can call Explorer.exe which is certain to be running in the user session.
    • Eg, ServiceUI_x86 -process:Explorer.exe
  • You must specify the full path to powershell.exe
    • Eg, %SYSTEMROOT%\System32\WindowsPowershell\v1.0\powershell.exe
  • Use the “-File” parameter to call the powershell script that displays the popup.
  • Do NOT use the “timeout” option in the step, as this will cause ServiceUI to give an access denied error.
  • On the Option tab of the step, I use a couple of WMI queries so that the step only runs if the correct OS architecture is detected, and the Internet Explorer process is actually running.  I don’t want to prompt the user to close IE if it’s not actually open.
    • Eg, Select * from win32_OperatingSystem where OSArchitecture = ’32-bit’
    • Select * from Win32_Process where Name = ‘iexplore.exe’

Capture

A couple of things to note:

  • You could include the PowerShell script in the package with the ServiceUI executables, then you can call it locally instead of from a network share.  But the advantage of keeping the script and the executables separate is that you don’t need to create a new package each time you want to add a prompt – you simply reuse the ServiceUI package and create a new PowerShell script in the network share by copying and updating and existing script.
  • If you are using the “Show task sequence progress” option, the script includes some code that will hide the progress UI temporarily while the popup is displayed, otherwise it may appear behind the progress UI.
  • Don’t try to pass parameters when calling the PowerShell script, ServiceUI doesn’t seem to like that.
  • The script function includes a “SecondsToWait” parameter – this is set to 0 by default, which means the popup will stay on the screen indefinitely until a button is clicked.  In some cases this may not be desirable, so you can set a value here such that the task sequence will continue if no button has been clicked for some time.

Next, in case the user ignored the prompt or it timed-out, we add another “Run command line” step to kill the process forcefully using taskkill, if it is still running.

  • Eg, cmd /c taskkill /F /IM iexplore.exe

Capture

Make sure to add the same WMI process query to this step:

Capture

Then in the last step, we install the application itself.

Now, when the application is deployed to the end user’s machine, the first thing that happens is they get a popup on the screen warning them to close Internet Explorer.

Capture

Sweet 🙂

You could customise this further by adding some code to the script that will set a task sequence variable based on the exit code of the popup function, which will tell you what button was pressed, for example Yes, No, Ok, Cancel, Abort, Retry etc.  Then you could perform different activities in the task sequence based on the value of the variable.