Hopefully, you read Part 3 of this series and thus have a basic LED under your control, congrats!

Add SignalR Extensions

To ensure we can write everything in C#, add a reference to the Client project for the Blazor.Extensions.SignalR package: <PackageReference Include="Blazor.Extensions.SignalR" Version="0.4.0" />

In the future, this will be built-in, but for now, we must add it explicitly.

Add imports

To reference the SignalR extensions in the razor views, we need to add a @using Blazor.Extensions statement to the Client/_Imports.razor file.

Add HubConnectionBuilder

In the Client/Startup.cs file, add the using Blazor.Extensions; statement, and update the ConfigureServices method to add a transient HubConnectionBuilder to DI

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<HubConnectionBuilder>();
}

Update razor view

Now let's update the Pages/Led.razor file to hook up to SignalR.

@page "/led"
@inject HttpClient Http

<h1>Led Toggle</h1>

<p>Current On: @toggle</p>
<button class="btn btn-primary" @onclick="ToggleLed">Toggle</button>

@code {
    bool toggle = false;
    [Inject] private HubConnectionBuilder _hubConnectionBuilder { get; set; }
    private Func<bool, Task> handleSwitchStatus;

    protected override async Task OnInitializedAsync()
    {
        handleSwitchStatus += ReceiveSwitchStatus;
        var gpioConnection = _hubConnectionBuilder // the injected one from above.
        .WithUrl("/gpio", // The hub URL. If the Hub is hosted on the server where the blazor is hosted, you can just use the relative path.
        opt =>
        {
            opt.LogLevel = SignalRLogLevel.Trace; // Client log level
            opt.Transport = HttpTransportType.WebSockets; // Which transport you want to use for this connection
        })
        .Build(); // Build the HubConnection

        gpioConnection.On("ReceiveSwitchStatus", this.handleSwitchStatus); // Subscribe to messages sent from the Hub to the "Receive" method by passing a handle (Func<object, Task>) to process messages.
        await gpioConnection.StartAsync(); // Start the connection.
    }

    Task ReceiveSwitchStatus(bool arg)
    {
        toggle = arg;
        StateHasChanged();
        return Task.CompletedTask;
    }

    async Task ToggleLed()
    {
        toggle = !toggle;
        await Http.SendJsonAsync(HttpMethod.Post, "LED", toggle);
    }
}

Add the Hub to the server

With the client ready to go, let's add the Hub to the server project. Create a new Hubs folder in the project, and create the GPIOHub.cs file in the folder.

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace Blazor.Pi.Server.Hubs
{
    public class GPIOHub : Hub
    {
    }
}

Add SignalR and Map the Hub route

With the Hub created, let's update Startup.cs. Add using Blazor.Pi.Server.Hubs;, inside the ConfigureServices method add services.AddSignalR();, and then inside the Configure method update the app.UseEndpoints statement to map the newly created Hub.

app.UseEndpoints(endpoints =>
{
    endpoints.MapDefaultControllerRoute();
    endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
    endpoints.MapHub<GPIOHub>("/gpio");
});

Update the controller

Now let's update the controller to use SignalR to inform the clients of the status change. Resolve the IHubContext<GPIOHub> from DI, and then update the Post method with _hubContext.Clients.All.SendAsync("ReceiveSwitchStatus", toggle);.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Blazor.Pi.Server.Hubs;
using System;
using System.Device.Gpio;
using System.Linq;

namespace Blazor.Pi.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class LEDController : ControllerBase
    {
        private readonly int ledPin = 17;
        private readonly ILogger<LEDController> logger;
        private readonly IHubContext<GPIOHub> _hubContext;
        private readonly GpioController _controller;

        public LEDController(
            ILogger<LEDController> logger,
            GpioController controller,
            IHubContext<GPIOHub> hubContext
            )
        {
            _hubContext = hubContext;
            this.logger = logger;

            _controller = controller;
            if (!controller.IsPinOpen(ledPin))
            {
                controller.OpenPin(ledPin, PinMode.Output);
            }
        }

        [HttpPost]
        public void Post([FromBody] bool toggle)
        {
            _hubContext.Clients.All.SendAsync("ReceiveSwitchStatus", toggle);
            if (toggle)
            {
                _controller.Write(ledPin, PinValue.High);
            }
            else
            {
                _controller.Write(ledPin, PinValue.Low);
            }
        }
    }
}

Winning?

If the computing gods are kind, you should now have the ability to control your LED from any browser on your network and have all of them correctly reflect the status change. In the final part of this series (coming soon), we will use all we've learned so far to read temperature and humidity data from our DHT sensor.