У меня есть хаб, который отлично работает. На данный момент есть 3 события:
ReceiveMessage
(взято непосредственно из примеров приложения чата)RefreshPage
(используется, когда одна страница делает недействительной другую страницу)NotificationsUpdated
(значок с цифрой количества уведомлений, которые у вас есть)Прямо сейчас страница чата работает, как и ожидалось, и я перехватываю каждое из трех событий на ней, чтобы доказать мне, что все они работают (и они работают). Однако я знаю, что некоторым страницам сайта не нужно будет подключаться к этому. концентратор, в то время как другие будут подключаться и должны обрабатывать только 1 или 2 события. Скорее всего, в конце концов я удалю событие ReceiveMessage чата.
На данный момент я использую HubConnectionBuilder
вот так:
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/hub/ping"), options =>
{
options.AccessTokenProvider = async () => await GetAccessTokenValueAsync();
})
.WithAutomaticReconnect()
.Build();
hubRegistrations.Add(hubConnection.OnMessageReceived(OnMessageReceivedAsync));
hubRegistrations.Add(hubConnection.OnRefreshPage(OnRefreshPageAsync));
hubRegistrations.Add(hubConnection.OnNotificationsUpdate(OnNotificationsUpdateAsync));
await hubConnection.StartAsync();
}
И как пример одного из событий:
public async Task OnMessageReceivedAsync(string user, string message) =>
await InvokeAsync(() =>
{
var encodedMessage = $"{user}: {message}";
Messages.Add(encodedMessage);
StateHasChanged();
});
И много дублированного кода, если мне нужно реализовать это на другой странице (конечно, если мне не нужно реализовывать все 3 события, меньше, но не намного меньше.)
[Inject]
public required NavigationManager NavigationManager { get; set; }
[Inject]
public required IAccessTokenProvider TokenProvider { get; set; }
[Inject]
public required StateManager State { get; set; }
private HubConnection hubConnection;
private readonly HashSet<IDisposable> hubRegistrations = new();
public async ValueTask DisposeAsync()
{
await PHST.DisposeAsync();
if (hubRegistrations is { Count: > 0 })
{
foreach (var disposable in hubRegistrations)
disposable.Dispose();
}
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
Итак, как кодер, я хочу сделать это максимально простым для всех будущих кодеров и страниц. Я хотел бы сделать это сервисом или синглтоном или чем-то еще. Это то, что я «могу» сделать, или есть лучший способ иметь один концентратор на любом количестве страниц?
Я играл с созданием базового класса, но наследование было бы немного болезненным на других страницах, где уже есть наследование. Я только немного попробовал услугу, которую я могу внедрить, но хотел остановиться и задать вопрос, прежде чем спуститься в кроличью нору.
Я также хотел бы сделать его довольно динамичным, чтобы события могли делать то, что они хотят, с данными. На данный момент каждый из них является динамическим с использованием нескольких методов расширения.
public static IDisposable OnMessageReceived(this HubConnection connection, Func<string, string, Task> handler) =>
connection.On("ReceiveMessage", handler);
public static IDisposable OnRefreshPage(this HubConnection connection, Func<string, Task> handler) =>
connection.On("RefreshPage", handler);
public static IDisposable OnNotificationsUpdate(this HubConnection connection, Func<int, Task> handler) =>
connection.On("NotificationsUpdate", handler);
Увидев, что мне не помогли, я продолжил движение вперед. Я не уверен, что сделал это идеально, но мое требование меньшего количества избыточности на каждой странице соответствует этому! Я даже хотел, чтобы имена групп были максимально защищены от идиотов, и я реализовал для этого интерфейс (прокомментировано ниже — см. методы HubGroupXXX
и класс HubGroupAdders
).
Это будет полный обзор всего, а не только абстракция во вспомогательный класс, на случай, если это кому-то поможет в будущем.
Начнем со стороны помощника.
builder.Services.AddScoped<IPingActionsHubHelper, PingActionsHubHelper>();
public interface IPingActionsHubHelper
{
/// <summary>
/// Use this method to do all of the heavy lifting to connect to the hub. It should be run within the OnInitializedAsync method.
/// It should be followed up by at least one of the AddOnXXXEvents and locally handled.
/// </summary>
/// <param name = "groupNames">Chances are your page would need to let the hub know what groups to be in, so they may be signaled later.</param>
/// <returns></returns>
Task HubSetup(List<IHubGroupAdder> groupNames = null);
/// <summary>
/// After the HubSetup and each of the AddOnXXXEvents, call this to start the hub.
/// </summary>
/// <returns></returns>
Task HubStart();
HubConnection HubConnection { get; }
ValueTask DisposeAsync();
void AddOnReceiveMessageEvent(Action<string, string> action);
void AddOnRefreshPageEvent(Action<SignalRRefreshTypes> action);
void AddOnNotificationsUpdateEvent(Action<int, bool> action);
}
public class PingActionsHubHelper : IPingActionsHubHelper
{
private readonly NavigationManager NavigationManager;
private readonly IAccessTokenProvider TokenProvider;
private readonly HashSet<IDisposable> HubRegistrations = new();
public HubConnection HubConnection { get; private set; }
public PingActionsHubHelper(IAccessTokenProvider tokenProvider, NavigationManager navigationManager)
{
NavigationManager = navigationManager;
TokenProvider = tokenProvider;
}
public async Task HubSetup(List<IHubGroupAdder> groupNames = null) // Run this within the OnInitializedAsync event
{
var groupNamesParsed = string.Empty;
if (groupNames is not null && groupNames.Any())
{
groupNamesParsed = string.Join("|", groupNames.Select(x => x.ToString()));
}
HubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri($"/hub/ping?{(string.IsNullOrWhiteSpace(groupNamesParsed) ? "" : groupNamesParsed)}"), options =>
{
options.AccessTokenProvider = async () => await GetAccessTokenValueAsync();
})
.WithAutomaticReconnect()
.Build();
}
public async Task HubStart()
{
await HubConnection.StartAsync();
}
#region Events to consume
public void AddOnReceiveMessageEvent(Action<string, string> action)
{
HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.ReceiveMessage, action));
}
public void AddOnRefreshPageEvent(Action<SignalRRefreshTypes> action)
{
HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.RefreshPage, action));
}
public void AddOnNotificationsUpdateEvent(Action<int, bool> action)
{
HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.NotificationsUpdate, action));
}
#endregion Events to consume
private async ValueTask<string> GetAccessTokenValueAsync()
{
var result = await TokenProvider.RequestAccessToken();
return result.TryGetToken(out var accessToken) ? accessToken.Value : null;
}
public async ValueTask DisposeAsync()
{
if (HubRegistrations is { Count: > 0 })
{
foreach (var disposable in HubRegistrations)
disposable.Dispose();
}
if (HubConnection is not null)
{
await HubConnection.DisposeAsync();
}
}
}
[Inject]
public IPingActionsHubHelper HubHelper { get; set; }
public bool IsConnected => HubHelper.HubConnection?.State == HubConnectionState.Connected;
protected override async Task OnInitializedAsync()
{
// Here is an example of adding group(s) to whatever page you are on that is logical
// You should be adding at least one group so it can be signaled
// By default, any instance of the PingActionsHub (using the HubHelper) will have the User's Id group
await HubHelper.HubSetup(new List<IHubGroupAdder>()
{
new HubGroupProject(333333),
new HubGroupPage(Core.SignalRPageNames.FGV, 444444),
new HubGroupFormulation(999999),
});
// This event will most likely never be used, it is really just for the chat page
HubHelper.AddOnReceiveMessageEvent((string user, string message) =>
{
var encodedMessage = $"{user}: {message}";
Messages.Add(encodedMessage);
StateHasChanged();
});
// Implement this on your page when another page could alater the Project or Formulation and should refresh the page where the hub is instantiated
HubHelper.AddOnRefreshPageEvent((SignalRRefreshTypes refreshType) =>
{
System.Console.WriteLine($"We have caught a Page Refresh of type: {refreshType}");
// This is where the page would refresh or LoadData or something based perhaps on the refreshType
});
// This should really only need to be implemented on the Navbar component (which it is), unless some other location should show
// how many notifications a user has
HubHelper.AddOnNotificationsUpdateEvent((int totalNotifications, bool inError) =>
{
State.UsersTotalNotifications.SetState(totalNotifications);
StateHasChanged();
});
await HubHelper.HubStart();
}
У меня есть комментарии в коде выше. Но это всего 3 шага:
HubSetup
и передайте любые группы, которые вы хотите пойматьHubHelper.AddOnXXXEvent
для прослушивания и что с ними делатьHubStart
Это из абстракции. Остальной код ниже мой полная реализация, где у меня есть клиентская служба, которая вызывает Контроллер на стороне сервера, который позволяет вызывать концентратор из любого места.
В следующем коде показано, как взаимодействовать со страницей, на которой есть кнопка «Отправить» в этом примере страницы Chat.razor. Он вызывает локальную службу, которая вызывает контроллер серверов, который, в свою очередь, вызывает метод концентраторов.
[Inject]
public IPingActionsService PingActionsService { get; set; }
public async Task SendMessage()
{
await PingActionsService.SendMessage(new PingSendMessageAction
{
ToAll = true,
Message = Message,
SentFrom = UsersName,
});
Message = string.Empty;
await MessageRef.FocusAsync();
}
public interface IPingActionsService
{
Task<bool> SendMessage(PingSendMessageAction notificationPing);
Task<bool> SendRefresh(PingSendRefreshAction notificationPing);
Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing);
}
Следующий файл просто реализует описанный выше интерфейс для простого вызова контроллера на стороне сервера.
// Client - PingActionsService.cs
public class PingActionsService : IPingActionsService
{
private const string BaseUri = "api/ping-actions";
private readonly IHttpService HttpService;
public PingActionsService(IHttpService httpService)
{
HttpService = httpService;
}
public async Task<bool> SendMessage(PingSendMessageAction notificationPing)
{
return await HttpService.PostAsJsonAsync<PingSendMessageAction, bool>($"{BaseUri}/send-message", notificationPing);
}
public async Task<bool> SendRefresh(PingSendRefreshAction notificationPing)
{
return await HttpService.PostAsJsonAsync<PingSendRefreshAction, bool>($"{BaseUri}/refresh-page", notificationPing);
}
public async Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing)
{
return await HttpService.PostAsJsonAsync<PingUsersNotificationsAction, bool>($"{BaseUri}/users-notifications", notificationPing);
}
}
public interface IPingActionsService
{
// This is the generic call to determine which one of the more specific calls to make
Task<bool> HandlePing(PingActionBase ping);
// These are the calls that can be used within any other service to pass to the UI
Task<bool> SendMessageToGroups(PingSendMessageAction ping);
Task<bool> SendRefreshSignalToGroups(PingSendRefreshAction ping);
Task<bool> UpdateUsersNotifications(PingUsersNotificationsAction ping);
}
[ApiController]
[Route("api/ping-actions")]
public class PingActionsController : ControllerBase
{
private readonly IPingActionsService PingActionsService;
public PingActionsController(IPingActionsService pingActionsService)
{
PingActionsService = pingActionsService;
}
[HttpPost]
[Route("ping")]
public async Task<bool> Ping(PingActionBase notificationPing)
{
return await PingActionsService.HandlePing(notificationPing);
}
[HttpPost]
[Route("send-message")]
public async Task<bool> SendMessage(PingSendMessageAction notificationPing)
{
return await PingActionsService.HandlePing(notificationPing);
}
[HttpPost]
[Route("refresh-page")]
public async Task<bool> SendRefresh(PingSendRefreshAction notificationPing)
{
return await PingActionsService.HandlePing(notificationPing);
}
[HttpPost]
[Route("users-notifications")]
public async Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing)
{
return await PingActionsService.HandlePing(notificationPing);
}
}
public class PingActionsService : IPingActionsService
{
private readonly IHubContext<PingActionsHub, IPingActionsHub> Context;
private readonly IProjectUsersService ProjectUsersService;
public PingActionsService(IHubContext<PingActionsHub, IPingActionsHub> context, IProjectUsersService projectUsersService)
{
Context = context;
ProjectUsersService = projectUsersService;
}
public async Task<bool> HandlePing(PingActionBase ping)
{
bool result;
if (ping is PingSendMessageAction sendAction)
result = await SendMessageToGroups(sendAction);
else if (ping is PingSendRefreshAction refreshAction)
result = await SendRefreshSignalToGroups(refreshAction);
else if (ping is PingUsersNotificationsAction notificationAction)
result = await UpdateUsersNotifications(notificationAction);
else
throw new ArgumentOutOfRangeException("PingType selection not valid");
return result;
}
public async Task<bool> SendMessageToGroups(PingSendMessageAction ping)
{
if (ping.ToAll)
{
await Context.Clients.All.ReceiveMessage(ping.SentFrom, ping.Message);
}
else
{
var toGroups = ParseGroups(ping);
foreach (var group in toGroups)
await Context.Clients.Group(group).ReceiveMessage(ping.SentFrom, ping.Message);
}
return true;
}
public async Task<bool> SendRefreshSignalToGroups(PingSendRefreshAction ping)
{
var toGroups = ParseGroups(ping);
foreach (var group in toGroups)
await Context.Clients.Group(group).RefreshPage(ping.RefreshType);
return true;
}
public async Task<bool> UpdateUsersNotifications(PingUsersNotificationsAction ping)
{
if ((!ping.UserId.HasValue || ping.UserId.Value <= 0) && (!ping.ProjectId.HasValue || ping.ProjectId <= 0))
throw new ArgumentOutOfRangeException($"You must specify a UserId or ProjectId when sending an UpdateUsersNotification");
var groupNames = new List<string>();
if (ping.UserId.HasValue)
groupNames.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{ping.UserId}");
if (ping.ProjectId.HasValue)
{
var projectUsers = await ProjectUsersService.GetProjectUsersAsync(ping.ProjectId.Value);
foreach (var projectUser in projectUsers)
groupNames.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{projectUser.UserId}");
}
foreach (var group in groupNames)
await Context.Clients.Group(group).NotificationsUpdate(ping.TotalNotificadtions, ping.InError);
return true;
}
private static HashSet<string> ParseGroups(PingActionReceivers ping)
{
var result = new HashSet<string>();
foreach (var projectId in ping.ProjectsToSignal.Where(x => x > 0))
result.Add($"{Core.Constants.PingActionsHub.GroupNames.Project}:{projectId}");
foreach (var formulationId in ping.FormulationsToSignal.Where(x => x > 0))
result.Add($"{Core.Constants.PingActionsHub.GroupNames.Formulation}:{formulationId}");
foreach (var userId in ping.UsersToSignal.Where(x => x > 0))
result.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{userId}");
foreach (var page in ping.PagesToSignal)
result.Add(page.ToString());
return result;
}
}
public interface IHubGroupAdder
{
abstract string ToString();
}
public record HubGroupProject : IHubGroupAdder
{
public int ProjectId { get; init; }
public HubGroupProject(int projectId) { ProjectId = projectId; }
public override string ToString() => $"{Core.Constants.PingActionsHub.GroupNames.Project}:{ProjectId}";
}
public record HubGroupFormulation : IHubGroupAdder
{
public int Formulation { get; init; }
public HubGroupFormulation(int formulationId) { Formulation = formulationId; }
public override string ToString() => $"{Core.Constants.PingActionsHub.GroupNames.Formulation}:{Formulation}";
}
public record HubGroupPage : IHubGroupAdder
{
public SignalRPageNames Page { get; init; }
public int ProjectId { get; init; }
public HubGroupPage(SignalRPageNames page, int projectId)
{
Page = page;
ProjectId = projectId;
}
public override string ToString() => $"{Page}:{ProjectId}";
}
public readonly struct PingActionsHub
{
public readonly struct GroupNames
{
public static readonly string Project = "ProjectId";
public static readonly string Formulation = "FormulationId";
public static readonly string User = "UserId";
}
public readonly struct MethodNames
{
public static readonly string ReceiveMessage = "ReceiveMessage";
public static readonly string RefreshPage = "RefreshPage";
public static readonly string NotificationsUpdate = "NotificationsUpdate";
}
}
public enum SignalRPingTypes
{
Unknown = 0,
SendMessage = 1,
Refresh = 2,
UserNotifications = 3,
}
public enum SignalRPageNames
{
FGV = 1,
PAndC,
PerfTests,
PM,
}
public enum SignalRRefreshTypes
{
RefreshOnly = 0,
SomethingElse = 1, // I think in the future I will need refreshes that only update 1 thing or possibly get a dataset to be used programmatically
}