SignalR Messaging with console server and client, web client, WPF client

The code in this post is a basic example of a SignalR messaging system. I wanted to create an example which works for 3 client types, WPF, Web and Console. It is built upon the hello worlds provided on the asp.net/signalR website. The hub was then changed to add 3 methods, a simple method with no parameters, a simple method with a DTO parameter and a simple method with 2 parameters. All clients (Web, .NET, WPF) provide example code to send and receive the 3 different message types. The server can also intercept the messages and send messages to the clients.

This example is my basic Hello World for SignalR. Only broadcast messages types are sent with no security, or user ids.

Code: https://github.com/damienbod/SignalRMessaging

The Server
The console server is built using the example here: SignalR Self hosted server

SignalRMessagesServer

The Hub

    public class MyHub : Hub
    {
        public void AddMessage(string name, string message)
        {
            Console.WriteLine("Hub AddMessage {0} {1}\n", name, message);
            Clients.All.addMessage(name, message);
        }

        public void Heartbeat()
        {
            Console.WriteLine("Hub Heartbeat\n");
            Clients.All.heartbeat();
        }

        public void SendHelloObject(HelloModel hello)
        {
            Console.WriteLine("Hub hello {0} {1}\n", hello.Molly, hello.Age );
            Clients.All.sendHelloObject(hello);
        }

        public override Task OnConnected()
        {
            Console.WriteLine("Hub OnConnected {0}\n", Context.ConnectionId);
            return (base.OnConnected());
        }

        public override Task OnDisconnected()
        {
            Console.WriteLine("Hub OnDisconnected {0}\n", Context.ConnectionId);
            return (base.OnDisconnected());
        }

        public override Task OnReconnected()
        {
            Console.WriteLine("Hub OnReconnected {0}\n", Context.ConnectionId);
            return (base.OnDisconnected());
        }
    }

The Startup Configuration

using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Cors;
using Owin;

namespace MSExampleSignalR
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Branch the pipeline here for requests that start with "/signalr"
            app.Map("/signalr", map =>
            {
                // Setup the CORS middleware to run before SignalR.
                // By default this will allow all origins. You can 
                // configure the set of origins and/or http verbs by
                // providing a cors options with a different policy.
                map.UseCors(CorsOptions.AllowAll);
                var hubConfiguration = new HubConfiguration
                {
                    // You can enable JSONP by uncommenting line below.
                    // JSONP requests are insecure but some older browsers (and some
                    // versions of IE) require JSONP to work cross domain
                    // EnableJSONP = true
                };
                // Run the SignalR pipeline. We're not using MapSignalR
                // since this branch already runs under the "/signalr"
                // path.

                hubConfiguration.EnableDetailedErrors = true;
                map.RunSignalR(hubConfiguration);
            });
        }
    }
}

Running in Main

            string url = "http://localhost:8089";
            using (WebApp.Start(url))
            {
                Console.WriteLine("Server running on {0}", url);
                while (true)
                {
                    string key = Console.ReadLine();
                    if (key.ToUpper() == "W")
                    {
                        IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
                        hubContext.Clients.All.addMessage("server", "ServerMessage");
                        Console.WriteLine("Server Sending addMessage\n");
                    }
                    if (key.ToUpper() == "E")
                    {
                        IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
                        hubContext.Clients.All.heartbeat();
                        Console.WriteLine("Server Sending heartbeat\n");
                    }
                    if (key.ToUpper() == "R")
                    {
                        IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<MyHub>();

                        var vv = new HelloModel {Age = 37, Molly = "pushed direct from Server "};

                        hubContext.Clients.All.sendHelloObject(vv);
                        Console.WriteLine("Server Sending sendHelloObject\n");
                    }
                    if (key.ToUpper() == "C")
                    {
                        break;
                    }
                }

                Console.ReadLine();
            }

The DTO

This class is required in all clients just like a WCF service. The DTO needs to be backwards compatible but no support is provided with SignalR unlike WCF services.

namespace MSExampleSignalR.Dto
{
    public class HelloModel
    {
        public string Molly { get; set; }

        public int Age { get; set; }
    }
}

The Web Client

This is probably the most robust of the different clients. It’s easy to implement and has lots of documentation on how to implement the different use cases.

SignalRMessagesWebClient

<!DOCTYPE html>
<html>
<head>
    <title>SignalR Simple Chat</title>
    <style type="text/css">
        .container {
            background-color: #99CCFF;
            border: thick solid #808080;
            padding: 20px;
            margin: 20px;
        }
    </style>
</head>
    <body>
        <div class="container">
            <input type="button" id="heartbeat" value="Heartbeat" />
            <input type="hidden" id="displayname" />
            
            <input type="button" id="sendHelloObject" value="sendHelloObject" />
            <input type="hidden" id="displayname" />
            
            <input type="text" id="message" />
            <input type="button" id="sendmessage" value="AddMessage" />
            <input type="hidden" id="displayname" />
        </div>
        <div class="container">
           
            <ul id="discussion"></ul>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="~/Scripts/jquery-1.6.4.js"></script>
        <!--Reference the SignalR library. -->
        <script src="~/Scripts/jquery.signalR-2.0.0.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="http://localhost:8089/signalr/hubs"></script>
        <!--Add script to update the page and send messages.-->
        <script type="text/javascript">
            $(function () {
                //Set the hubs URL for the connection
                $.connection.hub.url = "http://localhost:8089/signalr";
         
                // Declare a proxy to reference the hub.
                var chat = $.connection.myHub;

                // Create a function that the hub can call to broadcast messages.
                chat.client.addMessage = function (name, message) {
                    // Html encode display name and message.
                    var encodedName = $('<div />').text(name).html();
                    var encodedMsg = $('<div />').text(message).html();
                    // Add the message to the page.
                    $('#discussion').append('<li><strong>Recieved addMessage' + encodedName
                        + '</strong>&nbsp;&nbsp;' + encodedMsg + '</li>');
                };
            
                chat.client.sendHelloObject = function (hello) {
                    // Html encode display name and message.
                    var encodedName = $('<div />').text(hello.Molly).html();
                    var encodedMsg = $('<div />').text(hello.Age).html();
                    // Add the message to the page.
                    $('#discussion').append('<li><strong>Recieved sendHelloObject:' + encodedName + '</strong>:&nbsp;&nbsp;' + encodedMsg + '</li>');
                };

                chat.client.heartbeat = function () {
                    // Html encode display name and message.
                    var encodedName = $('<div />').text("heartbeat").html();
         
                    // Add the message to the page.
                    $('#discussion').append('<li><strong>Recieved ' + encodedName + '</strong></li>');
                };
            
                // Get the user name and store it to prepend to messages.
                $('#displayname').val(prompt('Enter your name:', ''));
                // Set initial focus to message input box.
                $('#message').focus();
                // Start the connection.
                $.connection.hub.start().done(function () {
                    $('#sendmessage').click(function () {
                        // Call the Send method on the hub.
                        chat.server.addMessage($('#displayname').val(), $('#message').val());
                        // Clear text box and reset focus for next comment.
                        $('#message').val('').focus();
                    });
                    
                    $('#heartbeat').click(function () {
                        // Call the Send method on the hub.
                        chat.server.heartbeat();
                        // Clear text box and reset focus for next comment.
                        $('#message').val('').focus();
                    });
                              
                    $('#sendHelloObject').click(function () {
                        // Call the Send method on the hub.
                        chat.server.sendHelloObject({ Age: 2, Molly: $('#message').val() });   
                        // Clear text box and reset focus for next comment.
                        $('#message').val('').focus();
                    });
                });
            });
        </script>
    </body>
</html>


The WPF Client

The client is easy to implement but would require much more error handling and a better abstraction between the messages and the binding.

SignalRMessagesWPfClient

The XAML file

<Window x:Class="SignalRClientWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="625" Loaded="ActionWindowLoaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"  />
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" MinWidth="149" />
        </Grid.ColumnDefinitions>

        <Label Content="Message:" Grid.Column="0" Grid.Row="0" Margin="5" />
        <TextBox Width="Auto" Grid.Column="1" Grid.Row="0" Margin="5,5,10,5" x:Name="ClientNameTextBox"  />
        <TextBox Width="Auto" Grid.Column="2" Grid.Row="0" Margin="5,5,10,5" x:Name="MessageTextBox" KeyDown="ActionMessageTextBoxOnKeyDown" />
        <Button Content="AddMessage" Grid.Column="3" Grid.Row="0" Margin="17,5" x:Name="SendButton" Click="ActionSendButtonClick" />

        <Label Content="Heartbeat:" Grid.Column="0" Grid.Row="1" Margin="5" />
        <Button Content="Send Heartbeat" Grid.Column="3" Grid.Row="1" Margin="17,5" x:Name="HeartbeatButton" Click="ActionHeartbeatButtonClick" />

        <Label Content="Age, Molly:" Grid.Column="0" Grid.Row="2" Margin="5" />
        <TextBox Width="Auto" Grid.Column="1" Grid.Row="2" Margin="5,5,10,5" x:Name="HelloTextBox"  />
        <TextBox Width="Auto" Grid.Column="2" Grid.Row="2" Margin="5,5,10,5" x:Name="HelloMollyTextBox" KeyDown="ActionMessageTextBoxOnKeyDown" />
        <Button Content="Send Object" Grid.Column="3" Grid.Row="2" Margin="17,5" x:Name="HelloButton" Click="ActionSendObjectButtonClick" />

        <ListBox Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="4" Margin="5" x:Name="MessagesListBox" />
    </Grid>
</Window>

XAML code

using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.AspNet.SignalR.Client;
using Microsoft.AspNet.SignalR.Client.Hubs;
using SignalRClientWPF.Dto;

namespace SignalRClientWPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public System.Threading.Thread Thread { get; set; }
        public string Host = "http://localhost:8089/";

        public IHubProxy Proxy { get; set; }
        public HubConnection Connection { get; set; }

        public bool Active { get; set; }

        public MainWindow()
        {
            InitializeComponent();
        }


        private async void ActionHeartbeatButtonClick(object sender, RoutedEventArgs e)
        {
            await SendHeartbeat();
        }

        private async void ActionSendButtonClick(object sender, RoutedEventArgs e)
        {
            await SendMessage();
        }

        private async void ActionSendObjectButtonClick(object sender, RoutedEventArgs e)
        {
            await SendMessage();
        }     

        private async Task SendMessage()
        {
            await Proxy.Invoke("Addmessage", ClientNameTextBox.Text , MessageTextBox.Text);
        }

        private async Task SendHeartbeat()
        {
            await Proxy.Invoke("Heartbeat");
        }

        private async Task SendHelloObject()
        {
            HelloModel hello = new HelloModel { Age = Convert.ToInt32(HelloTextBox.Text), Molly = HelloMollyTextBox.Text };
            await Proxy.Invoke("sendHelloObject", hello);
        }

        private async void ActionWindowLoaded(object sender, RoutedEventArgs e)
        {
            Active = true;
            Thread = new System.Threading.Thread(() =>
            {
                Connection = new HubConnection(Host);
                Proxy = Connection.CreateHubProxy("MyHub");

                Proxy.On<string, string>("addmessage", (name, message) => OnSendData("Recieved addMessage: " + name + ": " + message ));
                Proxy.On("heartbeat", () => OnSendData("Recieved heartbeat"));
                Proxy.On<HelloModel>("sendHelloObject", hello => OnSendData("Recieved sendHelloObject " + hello.Molly +  " " + hello.Age));

                Connection.Start();

                while (Active)
                {
                    System.Threading.Thread.Sleep(10);
                }
            }) { IsBackground = true };
            Thread.Start();

        }

        private void OnSendData(string message)
        {
            Dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => MessagesListBox.Items.Insert(0, message)));
        }

        private async void ActionMessageTextBoxOnKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter || e.Key == Key.Return)
            {
                await SendMessage();
                MessageTextBox.Text = "";
            }
        }
    }
}

The Console Client

This client works, but the error handling, start up threads need to be implemented.

SignalRMessagesConsoleClient

using System;
using System.Threading;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Client;
using SignalRClientConsole.Dto;

namespace SignalRClientConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting client  http://localhost:8089");
            
            var hubConnection = new HubConnection("http://localhost:8089/");
            //hubConnection.TraceLevel = TraceLevels.All;
            //hubConnection.TraceWriter = Console.Out;
            IHubProxy myHubProxy = hubConnection.CreateHubProxy("MyHub");

            myHubProxy.On<string, string>("addMessage", (name, message) => Console.Write("Recieved addMessage: " + name + ": " + message + "\n"));
            myHubProxy.On("heartbeat", () => Console.Write("Recieved heartbeat \n"));
            myHubProxy.On<HelloModel>("sendHelloObject", hello => Console.Write("Recieved sendHelloObject {0}, {1} \n", hello.Molly, hello.Age));

            hubConnection.Start().Wait();

            while (true)
            {
                string key = Console.ReadLine();
                if (key.ToUpper() == "W")
                {
                    myHubProxy.Invoke("addMessage", "client message", " sent from console client").ContinueWith(task =>
                    {
                        if (task.IsFaulted)
                        {
                            Console.WriteLine("!!! There was an error opening the connection:{0} \n", task.Exception.GetBaseException());
                        }

                    }).Wait();
                    Console.WriteLine("Client Sending addMessage to server\n");
                }
                if (key.ToUpper() == "E")
                {
                    myHubProxy.Invoke("Heartbeat").ContinueWith(task =>
                    {
                        if (task.IsFaulted)
                        {
                            Console.WriteLine("There was an error opening the connection:{0}", task.Exception.GetBaseException());
                        }

                    }).Wait();
                    Console.WriteLine("client heartbeat sent to server\n");
                }
                if (key.ToUpper() == "R")
                {
                    HelloModel hello = new HelloModel { Age = 10, Molly = "clientMessage" };
                    myHubProxy.Invoke<HelloModel>("SendHelloObject", hello).ContinueWith(task =>
                    {
                        if (task.IsFaulted)
                        {
                            Console.WriteLine("There was an error opening the connection:{0}", task.Exception.GetBaseException());
                        }

                    }).Wait();
                    Console.WriteLine("client sendHelloObject sent to server\n");
                }
                if (key.ToUpper() == "C")
                {
                    break;
                }
            }

        }
    }
}

My Thoughts

SignalR is very easy to start using, but to use in a production application, the lifecycle events, the details need to be implemented.

My next steps are to create a robust messaging system using SignalR and then to do performance testing. (Max number of messages, max amount of data.)

This is a very exciting technology, which if stable offers a great alternative to WCF full-duplex communication.

Links:

http://www.asp.net/signalr

http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/introduction-to-signalr

http://signalr.net

About these ads

3 comments

  1. Good article bro ! Two thumbs up !

  2. lukelookli · · Reply

    Good Stuff! thanks.

  3. Richard · · Reply

    Thanks very much for this. I was trying to follow along with the asp.net signal r how-to articles but their code is just plan wrong re: SignalR console clients. Looking at the diff it seems they have left out simple things like .Wait() on the hubConnection.Start().Wait() which means the client just returns and nothing actually happens. Now I have enough of the pieces I can go ahead and explore Signal R, so tyvm.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 40 other followers

%d bloggers like this: