Time Tortoise: Using SignalR with UWP

SignalR

This is one in a series of articles about Time Tortoise, a Universal Windows Platform app for planning and tracking your work schedule. For more on the development of this app and the ideas behind it, see my Time Tortoise category page.

To communicate with its companion app, Time Tortoise needs to use sockets. In recent weeks, I have been experimenting with socket communication between a UWP client app and a server running in a console app. This architecture works when everything is set up and running as intended. But it can be tricky to make sockets robust. So this week I’m looking into adding a layer that makes sockets easier to use and more resistant to unexpected failures.

Socket Scenarios

Basic communication

Time Tortoise: A Companion App summarizes how to set up a UWP client and a non-UWP server so they can communicate over sockets. Two key client method calls used in that arrangement are:

  • Socket.ConnectAsync: Given a server name (or IP address) and port, connect the client and server.
  • StreamWriter.FlushAsync: This method is used after WriteLineAsync to exchange data between the client and server.

If all of the steps happen in order and both the client and server continue running, everything works well. If not, various exception conditions are possible. Here are some examples:

Server isn’t listening

If there’s no server listening on the specified address and port, ConnectAsync throws a COMException with the message “No connection could be made because the target machine actively refused it.”

Example: This happens if you try to connect the client without first starting the server.

Client and server are not connected

Attempting to write data to a disconnected socket using StreamWriter.FlushAsync results in an InvalidOperationException with the message “A method was called at an unexpected time.”

Example: This happens if you try to send data from the client without first connecting to the server.

Client tries to connect using a socket that is already connected

If ConnectAsync is called on a socket that’s already connected, it throws an InvalidOperationException with the message “A method was called at an unexpected time.”

Example: This happens if you start the server, connect the client, and then try to connect the client again.

Client and server connect, but then server shuts down

If the client and server were connected through a socket, but the server shuts down unexpectedly, writing to the socket using StreamWriter.FlushAsync results in a COMException with the message “An existing connection was forcibly closed by the remote host.”

Example: This happens if you start the server, connect the client, terminate the server, and then try to send data from the client.

SignalR

In addition to the examples described above, many other combinations of events can happen that cause problems for the socket connection. It’s possible to defend against these invalid conditions, but as with many things in software development, someone else has already done that work. Inspired by this Stack Overflow answer (“If at all possible, use SignalR instead of raw sockets. They do all the hard stuff for you.”), I decided to try SignalR.

SignalR was created to allow ASP.NET developers to write web applications in which the server contacts the client with new information. This supplements the usual web development pattern in which the client contacts the server to request information.

Although it was designed for ASP.NET, SignalR doesn’t require a web server. It works fine with other kinds of apps. In particular, I found an example of a simple chat app in which the client uses UWP and the server runs on the same machine. This is exactly what we need for Time Tortoise and Time Tortoise Companion.

SignalR Chat

The SignalR chat example works as follows.

Server

In this example, the server is a console app. It first does the following:

  • Start a WebApp (from Microsoft.Owin.Hostring) on a port on localhost. OWIN is the Open Web Interface for .NET. This is what allows this console app to use SignalR, an ASP.NET technology, without running in an IIS web server context.
  • Call MapSignalR, which makes a SignalR endpoint available at /signalr.

Client

Once the server is running, the client can connect to it. In this sample app, the user clicks the Join button to join a chat group. This causes the client to do the following:

  • If the client and server are not yet connected, call HubConnection.Start to connect.
  • Invoke the JoinGroup method on the server using IHubProxy.Invoke. SignalR allows the client to call methods on the server using this pattern.

Server

The JoinGroup method adds the client to the new group. Groups are used by SignalR to determine which clients to send messages to. For Time Tortoise, we’ll probably just need one group, since we’ll only have one client.

Client

Once joined to a group, the client can send messages to the chat session. As with joining a group, this involves invoking a server method. In this case, the method is called SendToGroup.

Server

In the SendToGroup method, the server calls ReceiveMessage on the current group.

Client

The client has previously set up a handler for ReceiveMessage. Therefore, when the message arrives from the server, a client method that is also named ReceiveMessage is called. This method prints the chat message to the client window.

Time Tortoise and Time Tortoise Companion

A chat client that can exchange messages with a chat server is exactly we need for Time Tortoise. However, rather than a user typing a chat message, in Time Tortoise the messages will all be generated automatically by the client and server programs. For example, when the server (Time Tortoise Companion) has new data about user idle time, it can send it to the client (Time Tortoise) by invoking a method on the current SignalR group. This will cause a method to be called in Time Tortoise to receive the data and take appropriate action in the Time Tortoise app.