Click here to Skip to main content
15,891,597 members
Articles / Web Development / HTML

Adding a WebApp to a Winforms Project

Rate me:
Please Sign up or sign in to vote.
4.85/5 (5 votes)
29 Nov 2019CPOL3 min read 8.5K   10   3
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.

HTML
<!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:

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

document.addEventListener('DOMContentLoaded', init);

The connection is opened to the original http address:

JavaScript
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.

JavaScript
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:

XML
<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:

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

Any display updates are sent to all connections:

C#
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:

XML
<command>play</command>

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

C#
string strXML = Encoding.UTF8.GetString(receiveBuffer, 0, receiveResult.Count);
//Debug.WriteLine(strXML);
XmlDocument xml = new XmlDocument();
xml.LoadXml(strXML);
var nodes = xml.SelectNodes("command");
C#
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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
Adrian is a Hardware/Software Engineer specialising in Pipe Organ Controls (www.ssosystems.com) with completed projects all over the world including Westminster Abbey, Royal Albert Hall, Sydney Opera House, Walt Disney Concert Hall. Recent projects have included various Touch-Screen controllers and the Johnnie Walker Symphony in Blue events.
The first computer he ever used was a Commodore PET. He has previously worked in arcade game repairs and writing home computer games including translating Boulder Dash to the Sinclair Spectrum.
In his spare time he does sound engineering in local theatre groups and live music productions.
Current projects include a web based central heating controller.

Comments and Discussions

 
QuestionVery interesting idea Pin
Daniel Kamisnki4-Dec-19 9:26
Daniel Kamisnki4-Dec-19 9:26 
AnswerRe: Very interesting idea Pin
Adrian Wadey8-Dec-19 9:32
professionalAdrian Wadey8-Dec-19 9:32 
AnswerRe: Very interesting idea Pin
Adrian Wadey8-Dec-19 9:36
professionalAdrian Wadey8-Dec-19 9:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.