forked from Eauldane/SnowcloakClient
Initial
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this);
|
||||
UnsubscribeAll();
|
||||
}
|
||||
}
|
6
MareSynchronos/Services/Mediator/IMediatorSubscriber.cs
Normal file
6
MareSynchronos/Services/Mediator/IMediatorSubscriber.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public interface IMediatorSubscriber
|
||||
{
|
||||
MareMediator Mediator { get; }
|
||||
}
|
222
MareSynchronos/Services/Mediator/MareMediator.cs
Normal file
222
MareSynchronos/Services/Mediator/MareMediator.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public sealed class MareMediator : IHostedService
|
||||
{
|
||||
private readonly Lock _addRemoveLock = new();
|
||||
private readonly ConcurrentDictionary<SubscriberAction, DateTime> _lastErrorTime = [];
|
||||
private readonly ILogger<MareMediator> _logger;
|
||||
private readonly CancellationTokenSource _loopCts = new();
|
||||
private readonly ConcurrentQueue<MessageBase> _messageQueue = new();
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private readonly ConcurrentDictionary<(Type, string?), HashSet<SubscriberAction>> _subscriberDict = [];
|
||||
private bool _processQueue = false;
|
||||
private readonly ConcurrentDictionary<(Type, string?), MethodInfo?> _genericExecuteMethods = new();
|
||||
public MareMediator(ILogger<MareMediator> logger, PerformanceCollectorService performanceCollector, MareConfigService mareConfigService)
|
||||
{
|
||||
_logger = logger;
|
||||
_performanceCollector = performanceCollector;
|
||||
_mareConfigService = mareConfigService;
|
||||
}
|
||||
|
||||
public void PrintSubscriberInfo()
|
||||
{
|
||||
foreach (var subscriber in _subscriberDict.SelectMany(c => c.Value.Select(v => v.Subscriber))
|
||||
.DistinctBy(p => p).OrderBy(p => p.GetType().FullName, StringComparer.Ordinal).ToList())
|
||||
{
|
||||
_logger.LogInformation("Subscriber {type}: {sub}", subscriber.GetType().Name, subscriber.ToString());
|
||||
StringBuilder sb = new();
|
||||
sb.Append("=> ");
|
||||
foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == subscriber)).ToList())
|
||||
{
|
||||
sb.Append(item.Key.Item1.Name);
|
||||
if (item.Key.Item2 != null)
|
||||
sb.Append($":{item.Key.Item2!}");
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal))
|
||||
_logger.LogInformation("{sb}", sb.ToString());
|
||||
_logger.LogInformation("---");
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish<T>(T message) where T : MessageBase
|
||||
{
|
||||
if (message.KeepThreadContext)
|
||||
{
|
||||
ExecuteMessage(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_messageQueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting MareMediator");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!_loopCts.Token.IsCancellationRequested)
|
||||
{
|
||||
while (!_processQueue)
|
||||
{
|
||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||
|
||||
while (_messageQueue.TryDequeue(out var message))
|
||||
{
|
||||
ExecuteMessage(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_logger.LogInformation("Started MareMediator");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_messageQueue.Clear();
|
||||
_loopCts.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Subscribe<T>(IMediatorSubscriber subscriber, Action<T> action) where T : MessageBase
|
||||
{
|
||||
lock (_addRemoveLock)
|
||||
{
|
||||
_subscriberDict.TryAdd((typeof(T), null), []);
|
||||
|
||||
if (!_subscriberDict[(typeof(T), null)].Add(new(subscriber, action)))
|
||||
{
|
||||
throw new InvalidOperationException("Already subscribed");
|
||||
}
|
||||
|
||||
_logger.LogTrace("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
public void SubscribeKeyed<T>(IMediatorSubscriber subscriber, string key, Action<T> action) where T : MessageBase
|
||||
{
|
||||
lock (_addRemoveLock)
|
||||
{
|
||||
_subscriberDict.TryAdd((typeof(T), key), []);
|
||||
|
||||
if (!_subscriberDict[(typeof(T), key)].Add(new(subscriber, action)))
|
||||
{
|
||||
throw new InvalidOperationException("Already subscribed");
|
||||
}
|
||||
|
||||
_logger.LogTrace("Subscriber added for message {message}:{key}: {sub}", typeof(T).Name, key, subscriber.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe<T>(IMediatorSubscriber subscriber) where T : MessageBase
|
||||
{
|
||||
lock (_addRemoveLock)
|
||||
{
|
||||
if (_subscriberDict.ContainsKey((typeof(T), null)))
|
||||
{
|
||||
_subscriberDict[(typeof(T), null)].RemoveWhere(p => p.Subscriber == subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UnsubscribeAll(IMediatorSubscriber subscriber)
|
||||
{
|
||||
lock (_addRemoveLock)
|
||||
{
|
||||
foreach (var kvp in _subscriberDict.Select(k => k.Key))
|
||||
{
|
||||
int unSubbed = _subscriberDict[kvp]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0;
|
||||
if (unSubbed > 0)
|
||||
{
|
||||
_logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Item1.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteMessage(MessageBase message)
|
||||
{
|
||||
if (!_subscriberDict.TryGetValue((message.GetType(), message.SubscriberKey), out HashSet<SubscriberAction>? subscribers) || subscribers == null || !subscribers.Any()) return;
|
||||
|
||||
List<SubscriberAction> subscribersCopy = [];
|
||||
lock (_addRemoveLock)
|
||||
{
|
||||
subscribersCopy = subscribers?.Where(s => s.Subscriber != null).ToList() ?? [];
|
||||
}
|
||||
|
||||
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
|
||||
var msgType = message.GetType();
|
||||
if (!_genericExecuteMethods.TryGetValue((msgType, message.SubscriberKey), out var methodInfo))
|
||||
{
|
||||
_genericExecuteMethods[(msgType, message.SubscriberKey)] = methodInfo = GetType()
|
||||
.GetMethod(nameof(ExecuteReflected), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
|
||||
.MakeGenericMethod(msgType);
|
||||
}
|
||||
|
||||
methodInfo!.Invoke(this, [subscribersCopy, message]);
|
||||
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
|
||||
}
|
||||
|
||||
private void ExecuteReflected<T>(List<SubscriberAction> subscribers, T message) where T : MessageBase
|
||||
{
|
||||
foreach (SubscriberAction subscriber in subscribers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_mareConfigService.Current.LogPerformance)
|
||||
{
|
||||
var isSameThread = message.KeepThreadContext ? "$" : string.Empty;
|
||||
_performanceCollector.LogPerformance(this, $"{isSameThread}Execute>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}>{subscriber.Subscriber}",
|
||||
() => ((Action<T>)subscriber.Action).Invoke(message));
|
||||
}
|
||||
else
|
||||
{
|
||||
((Action<T>)subscriber.Action).Invoke(message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow)
|
||||
continue;
|
||||
|
||||
_logger.LogError(ex.InnerException ?? ex, "Error executing {type} for subscriber {subscriber}",
|
||||
message.GetType().Name, subscriber.Subscriber.GetType().Name);
|
||||
_lastErrorTime[subscriber] = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StartQueueProcessing()
|
||||
{
|
||||
_logger.LogInformation("Starting Message Queue Processing");
|
||||
_processQueue = true;
|
||||
}
|
||||
|
||||
private sealed class SubscriberAction
|
||||
{
|
||||
public SubscriberAction(IMediatorSubscriber subscriber, object action)
|
||||
{
|
||||
Subscriber = subscriber;
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public object Action { get; }
|
||||
public IMediatorSubscriber Subscriber { get; }
|
||||
}
|
||||
}
|
23
MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs
Normal file
23
MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public abstract class MediatorSubscriberBase : IMediatorSubscriber
|
||||
{
|
||||
protected MediatorSubscriberBase(ILogger logger, MareMediator mediator)
|
||||
{
|
||||
Logger = logger;
|
||||
|
||||
Logger.LogTrace("Creating {type} ({this})", GetType().Name, this);
|
||||
Mediator = mediator;
|
||||
}
|
||||
|
||||
public MareMediator Mediator { get; }
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected void UnsubscribeAll()
|
||||
{
|
||||
Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this);
|
||||
Mediator.UnsubscribeAll(this);
|
||||
}
|
||||
}
|
20
MareSynchronos/Services/Mediator/MessageBase.cs
Normal file
20
MareSynchronos/Services/Mediator/MessageBase.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
#pragma warning disable MA0048
|
||||
public abstract record MessageBase
|
||||
{
|
||||
public virtual bool KeepThreadContext => false;
|
||||
public virtual string? SubscriberKey => null;
|
||||
}
|
||||
|
||||
public record SameThreadMessage : MessageBase
|
||||
{
|
||||
public override bool KeepThreadContext => true;
|
||||
}
|
||||
|
||||
public record KeyedMessage(string MessageKey, bool SameThread = false) : MessageBase
|
||||
{
|
||||
public override string? SubscriberKey => MessageKey;
|
||||
public override bool KeepThreadContext => SameThread;
|
||||
}
|
||||
#pragma warning restore MA0048
|
113
MareSynchronos/Services/Mediator/Messages.cs
Normal file
113
MareSynchronos/Services/Mediator/Messages.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Events;
|
||||
using MareSynchronos.WebAPI.Files.Models;
|
||||
using System.Numerics;
|
||||
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
#pragma warning disable MA0048 // File name must match type name
|
||||
#pragma warning disable S2094
|
||||
public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||
public record FrameworkUpdateMessage : SameThreadMessage;
|
||||
public record ClassJobChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase;
|
||||
public record DelayedFrameworkUpdateMessage : SameThreadMessage;
|
||||
public record ZoneSwitchStartMessage : MessageBase;
|
||||
public record ZoneSwitchEndMessage : MessageBase;
|
||||
public record CutsceneStartMessage : MessageBase;
|
||||
public record GposeStartMessage : SameThreadMessage;
|
||||
public record GposeEndMessage : MessageBase;
|
||||
public record CutsceneEndMessage : MessageBase;
|
||||
public record CutsceneFrameworkUpdateMessage : SameThreadMessage;
|
||||
public record ConnectedMessage(ConnectionDto Connection) : MessageBase;
|
||||
public record DisconnectedMessage : SameThreadMessage;
|
||||
public record PenumbraModSettingChangedMessage : MessageBase;
|
||||
public record PenumbraInitializedMessage : MessageBase;
|
||||
public record PenumbraDisposedMessage : MessageBase;
|
||||
public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : SameThreadMessage;
|
||||
public record GlamourerChangedMessage(IntPtr Address) : MessageBase;
|
||||
public record HeelsOffsetMessage : MessageBase;
|
||||
public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : SameThreadMessage;
|
||||
public record CustomizePlusMessage(nint? Address) : MessageBase;
|
||||
public record HonorificMessage(string NewHonorificTitle) : MessageBase;
|
||||
public record PetNamesReadyMessage : MessageBase;
|
||||
public record PetNamesMessage(string PetNicknamesData) : MessageBase;
|
||||
public record MoodlesMessage(IntPtr Address) : MessageBase;
|
||||
public record HonorificReadyMessage : MessageBase;
|
||||
public record PlayerChangedMessage(CharacterData Data) : MessageBase;
|
||||
public record CharacterChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase;
|
||||
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
|
||||
public record HaltScanMessage(string Source) : MessageBase;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record NotificationMessage
|
||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
|
||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||
public record CharacterDataAnalyzedMessage : MessageBase;
|
||||
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
|
||||
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||
public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase;
|
||||
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
||||
public record PauseMessage(UserData UserData) : MessageBase;
|
||||
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
||||
public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase;
|
||||
public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase;
|
||||
public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase;
|
||||
public record PlayerVisibilityMessage(string Ident, bool IsVisible, bool Invalidate = false) : KeyedMessage(Ident, SameThread: true);
|
||||
public record PairHandlerVisibleMessage(PairHandler Player) : MessageBase;
|
||||
public record OpenReportPopupMessage(Pair PairToReport) : MessageBase;
|
||||
public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase;
|
||||
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||
public record OpenPairAnalysisWindow(Pair Pair) : MessageBase;
|
||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||
public record CombatOrPerformanceStartMessage : MessageBase;
|
||||
public record CombatOrPerformanceEndMessage : MessageBase;
|
||||
public record EventMessage(Event Event) : MessageBase;
|
||||
public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase;
|
||||
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
|
||||
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
|
||||
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
|
||||
public record RecalculatePerformanceMessage(string? UID) : MessageBase;
|
||||
public record NameplateRedrawMessage : MessageBase;
|
||||
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
||||
public record UnholdPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
|
||||
public record HoldPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID);
|
||||
public record UnholdPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID);
|
||||
public record PairDataAppliedMessage(string UID, CharacterData? CharacterData) : KeyedMessage(UID);
|
||||
public record PairDataAnalyzedMessage(string UID) : KeyedMessage(UID);
|
||||
public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase;
|
||||
public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase;
|
||||
public record HaltCharaDataCreation(bool Resume = false) : SameThreadMessage;
|
||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||
public record GposeLobbyUserJoin(UserData UserData) : MessageBase;
|
||||
public record GPoseLobbyUserLeave(UserData UserData) : MessageBase;
|
||||
public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadDto) : MessageBase;
|
||||
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
|
||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||
|
||||
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
||||
#pragma warning restore S2094
|
||||
#pragma warning restore MA0048 // File name must match type name
|
@@ -0,0 +1,54 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable
|
||||
{
|
||||
protected readonly ILogger _logger;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
|
||||
protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name,
|
||||
PerformanceCollectorService performanceCollectorService) : base(name)
|
||||
{
|
||||
_logger = logger;
|
||||
Mediator = mediator;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_logger.LogTrace("Creating {type}", GetType());
|
||||
|
||||
Mediator.Subscribe<UiToggleMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.UiType == GetType())
|
||||
{
|
||||
Toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MareMediator Mediator { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||
}
|
||||
|
||||
protected abstract void DrawInternal();
|
||||
|
||||
public virtual Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
_logger.LogTrace("Disposing {type}", GetType());
|
||||
|
||||
Mediator.UnsubscribeAll(this);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user