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 🙂

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.