Creating a CefSharp Application: End-to-End


From Developing the App to Writing an Installer

It is widely known that the HTML, CSS and Javascript combo is the very best way to create application front ends across all ranges of computers and all ranges of applications. The thing is, some of these "frameworks" for writing amazing cross platform apps are quite daunting to get started with.

Our major benefit in using CefSharp, is that we can pair it with a friendly and welcoming framework for writing desktop Apps - WinForms. Most, if not all C# developers have made use of this easy yet powerful framework for writing Apps, but let's be honest - it's old and it looks the part.

Your work here won't be cross-platform, but it'll be modern and meaningful as these concepts will be carried over when you move on to the cross-platform giants like Apache Cordova and Electron. This goes well and I will write a guide for them as well.

Requirements

  • Basic HTML, CSS, JS
  • C# Winforms Experience
  • Markup syntax

This guide will show you just about everything you need to know to write a basic application - and deploy it!

What does the App do?

Well, I ran into some trouble with my ISP where the connection was intermittently dropping, and I couldn't find an App that would just log my downtime - for free.

So, that is what this application will do - in no glamourous way whatsoever.

Contents


1. Technologies:

  • WinForms .Net Framework 4.5.2 - backbone functionality.
  • CefSharp - For pretty front ends
  • We'll be using bootstrap just because it's easy (Front end frameworks aren't really the focus here)
  • Font Awesome will also be used
  • WIX - For reliable and easy installers
  • BrighstarDB as a datastore

I am using Visual Studio 2017 Community for this guide - however I believe it will be much of the same back until 2013 versions.


2. Project Setup

2.1 Creating The Project

Simple enough - let's create a new WinForms application from Visual Studio:

  1. File >
  2. New >
  3. Project >
  4. Windows Forms App (.Net Framework)

I'm calling my project file "Nonitor".

Lets be sure our Target Framework is correct, we have the Visual C++ Redistributable 2015 installed, and we're building explicitly for x86 or x64:

  1. Right Click Project >
  2. Properties >
  3. Target Framework 4.5.2

Alt Text

Installing the required framework does not fall within the scope of this article

Go here for the VCredist2015, the 2013 version is good as well.

The following is for setting the build target explicitly (I will be targeting 64-bit machines):

  1. Right Click Solution >
  2. Properties >
  3. Configuration Manager >
  4. Platform Dropdown >
  5. Select x64 or x86 explicitly from the list

alt text

Go ahead and run it - works like a charm? Good.

2.2 Adding Dependencies

Open up your project's Nuget manager, we will be installing the following packages:

  • BrightstarDB - take the latest stable version (My version 1.13.3
  • It's going to install a couple of other dependencies as well
  • cefsharp.Winforms - again, the latest stable (57.0.0)

We've come this far - run your app again to see everything is still tight.

2.3 Getting Rid of some kinks

Quickly add the following code to your Form1 (don't bother renaming we will be getting rid of form 1):

 partial class Form1 : Form
{
    private ChromiumWebBrowser _browser;

    public Form1()
    {
        InitializeComponent();
        initBrowser();
    }

    public void initBrowser()
    {
        Cef.Initialize();

        _browser = new ChromiumWebBrowser("https://www.google.com");
        _browser.Dock = DockStyle.Fill;
        this.Controls.Add(_browser);

    }
}

Now, this will open up the form with a window to Google - but on some machines the display will be all skewed:

alt text

This is because of DPI scaling - and the following will fix it:

 Program.cs -> void main()

[STAThread]
static void Main()
{
  Cef.EnableHighDPISupport();
  Cef.Initialize(new CefSettings() { });

  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  Application.Run(new Form1());
};

As far as setting up the general environment is concerned, we're done.


3. File Structure

We want to split up our coding modules into a couple of different folders (namespaces) so we can keep proper track of them.

Let's add a couple of folders that we'll need to the root directory: Data Code Forms Resources * Note: To have a valid and secure application - we don't want to be getting the Html resources from the file structure as this is ridiculously insecure in many ways. (They can be deleted, edited, moved etc. all of which will cause breaking changes to our app) In this folder we'll map out which html resources need to be embedded into our application.


4. Embedding Resources

4.1 Place Files and set to Embedded Resource

The method I use to serve the HTML resources directly from memory will be discussed in this section.

The first step is to go get the resources from the respective web sites:

Note: If you have the know how from web development, use Bower and Gulp to get the .sass files and customize the look and feel at your leisure.

Extract these files somewhere on your machine and place only the files you need in the resources folder (I place my unnecessary files inside the project in a file called unembedded):

Now copy the needed files to the Resources folder and set the build action to Embedded Resource:

alt text

4.2 Static GetBrowser Method With Access to our embedded resources

The most important aspects to doing this right:

  • pay special attention to your libraries' dependencies (JQuery and Tether before bootstrap in this case),
  • and pay attention to the file structure, we must reference Font Awesome inside a "css" folder and we must reference the font files inside a "fonts" folder.

Let's add an icon to the "Resources" folder as well. I generated one from here

Now lets create our "GetBrowser" method, remember the goal here is to create a browser that can reference our js and css libraries:

  1. Create a new static class "NonitorStaticMethods" inside the code folder.
  2. Add the following using statements:

 CefSharp.WinForms;
using System.IO;
using System.Reflection;
using CefSharp;

  1. Create the following methods:

 class NonitorStaticMethods
    {
        private static Assembly _assembly = Assembly.GetExecutingAssembly();
        public static ChromiumWebBrowser GetBrowser()
        {
            var result = new ChromiumWebBrowser("http://rendering");
            result.RegisterResourceHandler("http://rendering/css/bootstrap.min.css", GetStreamResource("Nonitor.Resources.bootstrap.min.css"), "text/css");
            result.RegisterResourceHandler("http://rendering/js/bootstrap.min.js", GetStreamResource("Nonitor.Resources.bootstrap.min.js"), "text/javascript");
            result.RegisterResourceHandler("http://rendering/js/Chart.min.js", GetStreamResource("Nonitor.Resources.Chart.min.js"), "text/javascript");
            result.RegisterResourceHandler("http://rendering/js/flat-l.js", GetStreamResource("Nonitor.Resources.flat-l.js"), "text/javascript");
            result.RegisterResourceHandler("http://rendering/js/jquery-3.2.1.js", GetStreamResource("Nonitor.Resources.jquery-3.2.1.js"), "text/javascript");
            result.RegisterResourceHandler("http://rendering/js/tether.min.js", GetStreamResource("Nonitor.Resources.tether.min.js"), "text/javascript");
            result.RegisterResourceHandler("http://rendering/css/tether.min.css", GetStreamResource("Nonitor.Resources.tether.min.css"), "text/css");
            result.RegisterResourceHandler("http://rendering/css/font-awesome.min.css", GetStreamResource("Nonitor.Resources.font-awesome.min.css"), "text/css");

            result.RegisterResourceHandler("http://rendering/fonts/fontawesome-webfont.eot", GetStreamResource("Nonitor.Resources.fontawesome-webfont.eot"), "font/openfont");
            result.RegisterResourceHandler("http://rendering/fonts/fontawesome-webfont.svg", GetStreamResource("Nonitor.Resources.fontawesome-webfont.svg"), "font/openfont");
            result.RegisterResourceHandler("http://rendering/fonts/fontawesome-webfont.ttf", GetStreamResource("Nonitor.Resources.fontawesome-webfont.ttf"), "font/openfont");
            result.RegisterResourceHandler("http://rendering/fonts/fontawesome-webfont.woff", GetStreamResource("Nonitor.Resources.fontawesome-webfont.woff"), "font/openfont");
            result.RegisterResourceHandler("http://rendering/fonts/fontawesome-webfont.woff2", GetStreamResource("Nonitor.Resources.fontawesome-webfont.woff2"), "font/openfont");
            result.RegisterResourceHandler("http://rendering/fonts/FontAwesome.otf", GetStreamResource("Nonitor.Resources.FontAwesome.otf"), "font/openfont");

            return result;
        }

        public static Stream GetStreamResource(string name)
        {
            return _assembly.GetManifestResourceStream(name);
        }

        public static string GetStringResource(string name)
        {
            string text = string.Empty;
            using (var reader = new StreamReader(_assembly.GetManifestResourceStream(name)))
            {
                text = reader.ReadToEnd();
            }
            return text;
        }
    }

This will allow us to instantiate a browser on any form that has all our embedded resources ready and available.

4.3 A Quick Test (Methodology for utilizing HTML resources)

Note Section 4.3 is a test to see that your resources are ready and available when you need them.

Lets embed the following html:

 html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" content="width=device-width, initial-scale=1.0" />
    <title></title>
    <link href="http://rendering/css/tether.min.css" rel="stylesheet" />
    <link href="http://rendering/css/bootstrap.min.css" rel="stylesheet" />
    <link href="http://rendering/css/font-awesome.min.css" rel="stylesheet" />
</head>
<body>

    <div class="container">
        <div class="col-10 offset-1">
            <button id="clicker" onclick="flatL.start({container: document.getElementById('test1'), flatColor:'#f49ac0', beatColor:'green'})" data-toggle="popover" title="TestPopover" data-trigger="click" data-placement="bottom" data-container="body" class="btn btn-secondary">
                TestButton
            </button>
            <span class="fa fa-spin fa-cog fa-5x"></span>
        </div>
        <div class="col-10 offset-1">
            <canvas id="myChart" width="400" height="400"></canvas>
        </div>
        <div id="test1" style="display:block;width:100%;height:200px;">
        </div>
    </div>
    <script src="http://rendering/js/jquery-3.2.1.min.js"></script>
    <script src="http://rendering/js/tether.min.js"></script>
    <script src="http://rendering/js/bootstrap.min.js"></script>
    <script src="http://rendering/js/Chart.min.js"></script>
    <script src="http://rendering/js/flat-l.js"></script>
    <script>
        $("#clicker").popover();
        var ctx = document.getElementById("myChart").getContext('2d');
        var myChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                datasets: [{
                    label: '# of Votes',
                    data: [12, 19, 3, 5, 2, 3],
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    borderColor: [
                        'rgba(255,99,132,1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)',
                        'rgba(153, 102, 255, 1)',
                        'rgba(255, 159, 64, 1)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero: true
                        }
                    }]
                }
            }
        });
    </script>

</body>
</html>
And call it in form1 like so:

 void initBrowser()
        {
            string html = Code.NonitorStaticMethods.GetStringResource("Nonitor.Resources.testpage.html");

            _browser = Code.NonitorStaticMethods.GetBrowser();
            _browser.IsBrowserInitializedChanged += _browser_IsBrowserInitializedChanged;
            _browser.Dock = DockStyle.Fill;
            this.Controls.Add(_browser);
            _browser.LoadHtml(html, "http://rendering", Encoding.UTF8);
        }

        private void _browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e)
        {
            _browser.ShowDevTools();
        }

Note that _browser.ShowDevTools() line - this is so we can debug what is happening on the actual html page. To be taken out when we're ready to deploy

You should see the following:

alt text

Also the test button should give us an EKG graph.

Working? Awesome...


5. Performing Net Tests; Storing the Data;

5.1 Data Setup

Check this out as a reference for setting up your entity context.

The structure for the data is the following (In the data folder as ITest.cs and IFailure.cs respectively):

public interface IFailure
{
    int FailureKey { get; set; }
    DateTime FailureStart { get; set; }

    bool FailureOpen { get; set; }

    DateTime FailureEnd { get; set; }

    [InverseProperty("Failures")]
    ICollection<ITest> Test { get; }
}

public interface ITest
{
    int TestKey { get; set; }
    DateTime TestStart { get; set; }
    DateTime TestEnd { get; set; }

    bool TestOpen { get; set; }

    ICollection<IFailure> Failures { get; set; }
}

Copy the MyEntityContext.tt (Created when you install the brighstarDB library from nuget - reinstall if any issues) file into the "Data" directory and rename to NonitorEntityContext. Right click and run the tool to generate your context.

5.2 Setting Up a Data Layer

The goal here is to create a class that can handle all of our data needs: CRUD Operations Getting the data in a useable format

We will create a class NonitorData within the Data directory which we will instantiate only once - this class will serve all our data needs.

 class NonitorData
{
    private NonitorEntityContext _ctx;
    public NonitorData()
    {
        // define a connection string
        const string connectionString = "type=embedded;storesdirectory=.\\;storename=tests";
        _ctx = new NonitorEntityContext(connectionString);

        var unclosedFailures = _ctx.Failures.Where(row => row.FailureOpen).ToList();
        foreach (var failure in unclosedFailures)
        {
            _ctx.Failures.ToList().Remove(failure);
        }


        _ctx.SaveChanges();

        var unclosedTests = _ctx.Tests.Where(row => row.TestOpen).ToList();
        foreach (var test in unclosedTests)
        {
            _ctx.Tests.ToList().Remove(test);
        }
        _ctx.SaveChanges();

    }

    private int getNewTestKey()
    {
        if (_ctx.Tests.Count() == 0)
            return 1;
        else
        {
            List<int> keyList = new List<int>();
            foreach(var test in _ctx.Tests)
            {
                keyList.Add(test.TestKey);
            }

            return keyList.Max() + 1;
        }
    }

    private int getNewFailureKey()
    {
        if (_ctx.Failures.Count() == 0)
            return 1;
        else
        {
            List<int> keyList = new List<int>();
            foreach (var failure in _ctx.Failures)
            {
                keyList.Add(failure.FailureKey);
            }

            return keyList.Max() + 1;
        }
    }

    public int StartTest()
    {
        int key = getNewTestKey();
        var newTest = _ctx.Tests.Create();
        newTest.TestStart = DateTime.Now;
        newTest.TestOpen = true;
        newTest.TestEnd = new DateTime();
        newTest.TestKey = key;
        _ctx.SaveChanges();
        return key;
    }

    public void EndTest(int key)
    {
        var currentTest = _ctx.Tests.Where(row => row.TestKey == key).FirstOrDefault();
        if (currentTest.TestOpen)
        {
            currentTest.TestEnd = DateTime.Now;
            currentTest.TestOpen = false;
            _ctx.SaveChanges();
        }
    }

    public void EndFailure(int key)
    {
        var currentFailure = _ctx.Failures.Where(row => row.FailureKey == key).FirstOrDefault();
        if (currentFailure.FailureOpen)
        {
            currentFailure.FailureEnd = DateTime.Now;
            currentFailure.FailureOpen = false;
            _ctx.SaveChanges();
        }
    }

    public ITest GetTest(int key)
    {
        return _ctx.Tests.Where(row => row.TestKey == key).FirstOrDefault();
    }

    public int StartFailure(int testKey)
    {
        var test = _ctx.Tests.Where(row => row.TestKey == testKey).FirstOrDefault();

        int key = getNewFailureKey();

        var failure = _ctx.Failures.Create();
        failure.FailureKey = key;
        failure.FailureOpen = true;
        failure.FailureStart = DateTime.Now;
        failure.FailureEnd = new DateTime();
        test.Failures.Add(failure);

        _ctx.SaveChanges();

        return key;
    }

    public List<ITest> GetTests()
    {
        var tests = _ctx.Tests.ToList();
        return _ctx.Tests.ToList();
    }
}

At this point in time it is a good idea to compile the application and run some tests on your data service: Create a new NonitorData instance. Check inside your /bin/debug directory that the tests folder has been created containing the data. * Create some tests and some failures and be sure you can access them.

5.3 The Net Testing Layer

We need to create a static method that can ping a server (In this case Google [8.8.8.8]). So we create another static class in the "Code" folder:

 static class NetTester
{
    public static bool DoTest()
    {
        try
        {
            Ping p = new Ping();

            PingReply r;
            string s;
            s = "8.8.8.8";
            r = p.Send(s);

            if (r.Status == IPStatus.Success)
                return true;
            else
                return false;
        }
        catch (Exception ex)
        {
            return false;
        }
    }
}

5.4 Creating an Application context

As mentioned earlier in the article, we won't be using form1. We will create a form in the forms folder and - in the file Program.cs we run the ApplicationContext instead of a form. We can display the form in the constructor.

Create a new form in the "Forms" folder, called frmMain.

And also create a class NonitorContext that inherits from ApplicationContext.

The NonitorContext class will have the following purposes: Keep an up to date data store of the current state (Connected or not) Test with the amount of failures It will also provide an interface for using our class NonitorContext Is an interface for calling tests and running them asynchronously (We don't want to block the UI thread - so our animations keep running.)

Take note of the following: We pass the NonitorContext to the constructor of the frmMain class so we have access to it's methods. We close the application on the form closing event so it doesn't keep running

So NonitorContext looks like this:

 class NonitorContext : ApplicationContext
{
    public int _TestKey = 0;
    public ITest _Test = null;
    public bool _Connected;
    private int _FailureKey = 0;
    private BackgroundWorker _Tester;
    private NonitorData _NonitorData;
    private Forms.frmMain _FrmMain;
    public NonitorContext()
    {
        _NonitorData = new NonitorData();

        _FrmMain = new Forms.frmMain(this);
        _FrmMain.Show();

        _FrmMain.FormClosing += _FrmMain_FormClosing;

    }
    public List<ITest> GetClosedTests()
    {
        return _NonitorData.GetTests().Where(row => row.TestOpen == false).ToList();
    }

    private void _FrmMain_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (_Tester != null)
        {

        }
        Cef.Shutdown();
        Application.Exit();
    }

    public void StartTest()
    {
        _TestKey = _NonitorData.StartTest();
        _Test = _NonitorData.GetTest(_TestKey);
        _Connected = true;
        _Tester = new BackgroundWorker()
        {
            WorkerSupportsCancellation = true
        };

        _Tester.RunWorkerCompleted += _Tester_RunWorkerCompleted;
        _Tester.DoWork += _Tester_DoWork;
        _Tester.RunWorkerAsync();
    }

    private void _Tester_DoWork(object sender, DoWorkEventArgs e)
    {
        if (NetTester.DoTest())
        {
            if (!_Connected)
            {
                _Connected = true;
                _NonitorData.EndFailure(_FailureKey);
                _FailureKey = 0;

            }
            _Test = _NonitorData.GetTest(_TestKey);
        }
        else
        {
            if (_Connected)
            {
                _FailureKey = _NonitorData.StartFailure(_Test.TestKey);
                _Connected = false;
            }
            _Test = _NonitorData.GetTest(_TestKey);
        }
    }

    private void _Tester_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (_Tester != null && !_Tester.IsBusy)
        {
            _Tester.RunWorkerAsync();
        }
    }

    public void EndTest()
    {
        _Tester.CancelAsync();
        _Tester.Dispose();
        _Tester = null;


        if (_FailureKey != 0)
            _NonitorData.EndFailure(_FailureKey);

        _NonitorData.EndTest(_TestKey);

        _Test = null;
        _TestKey = 0;

        _Connected = false;
    }
}

Go ahead and give it a test run. It should popup an empty frmMain and close the Application when you close the form. Move on if everything is going as expected.

Now while you're at it go and apply that icon in the resources folder to your application. Two places: Project Properties Form Properties (omitted) alt text


6. Finishing Up - The GUI

6.1 Create the front end

This is the main method with which we communicate between C# and Javascript on the front end.

This involves creating a C# class with access to NonitorContext's methods and data, and registering it on the "ChromiumWebBrowser" control so that we have access to it from the HTML - which acts as our front end. This needs to happen on our main form. We give access to the NonitorContext by passing it through the constructor from the frmMain that already has a reference.

The code for NonitorJSObject in the code namespace:

 class NonitorJSObject
{
    private NonitorContext _NonitorContext;

    public NonitorJSObject(NonitorContext NonitorContext)
    {
        _NonitorContext = NonitorContext;
    }

    public string getClosedTests()
    {
        List<ITest> closedTests = _NonitorContext.GetClosedTests();

        List<TestDataSet> list = new List<TestDataSet>();

        foreach (ITest test in closedTests)
        {
            list.Add(NonitorStaticMethods.GetTestDataSet(test));
        }


        return JsonConvert.SerializeObject(list);

    }

    public string getState()
    {
        return JsonConvert.SerializeObject(new {
            Connected = _NonitorContext._Connected,
            TestStart = _NonitorContext._Test != null ? Convert.ToInt64(_NonitorContext._Test.TestStart.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds) : 0,
            TestDuration = _NonitorContext._Test != null ? Convert.ToInt64(DateTime.Now.Subtract(_NonitorContext._Test.TestStart).TotalMilliseconds) : 0,
            FailureCount = _NonitorContext._Test != null ? _NonitorContext._Test.Failures.Count() : 0
        });
    }
    public bool startTest()
    {
        _NonitorContext.StartTest();
        return true;
    }

    public bool endTest()
    {
        _NonitorContext.EndTest();
        return true;
    }
}

These JS objects need to be registered when the ChromiumWebBrowser renders the HTML page. Then you can call C# methods from your browser's JS code.

The code for frmMain in the forms namespace:

 partial class frmMain : Form
{
    private ChromiumWebBrowser _browser;
    private NonitorContext _NonitorContext;
    public frmMain(NonitorContext nonitorContext)
    {
        _NonitorContext = nonitorContext;
        InitializeComponent();
        initBrowser();
    }

    public void initBrowser()
    {
        string html = Code.NonitorStaticMethods.GetStringResource("Nonitor.Resources.nonitor.html");

        _browser = Code.NonitorStaticMethods.GetBrowser();
        _browser.IsBrowserInitializedChanged += _browser_IsBrowserInitializedChanged;
        _browser.Dock = DockStyle.Fill;
        this.Controls.Add(_browser);
        _browser.LoadHtml(html, "http://rendering", Encoding.UTF8);
        _browser.RegisterJsObject("nonitorJSObject", new NonitorJSObject(_NonitorContext));
    }

    private void _browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e)
    {
        _browser.ShowDevTools();
    }
}

Again, the ShowDevTools() is included - but should be removed at production. I included it because it is absolutely necessary to get your App working correctly. There will always be debugging involved... always.

Note: As mentioned in the introduction for this article, I will not be discussing front-end mechanics at length. The scripting is left for the reader to figure out.

A Short Summary of what our front end page does: Provides the means to start and stop a test If the test is running, lets the user know whether or not it is connected to the internet. Shows previous tests that were completed previously with some useful data There is a lot of opportunity to improve this front end page with better management of data, search features, sorting features etc._

Herewith the HTML page called nonitor as an embbeded resource!:

 html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" content="width=device-width, initial-scale=1.0" />
    <title></title>
    <link href="http://rendering/css/tether.min.css" rel="stylesheet" />
    <link href="http://rendering/css/bootstrap.min.css" rel="stylesheet" />
    <link href="http://rendering/css/font-awesome.min.css" rel="stylesheet" />
</head>
<body>

    <header>
        <nav data-toggle="popover" data-trigger="hover" data-container="body" data-placement="bottom" title="Welcome to Nonitor" data-content="Just a little open source tool to monitor your internet uptime!" class="navbar navbar-light bg-faded">
            <a class="navbar-brand" href="javascript:void(0)">
                <img src="http://rendering/images/web_hi_res_512.png" width="30" height="30" alt="">
                Nonitor
            </a>
        </nav>
    </header>

    <main class="container-fluid">
        <ul class="nav nav-tabs row no-gutters" role="tablist">
            <li class="nav-item col-6">
                <a class="nav-link active" data-toggle="tab" href="#testpage" role="tab">Main Test</a>
            </li>
            <li class="nav-item col-6">
                <a class="nav-link" data-toggle="tab" href="#pervioustestspage" role="tab">Profile</a>
            </li>
        </ul>

        <!-- Tab panes -->
        <div class="tab-content">
            <div class="tab-pane active" id="testpage" role="tabpanel">
                <div class="container-fluid mt-2">
                    <div class="row">
                        <div class="col-sm-10 offset-sm-1 col-12 card">
                            <div class="card-block">
                                <h3 id="testInfo" class="card-title">No Test Running</h3>
                                <p class="card-text">Click on the button below to get a test up and running</p>

                            </div>
                            <div id="heartbeat" class="card-block" style="height:150px;padding:0px;margin:0px;">

                            </div>
                            <div class="card-block">
                                <p class="text-center">
                                    <button id="btnStartTest" class="btn btn-outline-success">
                                        Start Monitoring
                                        <span class="fa fa-play fa-fw">

                                        </span>
                                    </button>
                                    <button id="btnEndTest" class="btn btn-outline-danger" disabled>
                                        End Monitoring
                                        <span class="fa fa-stop fa-fw">

                                        </span>
                                    </button>
                                </p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="tab-pane" id="pervioustestspage" role="tabpanel">
                <div class="container-fluid">
                    <div class="row">
                        <div class="col-sm-10 offset-sm-1 col-12">
                            <h3>
                                Test Data: <span id="btnRefresh" tabindex="-1" class="btn btn-outline-primary pull-right"><span class="fa fa-refresh"></span></span>
                            </h3>
                            <hr />
                        </div>

                        <div class="col-12">
                            <div id="dataList" class="list-group">

                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </main>

    <script src="http://rendering/js/jquery-3.2.1.min.js"></script>
    <script src="http://rendering/js/tether.min.js"></script>
    <script src="http://rendering/js/bootstrap.min.js"></script>
    <script src="http://rendering/js/Chart.min.js"></script>
    <script src="http://rendering/js/flat-l.js"></script>
    <script>
        function jsDateFromLong(longInt) {

            var date = new Date(longInt);

            var dateReturn = new Date((date.getTime() + date.getTimezoneOffset() * 60000));
            return dateReturn;

        }

        function getTime(milli) {
            return quotient = Math.floor(milli / 3600000) + ":" + Math.floor((milli / 60000) - 60 * (Math.floor(milli / 3600000))) + ":" + Math.floor((milli / 1000) - 60 * (Math.floor(milli / 60000)));
        }

        $().ready(function () {

            function AddLineChart(ctx, dataSet)
            {
                //console.log(JSON.stringify(dataSet));
                var labels = new Array();
                var data = new Array();
                for (var key in dataSet) {
                    if (dataSet.hasOwnProperty(key)) {
                        labels.push(Math.floor(((parseFloat(key) / 1000) == NaN) ? 0 : (parseFloat(key) / 1000)));
                        data.push(dataSet[key]);
                    }
                }

                var myLineChart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: labels,
                        datasets: [{
                            label: 'Up/Down',
                            data: data,
                            borderColor: 'rgba(54, 162, 235, 1)'
                        }],
                    },
                    options: {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    beginAtZero: true
                                }
                            }]
                        }
                    }
                });


            };

            function AddBarChart(ctx, dataSet) {
                var myChart = new Chart(ctx, {
                    type: 'bar',
                    data: {
                        labels: ["Uptime", "Downtime", "Totaltime"],
                        datasets: [{
                            label: 'Uptime Vs. DownTime Vs. TotalTime (Seconds)',
                            data: [dataSet.Uptime / 1000, dataSet.DownTime / 1000, dataSet.TotalTime / 1000],
                            backgroundColor: [
                                'rgba(54, 162, 235, 0.2)',
                                'rgba(255, 99, 132, 0.2)',
                                "rgba(51, 204, 51, 0.2)"
                            ],
                            borderColor: [
                                'rgba(54, 162, 235, 1)',
                                'rgba(255,99,132,1)',
                                "rgba(51, 204, 51,1)"
                            ],
                            borderWidth: 1
                        }]
                    },
                    options: {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    beginAtZero: true
                                }
                            }]
                        }
                    }
                });
            };

            function loadClosedTests()
            {
                var testData = JSON.parse(nonitorJSObject.getClosedTests());
                var dataList = document.getElementById('dataList');
                dataList.innerHTML = '';
                for (var i = 0; i < testData.length; i++)
                {
                    var li = document.createElement('div');
                    li.className = "list-group-item list-group-item-action flex-column";

                    li.innerHTML = "<p class='d-flex w-100'>Test On: " + jsDateFromLong(testData[i].StartDateTime).toLocaleDateString() + " " + jsDateFromLong(testData[i].StartDateTime).toLocaleTimeString()
                        + "   <small class='ml-3'>(Duration: " + getTime(testData[i].TotalTime)
                        + ", DownTime: " + getTime(testData[i].TotalDownTime)
                        + ", Failures: " + testData[i].FailureCount + ")</small></p>";

                    var flexRow = document.createElement("div");
                    flexRow.className = "d-flex flex-row w-100"
                    var barDiv = document.createElement("div");

                    var barCanvas = document.createElement("canvas");
                    flexRow.appendChild(barDiv);
                    barDiv.className = "w-50";
                    barCanvas.width = barDiv.clientWidth;
                    barDiv.appendChild(barCanvas);
                    li.appendChild(flexRow)

                    var lineDiv = document.createElement('div');

                    var lineCanvas = document.createElement('canvas');
                    lineCanvas.width = lineDiv.clientWidth;
                    lineDiv.appendChild(lineCanvas);
                    flexRow.appendChild(lineDiv);

                    lineDiv.className = "w-50"

                    dataList.appendChild(li);
                    AddLineChart(lineCanvas.getContext('2d'), testData[i].DataSet);
                    AddBarChart(barCanvas.getContext('2d'), { Uptime: (testData[i].TotalTime - testData[i].TotalDownTime), DownTime: testData[i].TotalDownTime, TotalTime: testData[i].TotalTime });
                    flexRow.style.height = "400px";
                    barDiv.style.height = "400px";
                    lineDiv.style.height = '400px';

                }
            };

            document.getElementById("btnRefresh").onclick = function () {
                loadClosedTests();
            };

            loadClosedTests();

            var timer;

            function GetConnectionData()
            {
                var state = JSON.parse(nonitorJSObject.getState());

                if (!state.Connected) {

                    document.getElementById('testInfo').innerHTML = 'Connection Status : Not Connected<br/>'
                        + 'Test Start : ' + jsDateFromLong(state.TestStart).toUTCString() + '<br/>'
                    + 'Test Duration : ' + getTime(state.TestDuration) + '<br/>'
                    + 'Failure Count : ' + state.FailureCount + '<br/>';

                    flatL.flatLine(document.getElementById("heartbeat"));
                }
                else {
                    flatL.start({ container: document.getElementById('heartbeat'), beatColor: "green", flatColor: "red" });

                    document.getElementById('testInfo').innerHTML = 'Connection Status : Connected!<br/>'
                        + 'Test Start : ' + jsDateFromLong(state.TestStart).toUTCString() + '<br/>'
                    + 'Test Duration : ' + getTime(state.TestDuration) + '<br/>'
                    + 'Failure Count : ' + state.FailureCount + '<br/>';

                }



            }

            function StartMonitoring()
            {
                if (!nonitorJSObject.startTest())
                    return;

                timer = setInterval(GetConnectionData, 100);

                if (!document.getElementById('btnStartTest').disabled) {
                    flatL.start({ container: document.getElementById('heartbeat'), beatColor: "green", flatColor: "red" });
                    document.getElementById('btnStartTest').disabled = true;
                    document.getElementById('btnEndTest').disabled = false;
                }
            };

            function EndMonitoring()
            {
                nonitorJSObject.endTest();
                clearInterval(timer);
                timer = null;

                document.getElementById('testInfo').innerHTML = 'No Test Running';



                if (!document.getElementById('btnEndTest').disabled) {
                    flatL.flatLine(document.getElementById('heartbeat'));
                    document.getElementById('btnEndTest').disabled = true;
                    document.getElementById('btnStartTest').disabled = false;
                }
            };

            $('[data-toggle="popover"]').popover();
            $('[data-toggle="tooltip"]').tooltip();
            flatL.start({ container: document.getElementById('heartbeat'), beatColor: "green", flatColor: "red" });
            flatL.flatLine(document.getElementById('heartbeat'));

            document.getElementById('btnStartTest').onclick = function () {
                StartMonitoring();
            };

            document.getElementById('btnEndTest').onclick = function () {
                EndMonitoring();
            };

        });
    </script>

</body>
</html>

Here is the code - added to our static methods - for getting the test data sets(for use with Chart.js).

 Class Definition:

public class TestDataSet
{
    public long TotalDownTime { get; set; }

    public long StartDateTime { get; set; }

    public long TotalTime { get; set; }

    public long EndDateTime { get; set; }

    public int FailureCount { get; set; }

    public Dictionary<long, int> DataSet { get; set; }
}
//Getting TestData from ITest entity:
public static TestDataSet GetTestDataSet(ITest test)
{
    long duration = Convert.ToInt64(test.TestEnd.Subtract(test.TestStart).TotalMilliseconds);

    TestDataSet testDataSet = new TestDataSet()
    {
        StartDateTime = Convert.ToInt64(test.TestStart.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds),
        EndDateTime = Convert.ToInt64(test.TestEnd.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds),
        DataSet = new Dictionary<long, int>(),
        TotalTime = duration,
        TotalDownTime = 0,
        FailureCount = test.Failures.Count()
    };
    long increment = duration / 100;
    long startingValue = 0;
    for (int i = 0; i < 100; i++)
    {
        testDataSet.DataSet.Add(startingValue,1);
        startingValue = startingValue + increment;
    }

    foreach (var failure in test.Failures)
    {
        long startingLongMs = Convert.ToInt64(failure.FailureStart.Subtract(test.TestStart).TotalMilliseconds);
        long failDurationMs = Convert.ToInt64(failure.FailureEnd.Subtract(failure.FailureStart).TotalMilliseconds);
        long endingLong = startingLongMs + failDurationMs;

        List<long> keysToChange = new List<long>();

        foreach (var keyVal in testDataSet.DataSet)
        {
            if ((keyVal.Key) >= startingLongMs && (keyVal.Key) <= endingLong)
            {
                keysToChange.Add(keyVal.Key);
            }
        }

        foreach(long lng in keysToChange)
        {
            testDataSet.DataSet[lng] = 0;
        }

        testDataSet.TotalDownTime += failDurationMs;
    }

    return testDataSet;

}

6.2 Test It!

It's time to test and debug. The following will be screenshots of Nonitor as our "finished product":

alt text alt text

7 Creating a WIX Installer

First install the WIX toolset (wix311.exe).

Be sure to install the extension for Visual Studio.

7.1 Build to a WIX project

Let's add two new projects to the solution: Setup Project for WIX v4 Bootstrapper Project for WIX v3

alt text

In the properties of the winforms project (There have been screenies already), set the build target to build to the WIX project as the release version.

What we want to do, and this is important, is to be able to create a build in a single step.

So I build to nonitorSetup/releaseBuild

7.2 Create an Installer

So now your entire release version should be easily accessible inside your WIX setup project.

No we have to include all the files in the install. WIX, is just a XML representation of all the files that you need to "copy over" to the client machine. Obviously it does a ton of other cool stuff such as create an uninstall and customizing for updates instead of clean installs.

It is a very powerful tool.

Herewith my WIX setup XML (product.wxs):

 version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
	<Product Id="*" Name="nonitorSetup" Language="1033" Version="1.0.0.0" Manufacturer="Terrance Jevon" UpgradeCode="c1fbf9f0-2382-46c4-aabf-518d422cf727">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
    <MediaTemplate EmbedCab="yes" />

    <Feature Id="ProductFeature" Title="Reminder" Level="1">
      <ComponentGroupRef Id="ProductComponents" />
      <ComponentGroupRef Id="LocalesFiles" />
      <ComponentGroupRef Id="shortcuts"/>
    </Feature>
	</Product>

	<Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLFOLDER" Name="Reminder">
          <Directory Id="Locales" Name="locales"></Directory>
        </Directory>
      </Directory>
      <Directory Id="ProgramMenuFolder">
        <Directory Id="ApplicationProgramsFolder" Name="Nonitor" />
      </Directory>
    </Directory>
	</Fragment>

	<Fragment>
    <ComponentGroup Id="shortcuts" Directory="ApplicationProgramsFolder">
      <Component Id="shortcut" Guid="6783cac9-b61f-4351-958b-68a85c5905f8">
        <Shortcut Id="ApplicationShortcut1" Name="Nonitor" Description="Net uptime monitoring."
                  Target="[INSTALLFOLDER]Nonitor.exe" WorkingDirectory="INSTALLFOLDER"/>
        <RegistryValue Root="HKCU" Key="Software\Nonitor"
                  Name="installed" Type="integer" Value="1" KeyPath="yes"/>
        <RemoveFolder Id="ProgramMenuSubfolder" On="uninstall"/>
      </Component>
    </ComponentGroup>

    <ComponentGroup Id="LocalesFiles" Directory="Locales">
      <Component Id="LocaleFile" Guid="284c3351-291f-4d90-b7f4-ca1fdfb60fc5">
        <File Id="lreq1" Source="releaseBuild/locales/am.pak" />
        <File Id="lreq2" Source="releaseBuild/locales/ar.pak" />
        <File Id="lreq3" Source="releaseBuild/locales/bg.pak" />
        <File Id="lreq4" Source="releaseBuild/locales/bn.pak" />
        <File Id="lreq5" Source="releaseBuild/locales/ca.pak" />
        <File Id="lreq6" Source="releaseBuild/locales/cs.pak" />
        <File Id="lreq7" Source="releaseBuild/locales/da.pak" />
        <File Id="lreq8" Source="releaseBuild/locales/de.pak" />
        <File Id="lreq9" Source="releaseBuild/locales/el.pak" />
        <File Id="lreq10" Source="releaseBuild/locales/en-GB.pak" />
        <File Id="lreq11" Source="releaseBuild/locales/en-US.pak" />
        <File Id="lreq12" Source="releaseBuild/locales/es.pak" />
        <File Id="lreq13" Source="releaseBuild/locales/es-419.pak" />
        <File Id="lreq14" Source="releaseBuild/locales/et.pak" />
        <File Id="lreq15" Source="releaseBuild/locales/fa.pak" />
        <File Id="lreq16" Source="releaseBuild/locales/fi.pak" />
        <File Id="lreq17" Source="releaseBuild/locales/fil.pak" />
        <File Id="lreq18" Source="releaseBuild/locales/fr.pak" />
        <File Id="lreq19" Source="releaseBuild/locales/gu.pak" />
        <File Id="lreq20" Source="releaseBuild/locales/he.pak" />
        <File Id="lreq21" Source="releaseBuild/locales/hi.pak" />
        <File Id="lreq22" Source="releaseBuild/locales/hr.pak" />
        <File Id="lreq23" Source="releaseBuild/locales/hu.pak" />
        <File Id="lreq24" Source="releaseBuild/locales/id.pak" />
        <File Id="lreq25" Source="releaseBuild/locales/it.pak" />
        <File Id="lreq26" Source="releaseBuild/locales/ja.pak" />
        <File Id="lreq27" Source="releaseBuild/locales/kn.pak" />
        <File Id="lreq28" Source="releaseBuild/locales/ko.pak" />
        <File Id="lreq29" Source="releaseBuild/locales/lt.pak" />
        <File Id="lreq30" Source="releaseBuild/locales/lv.pak" />
        <File Id="lreq31" Source="releaseBuild/locales/ml.pak" />
        <File Id="lreq32" Source="releaseBuild/locales/mr.pak" />
        <File Id="lreq33" Source="releaseBuild/locales/ms.pak" />
        <File Id="lreq34" Source="releaseBuild/locales/nb.pak" />
        <File Id="lreq35" Source="releaseBuild/locales/nl.pak" />
        <File Id="lreq36" Source="releaseBuild/locales/pl.pak" />
        <File Id="lreq37" Source="releaseBuild/locales/pt-BR.pak" />
        <File Id="lreq38" Source="releaseBuild/locales/pt-PT.pak" />
        <File Id="lreq39" Source="releaseBuild/locales/ro.pak" />
        <File Id="lreq40" Source="releaseBuild/locales/ru.pak" />
        <File Id="lreq41" Source="releaseBuild/locales/sk.pak" />
        <File Id="lreq42" Source="releaseBuild/locales/sl.pak" />
        <File Id="lreq43" Source="releaseBuild/locales/sr.pak" />
        <File Id="lreq44" Source="releaseBuild/locales/sv.pak" />
        <File Id="lreq45" Source="releaseBuild/locales/sw.pak" />
        <File Id="lreq46" Source="releaseBuild/locales/ta.pak" />
        <File Id="lreq47" Source="releaseBuild/locales/te.pak" />
        <File Id="lreq48" Source="releaseBuild/locales/th.pak" />
        <File Id="lreq49" Source="releaseBuild/locales/tr.pak" />
        <File Id="lreq50" Source="releaseBuild/locales/uk.pak" />
        <File Id="lreq51" Source="releaseBuild/locales/vi.pak" />
        <File Id="lreq52" Source="releaseBuild/locales/zh-CN.pak" />
        <File Id="lreq53" Source="releaseBuild/locales/zh-TW.pak" />
      </Component>
    </ComponentGroup>

    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
      <Component Id="ProductComponent" Guid="491351e2-927a-4ace-a5f8-c18fcb46893a">
        <File Id="executeable" Source="releaseBuild/Nonitor.exe"/>
        <File Id="req1" Source="releaseBuild/cef.pak" />
        <File Id="req2" Source="releaseBuild/cef_100_percent.pak" />
        <File Id="req3" Source="releaseBuild/cef_200_percent.pak" />
        <File Id="req4" Source="releaseBuild/cef_extensions.pak" />
        <File Id="req5" Source="releaseBuild/CefSharp.BrowserSubprocess.Core.dll" />
        <File Id="req6" Source="releaseBuild/CefSharp.BrowserSubprocess.Core.pdb" />
        <File Id="req7" Source="releaseBuild/CefSharp.BrowserSubprocess.exe" />
        <File Id="req8" Source="releaseBuild/CefSharp.BrowserSubprocess.pdb" />
        <File Id="req9" Source="releaseBuild/CefSharp.Core.dll" />
        <File Id="req10" Source="releaseBuild/CefSharp.Core.pdb" />
        <File Id="req11" Source="releaseBuild/CefSharp.Core.xml" />
        <File Id="req12" Source="releaseBuild/CefSharp.dll" />
        <File Id="req13" Source="releaseBuild/CefSharp.pdb" />
        <File Id="req14" Source="releaseBuild/CefSharp.WinForms.dll" />
        <File Id="req15" Source="releaseBuild/CefSharp.WinForms.pdb" />
        <File Id="req16" Source="releaseBuild/CefSharp.WinForms.xml" />
        <File Id="req17" Source="releaseBuild/CefSharp.xml" />
        <File Id="req18" Source="releaseBuild/chrome_elf.dll" />
        <File Id="req19" Source="releaseBuild/d3dcompiler_47.dll" />
        <File Id="req20" Source="releaseBuild/BrightstarDB.dll" />
        <File Id="req21" Source="releaseBuild/devtools_resources.pak" />
        <File Id="req22" Source="releaseBuild/dotNetRDF.dll" />
        <File Id="req23" Source="releaseBuild/dotNetRDF.pdb" />
        <File Id="req24" Source="releaseBuild/dotNetRDF.xml" />
        <File Id="req26" Source="releaseBuild/HtmlAgilityPack.dll" />
        <File Id="req27" Source="releaseBuild/HtmlAgilityPack.pdb" />
        <File Id="req28" Source="releaseBuild/HtmlAgilityPack.xml" />
        <File Id="req29" Source="releaseBuild/icudtl.dat" />
        <File Id="req30" Source="releaseBuild/libcef.dll" />
        <File Id="req31" Source="releaseBuild/libEGL.dll" />
        <File Id="req32" Source="releaseBuild/libGLESv2.dll" />
        <File Id="req33" Source="releaseBuild/natives_blob.bin" />
        <File Id="req34" Source="releaseBuild/Newtonsoft.Json.dll" />
        <File Id="req35" Source="releaseBuild/Newtonsoft.Json.xml" />
        <File Id="req36" Source="releaseBuild/Nonitor.exe.config" />
        <File Id="req37" Source="releaseBuild/Nonitor.pdb" />
        <File Id="req38" Source="releaseBuild/snapshot_blob.bin" />
        <File Id="req39" Source="releaseBuild/VDS.Common.dll" />
        <File Id="req40" Source="releaseBuild/VDS.Common.pdb" />
        <File Id="req43" Source="releaseBuild/VDS.Common.xml" />
        <File Id="req44" Source="releaseBuild/widevinecdmadapter.dll" />
      </Component>
    </ComponentGroup>

  </Fragment>
</Wix>

Now the next step is to set the output directory into the bootstrapper project.

Something like this /nonitorDependencySetup/nonitor

7.3 Bundling Dependencies

Let's go get offline versions of: .net 4.5.2 VC++ Redistributable

Go get what you need from here.

Also create a license file containing the licensing information you need - I create this in the root directory as .rtf.

Also include a 32 x 32 version of your icon.

Herewith the code for bundle.wxs:

 version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
  <Bundle Name="Nonitor" Version="1.0.0.0" Manufacturer="Nonitor" UpgradeCode="4bb65a10-7770-49a0-b6d4-0a50cf0b5b71">
    <BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense" >

      <bal:WixStandardBootstrapperApplication
        LicenseFile="license.rtf"
        LogoFile="web_hi_res_512.ico"
        />
    </BootstrapperApplicationRef>
    <Chain>
      <ExePackage SourceFile="NDP452-KB2901907-x86-x64-AllOS-ENU.exe" />
      <ExePackage SourceFile="vc_redist.x64.exe" />
      <MsiPackage SourceFile="nonitor/nonitorSetup.msi" />
    </Chain>
  </Bundle>
</Wix>

Right click on the solution and set the build order to build -> App -> Setup -> Bootstrapper.

That's it - your installer, complete with prerequisites, is done. Run it and see if it works.


8. Conclusion

You have achieved the following:

  • Desktop app with an HTML front end
  • With HTML5 your only limit is your imagination, it is a powerful cross-platform tool to create the perfect front faces for your apps.
  • Desktop App with its own data store
  • Created a dependency including installer for your app.
  • You did asynchronous tasks so as to not block the UI thread (Pretty important if you are performing any tasks that take time - or that might fail)

This should form a solid baseline from which you can make your own apps for the world. From here I would encourage you to expand your horizons into cross-platform development as this is only a guide that applies to windows (except for your .html)

You can get the project files from here, and if you would like to know more about me as a developer you can visit my page here.

Thanks for reading.