Sunday, February 16, 2020

Build a simple Sketchpad app with SignalR in ASP.NET Core 3.1

Companion Video: https://youtu.be/ktPTkISof4k
Source Code: https://github.com/medhatelmasry/SignalrSketchpad

What is SignalR

ASP.NET SignalR is a library to add real-time web functionality to applications. Real-time web functionality is the ability to have server-side code push content to the connected clients as it happens, in real-time. It essentially allows your server-side C# code to invoke client-side JavaScript functions and vice-versa.

SignalR takes advantage of several transports, automatically selecting the best available transport given the client's and server's capabilities. SignalR takes advantage of WebSockets, an HTML5 API that enables bi-directional communication between the browser and server. SignalR will use WebSockets under the covers when it's available, and gracefully fall back to other techniques and technologies when it is not, while the application code remains the same.

SignalR also provides a simple, high-level API for doing server-to-client RPC (call JavaScript functions in a client's browser from server-side .NET code) in an ASP.NET application, as well as adding useful hooks for connection management, such as connect/disconnect events, grouping connections, authorization, etc.

Assumptions

It is assumed that you have .NET Core 3.1 and VS Code installed on your computer. The following walkthrough works well on Linux, Mac and Windows 10.

The Mission

We will build an ASP.NET Code 3.1 application that allows multiple users, using multiple browsers, to share and scribble on the same canvas.

Getting Started

Let's get started. Go into a terminal windows at suitable workspace folder on your computer and create an ASP.NET Core 3.1 web app as follows:
mkdir SignalrSketchpad
cd SignalrSketchpad
dotnet new webapp --no-https

We will use Library Manager (LibMan) to get the client library from unpkg. unpkg is a content delivery network (CDN)) that can deliver anything found in npm, the Node.js package manager.
Run the following command if you do not already have LibMan installed on your computer:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
Run the following command in the root folder of the SignalrSketchpad project to get the SignalR JavaScript client library by using LibMan. You might have to wait a few seconds before seeing output.
libman install @aspnet/signalr -p unpkg -d wwwroot/lib/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js
The above parameters specify the following options:
- Use the unpkg provider.
- Copy files to the wwwroot/lib/signalr destination.
- Copy only the specified files.

In the SignalrSketchpad project folder, create a Hubs folder. In the Hubs folder, create a DrawDotHub.cs file with the following code:
public class DrawDotHub: Hub {
   public async Task UpdateCanvas(int x, int y) {
      await Clients.All.SendAsync("updateDot",x, y);
   }

   public async Task ClearCanvas() {
      await Clients.All.SendAsync("clearCanvas");
   }
}

The DrawDotHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and messaging.

The UpdateCanvas and ClearCanvas methods can be called by a connected JavaScript client to draw and clear drawings respectively on all clients.

Configure SignalR

Append this code to the ConfigureServices() method in Startup.cs:
services.AddSignalR();
Add this code to the Configure() method in Startup.cs. Put it inside the app.UseEndpoints() block:
endpoints.MapHub<DrawDotHub>("/drawDotHub");

Add SignalR client code

Replace contents of Pages\Index.cshtml with the following code:
@page
<style>
        /* Some CSS styling */
        .rightside {
            float: left;
            margin-left: 10px;
        }

        #sketchpad {
            float: left;
            height: 300px;
            width: 600px;
            border: 2px solid #888;
            border-radius: 4px;
            position: relative; /* Necessary for correct mouse co-ords in Firefox */
        }

        #clear_button, #save_button {
            float: left;
            font-size: 15px;
            padding: 10px;
            -webkit-appearance: none;
            background: #feee;
            border: 1px solid #888;
            margin-bottom: 5px;
        }
</style
<h1>SignalR Sketchpad</h1
<div id="sketchpadapp">
        <div class="rightside">
            <button id="clear_button" onclick="tellServerToClear()">Clear Canvas</button>
            <br />
            <canvas id="sketchpad" width="600" height="300"></canvas>
        </div
</div>

<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/draw.js"></script>

The preceding code:
- Creates a drawing canvas with id = sketchpad.
- Creates a “Clear Canvas” button right above the canvas that calls a function named tellServerToClear().
- Includes script references to SignalR and the draw.js application code that will be created in the next step.
- JavaScript files signalr.js and draw.js are loaded

We will not need Pages/Index.cshtml.cs so you can go ahead and delete it. 

In the wwwroot/js folder, create a file named draw.js with the following code: 

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/drawDotHub").build();

connection.on("updateDot", function (x, y) {
    drawDot(x, y, 8);
});

connection.on("clearCanvas", function () {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
});

connection.start().then(function () {
    // nothing here
}).catch(function (err) {
    return console.error(err.toString());
});

function tellServerToClear() {
    connection.invoke("ClearCanvas").catch(function (err) {
        return console.error(err.toString());
    });
}
//////////////////////////////////////////////////////
// Variables for referencing the canvas and 2dcanvas context
var canvas, ctx;
// Variables to keep track of the mouse position and left-button status
var mouseX, mouseY, mouseDown = 0;
// Draws a dot at a specific position on the supplied canvas name
// Parameters are: A canvas context, the x position, the y position, the size of the dot
function drawDot(x, y, size) {
    // Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
    var r = 0;
    var g = 0;
    var b = 0;
    var a = 255;
    // Select a fill style
    ctx.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (a / 255) + ")";
    // Draw a filled circle
    ctx.beginPath();
    ctx.arc(x, y, size, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}

// Keep track of the mouse button being pressed and draw a dot at current location
function sketchpad_mouseDown() {
    mouseDown = 1;
    drawDot(mouseX, mouseY, 8);

    connection.invoke("UpdateCanvas", mouseX, mouseY).catch(function (err) {
        return console.error(err.toString());
    });
}

// Keep track of the mouse button being released
function sketchpad_mouseUp() {
    mouseDown = 0;
}

// Keep track of the mouse position and draw a dot if mouse button is currently pressed
function sketchpad_mouseMove(e) {
    // Update the mouse co-ordinates when moved
    getMousePos(e);
    // Draw a dot if the mouse button is currently being pressed
    if (mouseDown == 1) {
        drawDot(mouseX, mouseY, 8);
        connection.invoke("UpdateCanvas", mouseX, mouseY).catch(function (err) {
            return console.error(err.toString());
        });
    }
}

// Get the current mouse position relative to the top-left of the canvas
function getMousePos(e) {
    if (!e)
        var e = event;
    if (e.offsetX) {
        mouseX = e.offsetX;
        mouseY = e.offsetY;
    }
    else if (e.layerX) {
        mouseX = e.layerX;
        mouseY = e.layerY;
    }
}

// Set-up the canvas and add our event handlers after the page has loaded
// Get the specific canvas element from the HTML document
canvas = document.getElementById('sketchpad');
// If the browser supports the canvas tag, get the 2d drawing context for this canvas
if (canvas.getContext)
    ctx = canvas.getContext('2d');

// Check that we have a valid context to draw on/with before adding event handlers
if (ctx) {
    // React to mouse events on the canvas, and mouseup on the entire document
    canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
    canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
    window.addEventListener('mouseup', sketchpad_mouseUp, false);
} else {
    document.write("Browser not supported!!");
}

The preceding code:
- gets a 2d handle to the canvas element in the page
- sets event listeners for mousedown, mousemove and mouseup events
- a connection is established with the server-side hub at endpoint /drawDotHub
- whenever the server invokes a function named updateDot() on the client, then the drawDot() JavaScript function is called
- whenever the server invokes a function named clearCanvas() on the client, then the clearCanvas() JavaScript statement is executed
- connection.start() establishes a live connection with the server
- JavaScript function tellServerToClear() invokes method ClearCanvas() on the server
- JavaScript function sketchpad_mouseDown() and sketchpad_mouseMove() invoke the UpdateCanvas() methods on the server.

Run the app by typing “dotnet run” in a terminal window inside the project root folder. Point your browser to http://localhost:5000. 
Open the same page in a different browser then put both browsers side-by-side. When you scribble on one canvas you will see the same sketch on the other browser.
Use the concept to build much more sophisticated SignalR apps.