Showing posts with label Html. Show all posts
Showing posts with label Html. Show all posts

Wednesday, 21 February 2018

C# - Embed Html page as resource in C# Winforms

So in previous posts I have shown a Winforms WebBrowser control navigating to a local html file.  It is possible to set the WebBrowser's DocumentStream property to a System.IO.Stream.  With the following code it is possible to create a System.IO.Stream from a string


        public static System.IO.Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                  new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }


The string would be an Html string but we want to avoid files so it looks like we can add the file as an embedded resource.

I named mine HTMLPage1 and then it can be referred to in code with


            string myFile2 = WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            System.IO.Stream s = GenerateStreamFromString(myFile2);


But beware, the document needs to complete loading and whilst that is happening the stream needs to kept alive so no early disposal. To solve define stream variable at class level and dispose later. So the code becomes

using System;
using System.Windows.Forms;
using System.IO;

namespace WebBrowserInWinForms
{
    public partial class Form1 : Form
    {
        Stream s = null;

        public Form1()
        {
            
            InitializeComponent();

            string myFile2 = 
               WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
            s = GenerateStreamFromString(myFile2);
            this.webBrowser1.DocumentStream = s;

        }

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

        public static Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                      new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }
        
        void clickMe_Click(object sender, HtmlElementEventArgs e)
        {
            //...
        }
    }
}

Copying the HTML string into the resource editor is clunky, the resource string editor is probably just for words and phrases when one ships an edition in a foreign language. It would be nice if in Debug environment we could use the file navigate method and then for Release we'd do a custom build step and pack the files into the embedded resources with code. We'd need conditional compilation.

The embedded resources can be found in <$SolutionDirector$>/Properties/Resources.resx which is Xml so they are entitised, this looks possible for one day.

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.