Wednesday, 21 February 2018

C# - Creating a simple Winforms and WebBrowser application to shows disk drives

Ok, so I've seen enough to be able to give a series of instructions as to how to build a simple Winforms app that uses the WebBrowser control and an HTML Gui to display a list of disk drives.

To begin we need an HTML file with a clickable anchor and the shell of a table into which we will writes rows (and delete rows when re-running).

1. Create a new Winforms project called WinformsBrowserShowDrives.
2. In Solution Explorer for the C# project select menu 'Add->New Item' and select an HTML Page, HTMLPage1.html.

Add the following HTML to HTMLPage1.html and save


<!DOCTYPE html>
<html>

<head>
    <title>Disk Drive List</title>
    <style>
        table {
            border: 1px solid black;
        }

        th, td {
            padding: 5px;
            text-align: left;
            font-family: Arial;
            font-size: 10pt;
        }

        th {
            background-color: darkblue;
            color: white;
        }

        tr:nth-child(even) {
            background-color: #f2f2f2;
        }

        a#clickMe:hover {
            background-color: #1A365B;
            color: white;
        }

        a#clickMe {
            background-color: #E0F1EA;
            color: #000000;
        }
    </style>
</head>

<body>
    <a id="clickMe">Click Me</a>
    <table>
        <tbody id="diskDrives">
            <tr>
                <th>Disk Drive</th>
                <th>Interface Type</th>
                <th>DeviceDesc</th>
                <th>DeviceSize</th>
            </tr>
        </tbody>
    </table>
</body>
</html>

3. Add a WebBrowser control to the WinForms.Form1. Expand the form to something close to a browser window's size.
4. Add the following code to the Form1.cs class

    public Form1()
    {
        InitializeComponent();

        Assembly myExe = Assembly.GetExecutingAssembly();
        string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
        string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

        if (File.Exists(myFile))
        {
            //this.webBrowser1.DocumentCompleted +
            this.webBrowser1.Navigate(myFile);
        }
    }

5. In the above code, right-click on "Assembly" and Resolve to get the correct Using.
6. In the above code, right-click on "Path" and Resolve to get the correct Using.
7. Build the Project, it should compile then run the project it should run and show the HTML page, if not diagnose file locations.

OK, so at this point the project should build and run and show the HTML in the form, yay! But it doesn't do much as the moment so let's add some active content. First thing we need to do is write an event handler for the DocumentComplete event because we have to wait for completion before accessing the Dom.

8. In the above code un-comment the commented line go to the plus sign at the end and type an equals sign this should bring up hints.
9. Follow the hint to press TAB and repeat. The C# IDE should create an event handler for you.

The code should now look like this


    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Assembly myExe = Assembly.GetExecutingAssembly();
            string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
            string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

            if (File.Exists(myFile))
            {
                this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
                this.webBrowser1.Navigate(myFile);
            }
        }

        void webBrowser1_DocumentCompleted(object sender, 
                                         WebBrowserDocumentCompletedEventArgs e)
        {
            throw new NotImplementedException();
        }
    }

10. In the newly created code block replace with the following

    void webBrowser1_DocumentCompleted(object sender, 
                WebBrowserDocumentCompletedEventArgs e)
    {
        System.Windows.Forms.HtmlElement clickMe = 
            this.webBrowser1.Document.Body.All["clickMe"];
        //clickMe.Click +
    }

11. Use same trick as steps 8. and 9. to create a click handler for the clickMe anchor element. Code should look like this

    public Form1()
    {
        InitializeComponent();

        Assembly myExe = Assembly.GetExecutingAssembly();
        string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
        string myFile = Path.Combine(filePath, @"..\..\HTMLPage1.html");

        if (File.Exists(myFile))
        {
            this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
            this.webBrowser1.Navigate(myFile);
        }
    }

    void webBrowser1_DocumentCompleted(object sender, 
                WebBrowserDocumentCompletedEventArgs e)
    {
        System.Windows.Forms.HtmlElement clickMe = 
            this.webBrowser1.Document.Body.All["clickMe"];
        clickMe.Click += clickMe_Click;
    }

    void clickMe_Click(object sender, HtmlElementEventArgs e)
    {
        throw new NotImplementedException();
    }
}

Next we need to write a class that gives us some data, we'll Wiindows Management Instrumentation (WMI) to provide something interesting to look at.

11. In the Solution Explorer, on the C# Project node right-click and take 'Add->New Itm' and add a Class file called WMIDriveDeviceLister.cs
12. Add using directive "using System.Management;" at top of class
13. Add Reference to System.Management.dll otherwise you'll get compilation errors
14. Copy in the following code

using System;
using System.Collections.Generic;
using System.Text;
using System.Management;           // Add Reference to System.Management.dll

namespace WinformsBrowserShowDrives
{
    internal class WMIDriveDeviceLister
    {
        internal string ListDiskDevicesAsHTML()
        {
            List<Tuple<int, string, string, string>> driveDetails;
            driveDetails = this.ListDiskDevices();

            StringBuilder rows = new StringBuilder();
            foreach (Tuple<int, string, string, string> driveDetail in driveDetails)
            {
                rows.Append("<tr>"
                        + "<td>" + driveDetail.Item1 + "</td>"
                        + "<td>" + driveDetail.Item2 + "</td>"
                        + "<td>" + driveDetail.Item3 + "</td>"
                        + "<td>" + driveDetail.Item4 + "</td>"
                        + "</tr>");
            }

            return rows.ToString();
        }

        internal List<Tuple<int, string, string, string>> ListDiskDevices()
        {
            // Add Reference to System.Management.dll
            System.Management.ManagementScope scope = new
                System.Management.ManagementScope(@"\\.");
            System.Management.ObjectQuery query = new
                System.Management.ObjectQuery("Select * from Win32_DiskDrive");
            ManagementObjectSearcher searcher = 
                      new ManagementObjectSearcher(scope, query);

            Dictionary<Int32, ManagementObject> dicUnSort = 
                      new Dictionary<Int32, ManagementObject>();

            ManagementObjectCollection queryCollection = searcher.Get();

            foreach (ManagementObject m in queryCollection)
            {
                int driveIndex = Int32.Parse(m["Index"].ToString());
                dicUnSort.Add(driveIndex, m);
            }

            List<Tuple<int, string, string, string>> sortedList = 
                                 new List<Tuple<int, string, string, string>>();
            for (int i = 0; i < dicUnSort.Count; i++)
            {
                ManagementObject m = dicUnSort[i];
                string s1 = m["InterfaceType"].ToString();
                string s2 = m["Caption"].ToString();
                string s3 = (m["Size"] != null) ? m["Size"].ToString() : "";
                Tuple<int, string, string, string> driveDetails =
                    new Tuple<int, string, string, string>(i,
                    m["InterfaceType"].ToString(),
                    m["Caption"].ToString(),
                    (m["Size"] != null) ? 
                      (Int64.Parse(m["Size"].ToString()) / (1000000)).ToString()
                      : "");

                sortedList.Add(driveDetails);
            }
            return sortedList;
        }
    }
}


Now we're going to write a class to handle the population of the disk drive HTML table

15. In the Solution Explorer, on the C# Project node right-click and take 'Add->New Item' and add a Class file called DrivesTable.cs
16. Add using directive "using System.Windows.Forms;" at top of class
17. Copy in code below

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WinformsBrowserShowDrives
{
    public static class MyHTMLExtensions
    {
        public static HtmlElement CreateElementWithInnerText(
                   this HtmlDocument doc, string elementTag, string innerText)
        {
            HtmlElement ele = doc.CreateElement(elementTag);
            ele.InnerText=innerText;
            return ele;
        }
    }

    internal class DrivesTable
    {
        protected HtmlDocument doc = null;
        protected HtmlElement tBody = null;

        internal DrivesTable(HtmlDocument doc, string tBodyId)
        {
            this.doc = doc;
            this.tBody = doc.Body.All[tBodyId];
        }

        internal void AddRows()
        {
            WMIDriveDeviceLister lister=new WMIDriveDeviceLister();
            List<Tuple<int, string, string, string>> driveDetails;
            driveDetails = lister.ListDiskDevices();

            foreach (Tuple<int, string, string, string> driveDetail in driveDetails)
            {
                HtmlElement tr = doc.CreateElement("tr");
                tr.Id = "diskDriveEntry";

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item1.ToString()));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item2));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item3));

                tr.AppendChild(doc.CreateElementWithInnerText("td", 
                    driveDetail.Item4));

                tBody.AppendChild(tr);
            }
        }
    }
}


//internal void RemoveRows()
//{
//    HTMLDocument htmlDoc = (HTMLDocument)doc.DomDocument;
//    bool skippedFirst = false;
//    foreach (HtmlElement row in rotTBody.Children)
//    {
//        if (skippedFirst)
//        {
//            IHTMLDOMNode node = htmlDoc.getElementById(row.Id) as IHTMLDOMNode;
//            node.parentNode.removeChild(node);
//        }
//        skippedFirst = true;
//    }
//}


The final step is make the anchor click handler call into the new classes. So in the Form1.cs change button handler code to this below.

        void clickMe_Click(object sender, HtmlElementEventArgs e)
        {
            DrivesTable table = new DrivesTable(webBrowser1.Document, "diskDrives");
            table.AddRows();
        }

So the code should run and you should see a "ClickMe anchor" (sadly hover CSS does not work at the moment, SO Q pending) and if you clik on it the C# code will query WMI and then write out an HTML table. Press it again and it add more rows because currently remove rows is commented out to simplify this blog entry.

No comments:

Post a Comment