Time Tortoise: Minimal SignalR Functionality

Signal

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.

Last week, I covered the reason for using SignalR in Time Tortoise, and how SignalR can be used to build a simple chat application. This week, I’ve been working on integrating minimal SignalR functionality into the Time Tortoise codebase.

Minimal SignalR Functionality

A chat application is the traditional SignalR example app. As described last week, it’s already fairly simple, but for the purpose of communicating between Time Tortoise (TT) and Time Tortoise Companion (TTC), we can pare it down even further. For example, we don’t need to keep track of separate “chat rooms,” since there’s just one client (TT) communicating with one server (TTC).

A minimal SignalR component in Time Tortoise can get away with doing only the following:

  • Start a SignalR server.
  • Connect to the server from a SignalR client.
  • Send a message from the server to the client.

In the TT/TTC design, TTC is the component that knows when new information arrives, so it’s responsible for pushing the information to TT. We don’t yet need to send messages in the other direction, but this will be easy to add if required.

Design

I added the following three projects to the existing Time Tortoise solution:

  • TimeTortoise.Server: A class library containing classes and methods to start a SignalR server, create a SignalR Hub, and send a message to the client. SignalR and its dependencies want this to be a .NET Framework 4.x (“Windows Classic Desktop”) assembly. That’s fine, since the purpose of the server is to handle non-UWP tasks, so it doesn’t need to be referenced from a UWP project.
  • TimeTortoise.Client: A class library containing classes to connect to the SignalR server and receive messages from it. This project needs to be of a type that can be referenced from a UWP app. Universal Windows Class Library works for that purpose.
  • TimeTortoise.ConsoleClassic: A Console App (.NET Framework 4.x) for testing the server.

The existing TimeTortoise.Console project (a .NET Core console app) can be used to test the client.

Implementation

Server

MessageHub: When a SignalR client and server communicate, they use an entity called a hub. The server developer creates a class that inherits from Microsoft.AspNet.SignalR.Hub, and adds methods to pass information between the server and the client. For now, I have a class called MessageHub containing just one method, SendMessage, with the following code:

var hubContext = GlobalHost.ConnectionManager.GetHubContext<MessageHub>();
hubContext.Clients.All.ReceiveMessage(message);

The Clients.All approach avoids the use of SignalR groups, which we don’t need because Time Tortoise only has one client. ReceiveMessage is a method defined in the client code.

Startup: The SignalR infrastructure requires a class called Startup that defines a Configuration method. This method is automatically called by SignalR as the server starts up:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR();
    }
}

This Configuration implementation just maps all hubs to the default endpoint at /signalr.

SignalRServer: The server class contains a method to start the server process at a particular address and port:

public void StartServer()
{
    const string url = "http://127.0.0.1:8080";

    using (WebApp.Start(url))
    {
        _messageHub = new MessageHub();
        Thread.Sleep(Timeout.Infinite);
    }
}

WebApp is the OWIN host that allows SignalR to run without an IIS server. It needs to continue running in order for the SignalR server to keep receiving connections. For now, I’m handling this by putting the foreground thread to sleep.

The other method in the server just passes SendMessage calls to the Hub:

_messageHub.SendMessage(message);

Client

On startup, the client needs to do a few things:

  • Create a hub connection to the address and port that the server is listening on.
  • Create a hub proxy using the hub name defined by the server.
  • Using the hub proxy, define which local method will be called when a particular event name request arrives from the server.
  • Start the hub connection.

The code looks like this:

_hubConnection = new HubConnection("http://127.0.0.1:8080");
_hubProxy = _hubConnection.CreateHubProxy("MessageHub");
_receiveMessageHandler =
    _hubProxy.On<string>("ReceiveMessage", ReceiveMessage);
_hubConnection.Start();

The local ReceiveMessage method can now do what it needs to with the data passed by the server. For now, I’m just adding the message to a queue:

private void ReceiveMessage(string message)
{
    Messages.Enqueue(message);
}

Testing

Soon the server class will be used by the Time Tortoise Companion WPF app, and the client class will be used by the Time Tortoise UWP app. For testing purposes, I’m using two console apps, one for the server and the other for the client.

Server test app

The first use case for Time Tortoise Companion will be to continually push user idle time data to Time Tortoise. To simulate that process in the console app test, I’ll just push the current system clock time.

The Main method in a .NET console app runs on a single thread. We need a new thread for the SignalR server, to keep the main thread free to send messages. Here’s an easy way to run a method on a different thread in .NET 4.x:

var p = new Program();
Task.Run(() => p.StartServer());

Now the server is running, and we can use it to send messages to the client. This code sends the current date/time once per second:

while (true)
{
    Thread.Sleep(1000);
    server.SendMessage(DateTime.Now.ToString());
}

Client test app

The purpose of the client test app is to do something with the message from the server. The simplest thing to do is to dequeue messages and write them to the console, so that’s what the client test app does.

Next Steps

Now that minimal SignalR functionality is working, the next steps are:

  • Do more in-depth testing to make sure the SignalR approach avoids the problems that I found with regular sockets.
  • Consume SignalRServer from Time Tortoise Companion (WPF), and SignalRClient from Time Tortoise (UWP).

(Image credit: Stephen Edmonds)