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

Create a WPF Application with Navigation using PowerShell

In WPF, there are two types of window which can host your application content.  The most commonly used is simply the window (System.Windows.Window in .Net).  But there is another window type, the navigation window (System.Windows.Navigation.NavigationWindow) which takes a slightly different form and includes some basic navigation controls similar to what you would use in a web browser.

capture

This kind of window can be useful in certain situations such as hosting documentation or help content, or recording input over several pages, as the user can go back and forward between the content pages using the navigation history.

The key to making the navigation window work is to use a page control (System.Windows.Control.Page) to host your content.  Like the window, a page can only contain a single child control, so you need to use a layout control like a grid to contain other window elements, such as textboxes and buttons etc.  You can then simply call the relevant page when you navigate.

Below is a simple example of a navigation window which has two actual pages, and the third “page” is a website.  The window itself and the pages are defined in XAML, stored to a hash table, then called by simply setting the Content property of the navigation window. In the case of the web page, we use the navigation service to load the page.

When the window is opened, the first page is displayed:

capture

Clicking next loads the second page:

capture2

 

Clicking next again loads the web page:

capture3

You’ll notice that after you leave the first page the navigation history is enabled and you can go back to the previous pages.

PowerShell Navigation Window


Add-Type -AssemblyName PresentationFramework

# Define Navigation Window in XAML
[XML]$Xaml = @"
<NavigationWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Name = "NavWindow" Width = "600" Height = "400" WindowStartupLocation = "CenterScreen" ResizeMode = "CanMinimize" >
</NavigationWindow>
"@

# Define Page 1 text
$Page1_Text1 = @"
Amazon has confirmed its virtual assistant Echo speakers are coming to the UK, Germany and Austria.
Until now, the company sold its voice-controlled devices only in the US.
"@
$Page1_Text2 = @"
The machines can answer questions, control other internet-connected devices, build shopping lists and link in to dozens of third-party services including Spotify, Uber and BBC News.
Experts say they appeal to early adopters' sense of curiosity but tend to be a harder sell to others.
"@

# Define Page 2 text
$Page2_Text1 = @"
The company set up to manufacture Europe's next-generation rocket - the Ariane 6 - says it is open to orders.
Airbus Safran Launchers (ASL) expects to introduce the new vehicle in 2020.
"@
$Page2_Text2 = @"
This week, member states of the European Space Agency gave their final nod to the project following an extensive review process.
The assessment confirmed the design and performance of the proposed rocket, and the development schedule that will bring it into service.
"@

# Define Page 1 in XAML
[XML]$Page1 = @"
<Page      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     x:Name = "Page1" Title="Amazon Echo"     >
    <Grid>
        <StackPanel>
        <TextBlock Padding = "10" TextWrapping = "Wrap" FontFamily = "Segui" FontSize = "18" Height = "Auto" Width = "Auto">
            $Page1_Text1 <LineBreak /><LineBreak />
            $Page1_Text2 <LineBreak />
        </TextBlock>
        <ListBox BorderThickness = "0" Width = "100" HorizontalAlignment = "Left" FontSize = "16">
            <ListBoxItem Content = "That's great!" Foreground = "Green" />
            <ListBoxItem Content = "I hate it!" Foreground = "Red" />
        </ListBox>
        </StackPanel>
        <DockPanel Margin = "480,280,0,0">
            <Button x:Name = "P1_Next" DockPanel.Dock = "Right" Content = "Next" Height = "30" Width = "50" />
        </DockPanel>
    </Grid>
</Page>
"@

# Define Page 2 in XAML
[XML]$Page2 = @"
<Page      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     x:Name = "Page2" Title="Ariane 6"     >
    <Grid>
        <StackPanel>
        <TextBlock Padding = "10" TextWrapping = "Wrap" FontFamily = "Segui" FontSize = "18" Height = "Auto" Width = "Auto">
            $Page2_Text1 <LineBreak /><LineBreak />
            $Page2_Text2 <LineBreak />
        </TextBlock>
        <ListBox BorderThickness = "0" Width = "100" HorizontalAlignment = "Left" FontSize = "16">
            <ListBoxItem Content = "That's great!" Foreground = "Green" />
            <ListBoxItem Content = "I hate it!" Foreground = "Red" />
        </ListBox>
        </StackPanel>
        <DockPanel Margin = "480,280,0,0">
            <Button x:Name = "P2_Next" DockPanel.Dock = "Right" Content = "Next" Height = "30" Width = "50" />
        </DockPanel>
    </Grid>
</Page>
"@

# Create hash table, add WPF named elements and objects
$hash = @{}
$Hash.NavWindow = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $xaml))
$Hash.Page1 = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $Page1))
$Hash.Page2 = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $Page2))

$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object -Process {
    $hash.$($_.Name) = $hash.NavWindow.FindName($_.Name)
    }
$Page1.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object -Process {
    $hash.$($_.Name) = $hash.Page1.FindName($_.Name)
    }
$Page2.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object -Process {
    $hash.$($_.Name) = $hash.Page2.FindName($_.Name)
    }

# Add code to handle the button click events, loading the next page
$Hash.P1_Next.Add_Click({
    $hash.NavWindow.Content = $Hash.Page2
    $Hash.NavWindow.Title = "Clear path to Ariane 6 rocket introduction"
})

$Hash.P2_Next.Add_Click({
    $Hash.NavWindow.Navigate([URI]"http://www.bbc.com")
    $Hash.NavWindow.Title = "BBC Website"
})

# Set the window content to the first page on load
$hash.NavWindow.Content = $Hash.Page1
$Hash.NavWindow.Title = "Amazon's Echo speakers head to UK and Germany"

# Show the NavWindow
$null = $Hash.NavWindow.Dispatcher.InvokeAsync{$Hash.NavWindow.ShowDialog()}.Wait()

Forcing a ConfigMgr Client to Send a New CCMEval Report

In order to maintain a healthy ConfigMgr environment, it is important to know that your clients have successfully run the Configuration Manager Health Evaluation task and reported the results to the Site server.  Sometimes you will find a number of systems that have not reported any health status to the Site server.  In the Devices node of the ConfigMgr Console, you will find “No Results” for the Client Check Result, and the Client Check Detail tab displays nothing, even though the system may be active.

capture

capture

To identify the list of active systems that either have not reported health evaluation results, or have failed the evaluation, I use the following SQL query:


select
sys.Name0 as 'Computer Name',
sys.User_Name0 as 'User Name',
summ.ClientStateDescription,
case when summ.ClientActiveStatus = 0 then 'Inactive'
 when summ.ClientActiveStatus = 1 then 'Active'
 end as 'ClientActiveStatus',
summ.LastActiveTime,
case when summ.IsActiveDDR = 0 then 'Inactive'
 when summ.IsActiveDDR = 1 then 'Active'
 end as 'IsActiveDDR',
case when summ.IsActiveHW = 0 then 'Inactive'
 when summ.IsActiveHW = 1 then 'Active'
 end as 'IsActiveHW',
case when summ.IsActiveSW = 0 then 'Inactive'
 when summ.IsActiveSW = 1 then 'Active'
 end as 'IsActiveSW',
case when summ.ISActivePolicyRequest = 0 then 'Inactive'
 when summ.ISActivePolicyRequest = 1 then 'Active'
 end as 'ISActivePolicyRequest',
case when summ.IsActiveStatusMessages = 0 then 'Inactive'
 when summ.IsActiveStatusMessages = 1 then 'Active'
 end as 'IsActiveStatusMessages',
summ.LastOnline,
summ.LastDDR,
summ.LastHW,
summ.LastSW,
summ.LastPolicyRequest,
summ.LastStatusMessage,
summ.LastHealthEvaluation,
case when LastHealthEvaluationResult = 1 then 'Not Yet Evaluated'
 when LastHealthEvaluationResult = 2 then 'Not Applicable'
 when LastHealthEvaluationResult = 3 then 'Evaluation Failed'
 when LastHealthEvaluationResult = 4 then 'Evaluated Remediated Failed'
 when LastHealthEvaluationResult = 5 then 'Not Evaluated Dependency Failed'
 when LastHealthEvaluationResult = 6 then 'Evaluated Remediated Succeeded'
 when LastHealthEvaluationResult = 7 then 'Evaluation Succeeded'
 end as 'Last Health Evaluation Result',
case when LastEvaluationHealthy = 1 then 'Pass'
 when LastEvaluationHealthy = 2 then 'Fail'
 when LastEvaluationHealthy = 3 then 'Unknown'
 end as 'Last Evaluation Healthy',
case when summ.ClientRemediationSuccess = 1 then 'Pass'
 when summ.ClientRemediationSuccess = 2 then 'Fail'
 else ''
 end as 'ClientRemediationSuccess',
summ.ExpectedNextPolicyRequest
from v_CH_ClientSummary summ
inner join v_R_System sys on summ.ResourceID = sys.ResourceID
where summ.LastEvaluationHealthy in (2,3)
and summ.ClientActiveStatus = 1
order by summ.LastActiveTime Desc

In most cases where the evaluation status reports “Unknown” by this query, you will find that the client has actually run the health evaluation task, it just hasn’t reported the results to the management point for some reason.  I published a PowerShell script previously that lets you view the current health evaluation status on any remote computer by reading the CCMEvalReport.xml file – you can find the script here.

For these “Unknown” status systems, however, I want to force the client to send a health evaluation report to its management point, so I prepared the following PowerShell script to do that.  It can run either against the local computer, or a remote computer.  Admin rights are required on the target system, and if running against the local computer the script must be run as administrator.

The script simply sets the SendAlways flag for CCMEval reports in the registry to “TRUE”, triggers the CM Health Evaluation task to run, waits for it to finish, then changes the SendAlways flag back to “FALSE”.  When the CCMEval program runs with the SendAlways flag set, it will always send the report to the management point even if the client health status has not changed since the last report.

You can verify that from the CcmEval.log on the client:

capture

Within a few minutes you should find that the status for that system has been updated in the ConfigMgr Console with the health evaluation results.

To run the script against the local machine, run PowerShell as administrator and simply do:


Send-CCMEvalReport

To run against a remote computer:


Send-CCMEvalReport -ComputerName PC001

The script also supports verbose output:

Send-CCMEvalReport -ComputerName PC001 -Verbose

capture

Here’s the full code:

Send-CCMEvalReport.ps1


[CmdletBinding()]
    param(
        [Parameter(Mandatory=$False)]
        [String]$ComputerName = $env:COMPUTERNAME
        )

# Code to set 'SendAlways' in registry
$SendAlways = {
    Param($Value)
    $Path = "HKLM:\Software\Microsoft\CCM\CcmEval"

    Try
    {
        $null = New-ItemProperty -Path $Path -Name 'SendAlways' -Value $Value -Force -ErrorAction Stop
    }
    Catch
    {
        $_
    }
}

# Run against local computer
If ($ComputerName -eq $env:COMPUTERNAME)
{
    If (!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
    {
        Write-Warning "This cmdlet must be run as administrator against the local machine!"
        Return
    }

    Write-Verbose "Enabling 'SendAlways' in registry"
    $Result = Invoke-Command -ArgumentList "TRUE" -ScriptBlock $SendAlways 

    If (!$Result.Exception)
    {
        Write-Verbose "Triggering CM Health Evaluation task"
        Invoke-Command -ScriptBlock { schtasks /Run /TN "Microsoft\Configuration Manager\Configuration Manager Health Evaluation" /I }

        Write-Verbose "Waiting for ccmeval to finish"
        do {} while (Get-process -Name ccmeval -ErrorAction SilentlyContinue)

        Write-Verbose "Disabling 'SendAlways' in registry"
        Invoke-Command -ArgumentList "FALSE" -ScriptBlock $SendAlways
    }
    Else
    {
        Write-Error $($Result.Exception.Message)
    }
}
# Run against remote computer
Else
{
    Write-Verbose "Enabling 'SendAlways' in registry"
    $Result = Invoke-Command -ComputerName $ComputerName -ArgumentList "TRUE" -ScriptBlock $SendAlways

    If (!$Result.Exception)
    {
        Write-Verbose "Triggering CM Health Evaluation task"
        Invoke-Command -ComputerName $ComputerName -ScriptBlock { schtasks /Run /TN "Microsoft\Configuration Manager\Configuration Manager Health Evaluation" /I }

        Write-Verbose "Waiting for ccmeval to finish"
        do {} while (Get-process -ComputerName $ComputerName -Name ccmeval -ErrorAction SilentlyContinue)

        Write-Verbose "Disabling 'SendAlways' in registry"
        Invoke-Command -ComputerName $ComputerName -ArgumentList "FALSE" -ScriptBlock $SendAlways
    }
    Else
    {
        Write-Error $($Result.Exception.Message)
    }
}