I am a SignalR junkie. Having an application provide real time updates opens up a ton of opportunities. I would even go as far as to say it is the way things are supposed to be — its a much more natural experience for the user to have pertinent information show up automatically. I usually go out of my way to figure out a way to include SignalR in things I build just because it is so easy to get working and the effect is downright magical.
Backstory
There was a need for a basic viewer to see the current status of the overall environment. The current state of DEV, TEST, and STAGE (maybe known to other organizations as PRE PROD or something) has been kind of a mystery. You could look at the individual builds, but they might not tell the entire story. Most of the teams at work have a projector or TV monitor driven by a computer to display their task boards, so it seemed to me that creating a simple, small WPF “stoplight” application that could live in the corner would be just the ticket.
All of the code is available on GitHub.
The Server
There are a few different ways to host a SignalR application. In my opinion, the easiest is just to add it to your existing ASP.NET application (since SignalR is part of the ASP.NET stack). But, just to show that it can be done, we’ll use OWIN and do a self-hosted app.
There’s a great tutorial from Microsoft on how to do this, but I’ll go over it again here with all the code I used to get this going.
Step 1: Create a console application
Step 2: Grab some NuGet packages.
SignalR Core – The main components of SignalR that make the magic happen.
SignalR Self Host – The stuff you need to run the self hosting bits.
Step 3: Initialize the server
[code language=”csharp”]
using Microsoft.Owin.Hosting;
using Owin;
using System;
namespace Server
{
class Program
{
static void Main(string[] args)
{
string url = "http://localhost:8616";
using (WebApp.Start(url))
{
Console.Title = $"Server running on {url}";
}
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
[/code]
Step 4: Initialize the SignalR Hub
You don’t have to override the OnConnected task, but doing so will allow you to keep track of who is connected. By default, SignalR doesn’t keep track of how many connections there are, so if you want to know, you’ll have to do it yourself.
[code language=”csharp”]
using Microsoft.AspNet.SignalR;
using System;
using System.Threading.Tasks;
namespace Server
{
public class EnvironmentStatusHub : Hub
{
public override Task OnConnected()
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"*** Client {Context.ConnectionId} connected.");
Console.ForegroundColor = ConsoleColor.White;
return base.OnConnected();
}
}
}
[/code]
Step 5: Create a “Hub Accessor”
Sometimes you need to access the hub to send messages to the connected clients from outside the hub itself. In the case of a ASP.NET application, this might be from a REST call to the server that provides data you want to send to connected clients. In this case, we will be accepting input from the console window to update the status of an environment. Accessing a hub is easy, but I like to wrap it in a static class so that it is easy to invoke when we need it.
[code language=”csharp”]
public static class EnvironmentStatusHubAccessor
{
private static IHubContext Context
{
get
{
return GlobalHost.ConnectionManager.GetHubContext<EnvironmentStatusHub>();
}
}
public static void UpdateEnvironment(string environment, string status)
{
Context.Clients.All.UpdateEnvironment(environment, status);
}
}
[/code]
GlobalHost is how we can get to the hub itself. Getting the hub context like in the code above will provide a IHubContext object, which will give you access to send messages to the connected clients. For the “Hub Accessor”, I usually follow a pattern where the method name and the hub method name are the same, just to keep things consistent.
Step 6: Get input and pass the information on to the clients
I wrote this little function to get the input from an array of strings. The information is then sent over to the Hub Accessor, and the thus sent to the connected clients.
[code language=”csharp”]
static void Main(string[] args)
{
string url = "http://localhost:8616";
using (WebApp.Start(url))
{
Console.Title = $"Server running on {url}. Press [ESC] to exit";
ConsoleKeyInfo cki = new ConsoleKeyInfo();
while (cki.Key != ConsoleKey.Escape)
{
Console.Clear();
Console.WriteLine("Pick an environment: ");
var env = GetInput("Dev", "Test", "Stage");
Console.WriteLine("Pick a status: ");
var status = GetInput("Good", "Progress", "Broken");
Console.WriteLine($"Setting {env} to {status}.");
EnvironmentStatusHubAccessor.UpdateEnvironment(env, status);
Console.WriteLine("Press any key to continue.");
cki = Console.ReadKey();
}
}
}
static string GetInput(params string[] choices)
{
ConsoleKeyInfo cki = new ConsoleKeyInfo();
for (int idx = 0; idx < choices.Length; idx++)
Console.WriteLine($"t{idx + 1}: {choices[idx]}");
while (!(cki.Key >= ConsoleKey.D1 && cki.Key < (ConsoleKey.D1 + choices.Length))) cki = Console.ReadKey(true);
return choices[cki.Key – (ConsoleKey.D1)];
}
[/code]
The Client
I’ve decided to do a simple WPF app because it can easily run on a desktop computer (like the ones that this would probably run on), and it can be programmed to do some more robust connection logic in case the server is taken down. For the purposes of this, I haven’t implemented any of that, but it would be fairly easy to implement.
Step 1: Create a new WPF application
I named mine EnvironmentStatusViewer.
Step 2: Create a View Model.
Sometimes I get a little carried away with how things looked, so I created a way for the “stoplights” to be linear gradients. Data binding in WPF means that this is pretty easy, but you do have to do the whole INotifyProperty changed dance. There are a lot of good resources on how to set this up, so, for now, here’s the code.
[code language=”csharp”]
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Media;
namespace EnvironmentStatusViewer
{
public class EnvironmentStatusViewModel : INotifyPropertyChanged
{
private static GradientStopCollection GoodColor = new GradientStopCollection(new GradientStop[] {
new GradientStop((Color)ColorConverter.ConvertFromString("#FF11F311"), 0.5),
new GradientStop((Color)ColorConverter.ConvertFromString("#FF128D12"), 1.0)
});
private static GradientStopCollection ProgressColor = new GradientStopCollection(new GradientStop[] {
new GradientStop((Color)ColorConverter.ConvertFromString("#FFF3F311"), 0.5),
new GradientStop((Color)ColorConverter.ConvertFromString("#FF8D8D12"), 1.0)
});
private static GradientStopCollection BrokenColor = new GradientStopCollection(new GradientStop[] {
new GradientStop((Color)ColorConverter.ConvertFromString("#FFF31111"), 0.5),
new GradientStop((Color)ColorConverter.ConvertFromString("#FF8D1212"), 1.0)
});
private string _devStatus;
public string DevStatus
{
get { return _devStatus; }
set
{
SetProperty(ref _devStatus, value);
if (_devStatus.IndexOf("Broken", StringComparison.InvariantCultureIgnoreCase) >= 0) DevColor = BrokenColor;
if (_devStatus.IndexOf("Progress", StringComparison.InvariantCultureIgnoreCase) >= 0) DevColor = ProgressColor;
if (_devStatus.IndexOf("Good", StringComparison.InvariantCultureIgnoreCase) >= 0) DevColor = GoodColor;
}
}
private GradientStopCollection _devColor = GoodColor;
public GradientStopCollection DevColor
{
get { return _devColor; }
private set
{
SetProperty(ref _devColor, value);
}
}
private string _testStatus;
public string TestStatus
{
get { return _testStatus; }
set
{
SetProperty(ref _testStatus, value);
if (_testStatus.IndexOf("Broken", StringComparison.InvariantCultureIgnoreCase) >= 0) TestColor = BrokenColor;
if (_testStatus.IndexOf("Progress", StringComparison.InvariantCultureIgnoreCase) >= 0) TestColor = ProgressColor;
if (_testStatus.IndexOf("Good", StringComparison.InvariantCultureIgnoreCase) >= 0) TestColor = GoodColor;
}
}
private GradientStopCollection _testColor = GoodColor;
public GradientStopCollection TestColor
{
get { return _testColor; }
private set
{
SetProperty(ref _testColor, value);
}
}
private string _stageStatus;
public string StageStatus
{
get { return _stageStatus; }
set
{
SetProperty(ref _stageStatus, value);
if (_stageStatus.IndexOf("Broken", StringComparison.InvariantCultureIgnoreCase) >= 0) StageColor = BrokenColor;
if (_stageStatus.IndexOf("Progress", StringComparison.InvariantCultureIgnoreCase) >= 0) StageColor = ProgressColor;
if (_stageStatus.IndexOf("Good", StringComparison.InvariantCultureIgnoreCase) >= 0) StageColor = GoodColor;
}
}
private GradientStopCollection _stageColor = GoodColor;
public GradientStopCollection StageColor
{
get { return _stageColor; }
private set { SetProperty(ref _stageColor, value); }
}
public void SetProperty<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (property != null && property.Equals(value))
return;
property = value;
NotifyPropertyChanged(propertyName);
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
[/code]
When the environment status is changed, it also updates the color for the environment. There are definitely better ways of doing this, but for a quick-and-dirty implementation it isn’t too bad.
Step 3: Add the view model to the Code Behind of the view.
[code language=”csharp”]
using System.Threading.Tasks;
using System.Windows;
namespace EnvironmentStatusViewer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public EnvironmentStatusViewModel ViewModel = new EnvironmentStatusViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = ViewModel;
ViewModel.DevStatus = "Good";
ViewModel.TestStatus = "Good";
ViewModel.StageStatus = "Good";
}
}
}
[/code]
Step 4: Create the view
[code language=”xml”]
<Window x:Class="EnvironmentStatusViewer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:EnvironmentStatusViewer"
mc:Ignorable="d"
Title="Status" Width="160" Height="160" ResizeMode="NoResize" WindowStyle="ToolWindow" Background="{x:Null}" BorderThickness="0">
<Window.DataContext>
<local:EnvironmentStatusViewModel/>
</Window.DataContext>
<Grid>
<Grid.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF818181" Offset="0"/>
<GradientStop Color="#FFB8B8B8" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontSize="32" FontWeight="Bold" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="0">DEV</TextBlock>
<Ellipse Width="32" Height="32 " Grid.Column="1" Grid.Row="0" Margin="5,5,0,0">
<Ellipse.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0" GradientStops="{Binding DevColor, Mode=OneWay}"/>
</Ellipse.Fill>
</Ellipse>
<TextBlock FontSize="32" FontWeight="Bold" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="1">TEST</TextBlock>
<Ellipse Width="32" Height="32 " Grid.Column="1" Grid.Row="1" Margin="5,5,0,0">
<Ellipse.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0" GradientStops="{Binding TestColor, Mode=OneWay}"/>
</Ellipse.Fill>
</Ellipse>
<TextBlock FontSize="32" FontWeight="Bold" HorizontalAlignment="Right" Grid.Column="0" Grid.Row="2">STAGE</TextBlock>
<Ellipse Width="32" Height="32 " Grid.Column="1" Grid.Row="2" Margin="5,5,0,0">
<Ellipse.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0" GradientStops="{Binding StageColor, Mode=OneWay}"/>
</Ellipse.Fill>
</Ellipse>
</Grid>
</Window>
[/code]
Step 5: Wire up the SignalR client
First you need to install the NuGet package Microsoft.AspNet.SignalR.Client. This will give you all the stuff you need to connect to the SignalR Hub.
Next you need to create the client to connect to the Hub, and figure out what to do when your client receives a message from the server.
[code language=”csharp”]
using Microsoft.AspNet.SignalR.Client;
using System;
using System.Threading.Tasks;
namespace EnvironmentStatusViewer
{
public class Client
{
private readonly HubConnection HubConnection;
private IHubProxy HubProxy;
private const string HOST_URL = "http://localhost:8616";
public Client()
{
HubConnection = new HubConnection(HOST_URL);
}
public async Task Start(Action<string, string> UpdateEnvironmentStatus)
{
HubProxy = HubConnection.CreateHubProxy("EnvironmentStatusHub");
HubProxy.On("UpdateEnvironment", UpdateEnvironmentStatus);
await HubConnection.Start();
}
}
}
[/code]
I opted to pass the event handlers to the Start function in the client so whomever is instantiating the client can create the action to take using a lambda function.
That means that you need to start the client in the Code Behind of the view, and wire up the function to invoke when the client receives the message from the server.
[code language=”csharp”]
public partial class MainWindow : Window
{
public EnvironmentStatusViewModel ViewModel = new EnvironmentStatusViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = ViewModel;
ViewModel.DevStatus = "Good";
ViewModel.TestStatus = "Good";
ViewModel.StageStatus = "Good";
var Client = new Client();
// Start the client asynchronously to not lock up the UI
Task.Run(async () =>
{
await Client.Start(UpdateEnvironmentStatus);
});
}
private void UpdateEnvironmentStatus(string environment, string status)
{
switch (environment.ToLower())
{
case "dev":
ViewModel.DevStatus = status;
break;
case "test":
ViewModel.TestStatus = status;
break;
case "stage":
ViewModel.StageStatus = status;
break;
}
}
}
[/code]
In Action
And that’s it! There are a lot more things you could do to make this more robust and, in general, better.
All of the code is available on GitHub.