Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adding a WebApp to a Winforms Project

0.00/5 (No votes)
29 Nov 2019 1  
Using httpListener, WebSockets and JavaScript to add a remote control for a Windows Forms program

Introduction

This is a guide to adding a hosted web page to a Windows Winforms desktop program that allows controlling the program through a web page viewed on another device.

This article looks at hosting/serving the web files, sending commands from the web page to the Windows Form and sending data updates from the winform to the web page.

The web page uses HTML/JavaScript and two-way communication between the WebApp and Windows program using XML wrapped data over a web-socket connection.

WebSocket connections work by opening an http request and asking to upgrade to a websocket. This allows connections to be opened wherever a web connection can be made. Unlike an http request which sends the requested data and then closes the connection, a WebSocket connection remains open for two-way communication until the connection is explicitly closed.

Background

I originally developed this to work with SFXPlayer, a Windows sound effects player developed for theatrical use. The web app adds a simple remote control to the Windows Forms program. It shows the cue description, the audio track to be played and buttons to play/stop/skip forwards/skip backwards.

The Web App

index.html has the webapp layout, sfx.js has the JavaScript for opening the websocket connection and handling the communications with the main program.

<!DOCTYPE html>
<html>
<head>
    <title id="Title">SFX Player</title>
    <script src="sfx.js"></script>
</head>

<body>
    <table style="width:100%">
        <tr>
            <td id="PrevMainText">Cell 1</td>
            <td id="MainText">Cell 2</td>
        </tr>
        <tr>
            <td colspan="2" id="TrackName"></td>
        </tr>
    </table>
    <button onclick="sfxws.sendCommand('previous')">Previous</button>
    <button onclick="sfxws.sendCommand('stop')">Stop</button>
    <button onclick="sfxws.sendCommand('play')">Go</button>
    <button onclick="sfxws.sendCommand('next')">Next</button>
</body>
</html>

I haven't added any styling or layout control to this other than a simple table. Nodes with an id (including the title) can be updated from the winforms program. The four buttons each send a command to the winforms program.

The sfx.js file contains the JavaScript implementation of the necessary websocket code. It creates the websocket connection once the page has loaded:

function init() {
    sfxws = new SFXWebSocket();
}

document.addEventListener('DOMContentLoaded', init);

The connection is opened to the original http address:

var ws = new WebSocket("ws://" + location.hostname + ":3030", "ws-SFX-protocol");

The message receive event expects XML and iterates through all nodes and updates DOM nodes whose IDs match the node name with the content of the node.

ws.onmessage = function (evt) {
    var received_msg = evt.data;
    //console.log("Message received:\n" + received_msg);
    BuildXMLFromString(received_msg);
    //document.getElementById("PrevMainText").innerHTML =
    //xmlDoc.getElementsByTagName("PrevMainText")[0].childNodes[0].nodeValue;
    var DisplaySettings = xmlDoc.getElementsByTagName("DisplaySettings")[0].childNodes;
    if (DisplaySettings != null) {
        for (i = 0; i < DisplaySettings.length; i++) {
            if (DisplaySettings[i].nodeType == Node.ELEMENT_NODE) {
                if (DisplaySettings[i + 1].nodeType == Node.TEXT_NODE) {
                    var field = document.getElementById(DisplaySettings[i].nodeName);
                    if (field != null) {
                        field.innerHTML = DisplaySettings[i].textContent;
                    } else {
                        console.log("Unable to locate id=" +
                                    DisplaySettings[i].nodeName +
                                    ". New value = " + DisplaySettings[i].textContent);
                    }
                }
            }
        }
    }
};

Example XML sent from winforms program to update title of web page:

<DisplaySettings>
    <Title>New Title Test</Title>
</DisplaySettings>

Web Server

The source file for the Web Server is available here (WebApp.cs)

A simple webserver is built using the HttpListener class. If the request is for a websocket connection (context.Request.IsWebSocketRequest), then the code handles this in ProcessWebSocketRequest().

Other requests are assumed to be for files and these are looked up and sent or flagged as errors.

The server is started by calling WebApp.Start(); from the main form's Load event handler.

A list of web socket connections is maintained so that multiple simultaneous connections can be handled:

private static List<WebSocket> webSockets = new List<WebSocket>();

Any display updates are sent to all connections:

LastMessage = Encoding.UTF8.GetBytes(e.SerializeToXmlString());
foreach (WebSocket ws in webSockets) {
    await ws.SendAsync(new ArraySegment<byte>(LastMessage, 0, LastMessage.Length), 
    WebSocketMessageType.Text, true, CancellationToken.None);
}

A quirk here is that because C# strings are unicode, the XML serialiser adds the header that they are UTF-16, but we are converting them to UTF-8 to send them. It would be better to change the text to reflect this.

Messages from the web-app are manually encoded as XML snippets in the form:

<command>play</command>

The winforms program receives this as a string, converts it to an XMLDocument and acts on the command nodes.

string strXML = Encoding.UTF8.GetString(receiveBuffer, 0, receiveResult.Count);
//Debug.WriteLine(strXML);
XmlDocument xml = new XmlDocument();
xml.LoadXml(strXML);
var nodes = xml.SelectNodes("command");
switch (command){
    case "play":
        Program.mainForm.PlayNextCue();
        break;

Because the web-socket handler runs in a different thread to the form, the updates have to use Invoke. I found that buttons don't return true for InvokeRequired, so I used my CueList object.

private delegate void SafeCommandDelegate();

internal void PlayNextCue() {
    if (CueList.InvokeRequired) {
        var d = new SafeCommandDelegate(PlayNextCue);
        CueList.Invoke(d);
    } else {
        bnPlayNext_Click(null, null);
    }
}

Points of Interest

Opening the port (3030 in the example) to serve the web files was difficult. When I figure out the steps that are actually relevant, I'll post them here.

History

  • 29th November, 2019: First edition (actual SFXPlayer program still requires styling for the web-app)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here