Monitoring Web Server Performance with Windows Presentation Foundation and ScottPlot

October 08, 2020
Written by
Jeff Rosenthal
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

wpf-scottplot (1).png

Windows Presentation Foundation (WPF) framework provides a rich graphical user interface for desktop applications. Charts are a familiar component in many applications, providing users with a visual way to consume data, observe trends and patterns, and provide a more attractive and engaging application.

Many libraries provide various charting capabilities. In selecting a library to use, you should consider many factors. Foremost is compliance and compatibility with the framework used to implement the project.

If you’re building a new .NET project these days, it’s likely you’ll be using .NET Core, so the charting library you select has to be compatible. Since .NET Core is relatively new, compared to the .NET Framework, there are fewer libraries available. 

Some charting packages are big, feature-rich, time-consuming to learn to use, and expensive. In many large applications, these tools provide continuity between components while also providing a high degree of styling, which is very desirable. The application you will build in this tutorial requires a chart to plot the connection speed to a website as presented in this project; getting it working with minimal overhead is the objective.

ScottPlot is easy to implement and use, and its compactness makes it a great choice for many real-time data acquisition projects. The size of the library (141.46 KB), installation, and functionality are all developer-friendly. It is MIT-licensed and only contains MIT-licensed components, so it can be used in commercial applications without requiring attribution, permission, or opening your source code.

This tutorial will guide you through building a WPF application that uses a bar chart to display continuously updated data. You’ll see how to design the user interface and how to link the UI to the stream of data from an external data source.

Prerequisites

You’ll need the following tools and resources to build and run this project:

Windows 10 – It puts Windows in the Windows Presentation Foundation.

.NET Core SDK 3.1 – The SDK includes the APIs, runtime, and CLI.

Visual Studio 2019 with the following workloads and individual components:

  • .NET desktop development workload (Includes C#)
  • GitHub Extension for Visual Studio (If you want to clone the companion repository.)

You should have a general knowledge of Visual Studio; you will be adding and editing files, and running and debugging code. Familiarity with C#, including interfaces, callbacks, and events, will be helpful.

There is a companion repository for this post available on GitHub. It contains the complete source code for the tutorial project.

Creating the project

Begin this tutorial by creating a WPF App (.NET Core) project for C# named WpfPingMonitor. You can put the solution and project folders wherever it’s most convenient for you.

The data you will be plotting in this project will be the amount of time it takes for your computer to ping google.com, referred to as the ping round trip or latency. This technique is often used in applications to determine if a website is reachable (by specifying a different URL instead of google.com) or if the computer is maintaining internet connectivity.

Add the NuGet package, ScottPlot.WPF, to add the charting capabilities for this project. You can do this by opening the Package Manager Console in Visual Studio and execute the following command-line instruction:

install-package scottplot.WPF -Version 4.0.42

You can, of course, also use the VS 2019 user interface or the .NET CLI.

Note: The author of ScottPlot, Scott Harden, suggests specifying the version number. ScottPlot is under active development and future versions associated may implement different method signatures. Selecting version 4.0 will ensure the application code provided in this tutorial will always work.

Adding the data model

Add a class file and name it PingResult.cs. Replace the entire contents with the following C# code:

using System;

namespace WpfPingMonitor
{
    public class PingResult
    {
        public double Latency { get; set; }
        public DateTime Timestamp { get; set; }
        public double OADate { get => Timestamp.ToOADate(); }
        public bool Success { get; set; } = false;
    }
}

This block of code defines a class that will store the data points.

  • Latency is the ping’s roundtrip time.
  • Timestamp is the time the data point is created.
  • OADate is a double representation of a timestamp. The ScottPlot chart component uses double-precision floating point values to represent dates, the old OLE Automation date format. This is a helper property to speed up operations.
  • Success is a boolean signifying the success or failure of the ping attempt.There are times when the Ping.Send method will time-out or fail. This flag will only be true if that method returns data.

Create a class file named Pinger.cs in the project root. This class is the view-model for the plot’s data. It will also be responsible for periodically pinging google.com and saving the returned data.

Replace the template-generated code in the new file with the following:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.CompilerServices;
using System.Windows.Threading;

namespace WpfPingMonitor
{
    public class Pinger : INotifyPropertyChanged
    {
        #region Fields
        const string HostNameOrAddress = "google.com";
        public ObservableCollection<PingResult> PingResults { get; set; } = new ObservableCollection<PingResult>();
        
        private const int timeout = 250;
        
        private int _missedcount;
        private int _datapointCount;
        private int _maxpoints;
        private double _average;
        private double _maxPing;
        private double _minPing;


        #endregion

        #region Properties
        public int DatapointCount
        {
            get => _datapointCount;
            set
            {
                _datapointCount = value;
                OnPropertyChanged();
            }
        }

        public int Maxpoints
        {
            get => _maxpoints;
            set
            {

                _maxpoints = value;
                OnPropertyChanged();
            }
        }
        public int MissedCount
        {
            get => _missedcount;
            set
            {

                _missedcount = value;
                OnPropertyChanged();
            }
        }

        public double Average
        {
            get => Math.Round(_average, 1);
            set
            {
                _average = value;
                OnPropertyChanged();
            }
        }
        public double MaxPing
        {
            get => _maxPing;
            set
            {
                _maxPing = value;
                OnPropertyChanged();
            }
        }
        public double MinPing
        {
            get => _minPing;
            set
            {
                _minPing = value;
                OnPropertyChanged();
            }
        }

        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        public Pinger()
        {
            Maxpoints = 60;
            MaxPing = 0;
            MinPing = timeout;

            DispatcherTimer timer = new DispatcherTimer
            {
                Interval = TimeSpan.FromSeconds(3),
                IsEnabled = true
            };

            PingNow();
            timer.Tick += (sender, args) => PingNow();
        }

        private void PingNow()
        {
            using var pingSender = new Ping();
            PingReply reply = pingSender.Send(HostNameOrAddress, timeout);

            var result = new PingResult
            {
                Timestamp = DateTime.Now,
                Latency = reply.RoundtripTime,
                Success = reply?.Status == IPStatus.Success
            };

            PingResults.Add(result);
            DatapointCount = PingResults.Count;
            Average = PingResults.Average(x => x.Latency);
            if (result.Latency < MinPing && result.Success)
            {
                MinPing = result.Latency;
            }
            if (result.Latency > MaxPing)
            {
                MaxPing = result.Latency;
            }
            if (!result.Success)
            {
                MissedCount++;
            }
        }

        #region Event Handler
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

The HostNameOrAddress field holds the URL that the application will check. The constant is set to google.com but can be any IPAddress class or URL string you are interested in monitoring.

The PingResults ObservableCollection<T> holds the collection of PingResult objects, one for each ping data point. This type of collection provides binding to the user interface and enables the UI to be updated when the data changes by implementing the INotifyCollectionChanged interface, which exposes the CollectionChanged event.

Inside the Pinger constructor, a DispatchTimer is constructed and initialized. On the event Tick, PingNow() is called to add a new data point to the PingResults collection.

Note that PingNow() also gets called from the constructor to add a data point to the collection. The rationale here is that the method is deferred for the timer’s interval. At the same time, the subsequent call will be invoked immediately, populating the collection with at least one point of data. This is a user experience issue, making the chart show data immediately upon startup instead of waiting for the timer interval to lapse. Users tend to be impatient. 😉

The PingNow() method is the heavy lifter here, pinging the remote website or URL and returning the amount of time it took. There is a possibility that the ping will not return in an appropriate amount of time. In those cases, the method marks the data point as unsuccessful. After the data point is collected, the variables holding the Average, MinPing, MaxPing, DatapointCount, and MissedCount are updated appropriately.

The event handler, PropertyChangedEventHandler, supports the  INotifyPropertyChanged interface that the Pinger class implements. This is required by WPF to implement data binding. It is the mechanism that informs the UIElements when changes to data occur. 

Implementing the presentation

With the data in place, the user interface  for the data can be defined. Open MainWindow.xaml and replace the boilerplate code with the following:

<Window x:Class="WpfPingMonitor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfPingMonitor"
        mc:Ignorable="d"
        Title="MyPinger" Height="500" Width="900">
    <DockPanel>
        <StackPanel Orientation="Vertical"  DockPanel.Dock="Left" TextElement.FontSize="16">
            <StackPanel Background="AntiqueWhite" Margin="10">
                <Label>Datapoints</Label>
                <TextBlock Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding DatapointCount}"></TextBlock>
            </StackPanel>

            <StackPanel Background="AntiqueWhite" Margin="10">
                <Label>Max Points</Label>
                <TextBox Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding Maxpoints, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged }"></TextBox>
            </StackPanel>

            <StackPanel Background='LawnGreen' Margin="10">
                <Label>Max</Label>
                <TextBlock Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding MaxPing}"></TextBlock>
            </StackPanel>

            <StackPanel Background="AntiqueWhite" Margin="10">
                <Label>Average</Label>
                <TextBlock Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding Average}"></TextBlock>
            </StackPanel>

            <StackPanel Background="Pink" Margin="10">
                <Label>Min</Label>
                <TextBlock Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding MinPing}"></TextBlock>
            </StackPanel>
            <StackPanel Background="IndianRed" Margin="10">
                <Label>Missed</Label>
                <TextBlock Width="50" Margin="20,0" VerticalAlignment="Center" Text="{Binding MissedCount}"></TextBlock>
            </StackPanel>
        </StackPanel>
        <WpfPlot Name="wpfPlot1"/>
    </DockPanel>
</Window>

The top-level container implemented is a DockPanel. Within the DockPanel, a StackPanel is nested to the left using the DockPanel.Dock="Left" attribute. 

Within the top-level StackPanel, other StackPanels present metric data. Each includes a label and a TextBlock to show read-only data.

The MaxPoints user interface element is an editable TextBox that accepts user input to specify the number of data points to display.

Also within the top-level DockPanel is the WpfPlot element, which is the ScottPlot chart component.

To get the application running, the MainWindow class needs an instance of the Pinger class, and a timer to update the chart. The PlotNow() method will be called every second. Its job is to gather the data and formulate the chart.

Open MainWindow.xaml.cs and replace the existing code with the following:

using System;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Threading;

namespace WpfPingMonitor
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        readonly Pinger myPinger = new Pinger();
        private DateTime lastUpdate;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = myPinger;
            
            DispatcherTimer plotTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
            plotTimer.Tick += PlotNow;
            plotTimer.Start();

        }
        void PlotNow(object sender, EventArgs e)
        {
            PingResult[] successfulPings = myPinger.PingResults.Where(x => x.Success).ToArray();

            // only update the plot if new data is available
            if (successfulPings.Length == 0)
                return;
            if (successfulPings.Last().Timestamp == lastUpdate)
                return;
            lastUpdate = myPinger.PingResults.Last().Timestamp;

            // clear any old data that may have been plotted
            wpfPlot1.plt.Clear();

            // transform data to double arrays and plot it as a scatter plot
            double[] xs = successfulPings.Select(x => x.OADate).TakeLast(myPinger.Maxpoints).ToArray();
            double[] ys = successfulPings.Select(x => x.Latency).TakeLast(myPinger.Maxpoints).ToArray();
            
            //Set the bar width
            double barWidth = .5 / (24 * 60 * 60); // units are fraction of a day
            //Initialize the plot
            wpfPlot1.plt.PlotBar(xs, ys, barWidth: barWidth);

            // decorate the plot, and disable the mouse since axis limits are set manually
            wpfPlot1.plt.Title("Web Server Latency");
            wpfPlot1.plt.YLabel("Latency (ms)");

            //Lock the Y axis to values of 0 to 100
            wpfPlot1.plt.Axis(y1: 0, y2: 100);
            wpfPlot1.Configure(enablePanning: false, enableRightClickZoom: false);

            // indicate horizontal values are DateTime and add padding to accomodate large tick labels
            wpfPlot1.plt.Ticks(dateTimeX: true);
            wpfPlot1.plt.Layout(y2LabelWidth: 40);

            // the programmer is in control of when the plot is rendered
            wpfPlot1.Render();
        }
        
    }
}

When the application launches a timer starts and calls PlotNow() every second. PlotNow() generates and updates the plot’s data. The data points to be plotted are extracted from the collection myPinger using a LINQ expression that converts the collection results to an array. If there is no new data, the method simply returns. When new data is ready to be presented, it is placed into arrays for each axis and passed to the plot object. The y-axis is confined to a scale of 0 to 100 to prevent the data from bouncing around if the setting is left at the default: autoscale.

With that in place, the application is ready to compile and run.

Testing the completed application

Before beginning, verify that the URL is pingable from the command line. Open a PowerShell or Windows Command console window and execute the following command-line instruction:

ping google.com

If the ping command responds with a “Request timed out.” message four times in a row, the application won’t be able to present any data. The most likely cause is that your Windows Security firewall settings have not been configured to allow ping requests. To make the required change to the File and Printer Sharing (Echo Request - ICMPv4-In) setting, follow these simple instructions provided by Indiana University. (Go Hoosiers!)

Other factors may impede the ping utility from working, including anti-virus software and  internet service provider rules. But start with the firewall before investigating other causes.

Build and run the application. As soon as the application starts, a data point will present. It represents the speed in milliseconds it took for the signal to hit the URL (google.com) and return. Your values will vary depending on your specific internet configuration. Here is an example of the application running. Changing the value is Max Points will change the maximum number of bars presented in the chart.

The fastest, slowest, and average ping speed and the total number of data points and missed data points will update with each data point.

Screen shot of the application running

Potential enhancements

There are many ways to enhance this project. Consider storing the data in a database to retain data. If you want to be notified of the loss of connection to a website or internet, sounding an alarm might make sense; the data collection process could run as a service in the background andhe presentation portion would simply reflect the data in the database. Consider using Twilio Programmable SMS to be notified of a loss of connection.

Summary

In this post you saw how you can easily add real time interactive data charts to WPF applications using WPF data binding and the ScottPlot NuGet package for WPF. You saw how to ping a website and plot the ping response time using ScottPlot, and how to display data about the monitoring process using XAML layout in WPF. You also saw the implementation of the ScottPlot.WPF library, which tackles the idea of charting slightly differently and offers a nice tool to have in your arsenal.

The charting capabilities of WPF can make presentation of data more attractive, comprehensive, and dynamic. Trends and patterns are easy to see in charts. Line charts, bar charts, area charts, and even pie charts are possible. Let your imagination and creativity soar.

Additional resources

The following resources will enable you to explore the technologies discussed in this post:

ScottPlot – A general description of the charting library.

ScottPlot Cookbook – An extensive collection of ways ScottPlot can be coded and implemented.

ScottPlot Demo – A demonstration of the ScottPlot in action.

Using Twilio Lookup in .NET Core WPF Applications – Learn how to verify phone numbers and obtain caller information with the WPF.

TwilioQuest – If you’d like to learn more about programming C# and other languages, try this action-adventure game inspired by the 16-bit golden era of computer gaming.

Jeffrey Rosenthal is a C/C++/C# developer and enjoys the architectural aspects of coding and software development. Jeff is an MCSD and has operated his own company, The Coding Pit, since 2008. When not coding, Jeff enjoys his home projects, rescuing dogs, and flying his drone. Jeff is available for consulting on various technologies and can be reached via email, Twitter, or LinkedIn.