loading...
Cover image for SignalR without Javascript, the Promise of Blazor

SignalR without Javascript, the Promise of Blazor

slorello profile image Stephen Lorello Originally published at slorello.com ・5 min read

SignalR is the canonical client-side async notification library for ASP.NET. With it, we can build clients that are ultra-responsive to changing conditions on our servers. But SignalR has always had one major flaw. To use it, you needed to use JavaScript. That's fair, right? We're writing web clients, which are always running in the browser, so of course, we need to use JavaScript. We'll suffer without type safety because of the functionality we're getting. It's always been a necessary evil, and we've dealt with it as such.

With the dawn of Blazor, this age of compromise is over. We can manage all of the data transfers between our servers and clients straight out of CLR types! That is what we're going to be demonstrating now.

Objectives

Let's concretize our objectives here. We're going to build a trivial Single-Paged-App(SPA) in Blazor wasm. That app will have a simple form to read inputs from users, and a table to see incoming messages.

Prerequisites

  • Latest .NET Core 3.1 SDK
  • Visual Studio Code or the latest version of Visual Studio. I will be using VS Code for this

Create Project

Navigate to your source directory in the console and run the following command.

dotnet new blazorwasm -ho -n SignalRClr

This will create a new directory SignalRClr, in that directory it will create a solution called SignalRClr and then projects:

  • SignalRClr.Shared.csproj
  • SignalRClr.Server.csproj
  • SignalRClr.Client.csproj

Those projects are what they say they are. The Shared will be the shared models between the client and the server. The client is going to be the compiled wasm that ends up in our client's browser. The Server is our server-side code. Run cd SignalRClr to navigate into the project's folder, then run code . to open it in VS Code.

Add Dependencies

We need to add a Microsoft.AspNetCore.SignalR.Client dependency to our SignalRClr.Client project, and a Microsoft.AspNetCore.SignalR.Core dependency to our SignalRClr.Server project. We will also need the System.ComponentModel.Annotations class for our Shared project. Cd into Client and run the following:

dotnet add package Microsoft.AspNetCore.SignalR.Client

Then cd into the Server directory parallel to the Client directory and run the following:

dotnet add package Microsoft.AspNetCore.SignalR.Core

Then cd into the Shared directory and run the following:

dotnet add package System.ComponentModel.Annotations

Build the Model

Add a file to our SignalRClr.Shared project folder called Message.cs. Here we'll add a simple message class that takes a UserName and Text. We'll annotate them to make both UserName and Text required.

using System.ComponentModel.DataAnnotations;
namespace SignalRClr.Shared
{
    public class Message
    {
        [Required]
        public string UserName { get; set; }
        [Required]
        public string Text { get; set; }
    }
}

Add Message Hub

In the Server Project, add a new folder called Hubs. In that folder, add a MessageHub class, we will add a new Hub class MessageHub that will only have one method SendMessage which will push the inbound message down to all of the HubConnection Client's using the SendAsync method.

using Microsoft.AspNetCore.SignalR;
using SignalRClr.Shared;
using System.Threading.Tasks;
namespace SignalRClr.Server.Hubs
{
    public class MessageHub : Hub
    {
        public async Task SendMessage(Message message)
        {
            await Clients.All.SendAsync("ReceiveMessage", message);
        }
    }
}

Configure Middleware

In the Startup.cs file, we'll need to add a couple of lines of code to enable the MessageHub. In ConfigureServices the line:

services.AddSignalR();

Then in the app.UseEndpoints's delegate in the Configure method, map the hub. Your UseEndpoints should look like:

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllers();
    endpoints.MapFallbackToFile("index.html");
    endpoints.MapHub<Hubs.MessageHub>("/messageHub"); //Add this line
});

Build the Frontend

We're going use the Client/Pages/Index.razor file for our frontend. Mind you, I'm not an expert on frontend development, so this will look simple.

Frontend

So as you can see here, we have two sections, a Messages Table and a form that we'll use to send the messages.

Pull in Dependencies

We need to add a reference to Microsoft.AspNetCore.SignalR.Client and SignalRClr.Shared. We also need to inject a NavigationManager, and to clean up the HubConnection we're going to use we'll need to implement IDisposable. We will declare all this for our Index component with the following:

@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@using SignalRClr.Shared
@inject NavigationManager NavigationManager
@implements IDisposable

Add the Messages Table

We now need to add a Messages Table to our component. One of the cool things about razor/blazor is that we can do all of this with the CLR types that we want to use. In particular, our Message Model. We'll create a table; then, in the Table's body, we'll loop through a Messages Array that we will declare later, and add the UserName and the Text as table data.

<h2>Messages</h2>
<table class="table-active">
    <thead>
        <tr>
            <th>User Name</th>
            <th>Text</th>
        </tr>
    </thead>
    <tbody>
        @foreach(var message in _messages)
        {
            <tr>
                <td>@message.UserName</td>
                <td>@message.Text</td>
            </tr>
        }
    </tbody>
</table>

Add Message Form

Next, we'll add an EditForm to provide it our Message model and add a ValidSubmit method to run when the form has successfully validated.

<EditForm Model="@_message" OnValidSubmit="SendMessage">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <h3>User Name</h3>
    <input @bind="@_message.UserName" placeholder="User Name" class="input-group-text" />
    <h3>Text</h3>
    <input @bind="@_message.Text" placeholder="User Name" class="input-group-text" />
    <br />
    <button class="btn btn-primary" type="submit">Send Message</button>
</EditForm>

Add Component Code

We will need to add a @code block to our component now. This block is all the logic our page needs to execute. We will add a _hubConnection property, which will manage the sending and receiving messages from our server over SignalR. We will add a _messages list, which is simply the messages we've received from the server. And we'll add a _message field, which will simply be the model we use for sending messages.

When initializing the Component, we will create the HubConnection, mapping it to the /messageHub URI we declared in our middleware. When that HubConnection gets a Receive Message signal, it will add the inbound message to our _messages collection and notify the component the state has changed. Naturally, when we get a valid submission from our form, we will submit that new message to the server. Finally, when finalizing the component, we will dispose of the _hubConnection. All the code to do this looks like:

@code {
    private HubConnection _hubConnection;
    private List<Message> _messages = new List<Message>();
    private Message _message = new Message();

    protected override async Task OnInitializedAsync(){
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/messageHub"))
            .Build();

        _hubConnection.On<Message>("ReceiveMessage",
            (message)=>{
                _messages.Add(message);
                StateHasChanged();
        });
        await _hubConnection.StartAsync();
    }

    public async Task SendMessage()
    {
        await _hubConnection.SendAsync("SendMessage", _message);
        _message = new Message();
    }

    public void Dispose()
    {
        _ = _hubConnection?.DisposeAsync();

    }
}

That's It

And that's it! Everything we need to do to manage simple messages in real time between clients. To run this project, you can run the Server project. Cd into the Server directory and run dotnet run, and it will launch the project. Navigate to localhost:5000 (or wherever you configure it to run kestrel) in your browser, and you will see our little message program.

Wrapping Up

Naturally, this was a simple example that explicitly shows how simple it is to use these frameworks together. But these are the building blocks of how Blazor can be used to wipe out the need for javascript when interacting with our models.

Resources

  • The source code from this demo can be found in GitHub

Posted on by:

slorello profile

Stephen Lorello

@slorello

.NET Developer Advocate at @Vonage, full-stack polyglottic Software Engineer - AI/ML grad student @GeorgiaTech. Hacker, runner, and traveler.

Discussion

pic
Editor guide
 

Nice! I'm told it's cleaner if you derive your hub from IHub<T> rather than just Hub. It made my code a little easier when I did that.

 

Hi Katie, thank you for reading, that is a valid point. While they threw out IHub<T> for SignalR Core, they replaced it with Hub<T>. You could, as you suggested, use a strongly typed interface with the Hub and drive everything through that.

I'm not entirely sold on that being cleaner, however. In the client, when you're building your HubConnection, you'd still need to tell it to register to an event with a stringified name of the called symbol.

So, if I had my Index component implement an interface IMessageClient and used a Hub extension I could call the clients like:

await Clients.All.ReceiveMessage(message);

But, I would still have to receive the message into my component like:

_hubConnection.On<Message>("ReceiveMessage", 
            (message)=>this.ReceiveMessage(message));

I found this, going from symbol => stringified Event Name versus stringified event name => stringified event name, more confusing. It's particularly odd as it doesn't enforce type-safety in the clients as there's no obligation for the client to actually implement the interface to receive the event. That's just a personal opinion, though; it could break either way.

It'd be nice if there were a HubConnection<T> in the Client library that would manage all that for you. This looks like it's something the aspnetcore team is at least considering: github.com/dotnet/aspnetcore/issue... I just upvoted this and I'd encourage you to as well :).

That's a really good suggestion though and I'd encourage anyone reading this to take it under advisement, for all we know the aforementioned issue may be resolved for .NET 5!

 

Okay, I up voted too. And, I looked at my code again and you are right, I used Hub<InterfaceType> for by hub. Also, on the client side, I'm using the @microsoft/signalr package in TypeScript. It could use the HubConnection<T> as you describe too.