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;
BuildXMLFromString(received_msg);
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);
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)