From 9d4970aa6711dd2ab25b5e7467337924fbd5e301 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:30:08 +1000 Subject: [PATCH] Create EventWrapper --- .../Features/Debugging/DebuggingService.cs | 2 +- .../Features/Events/EventWrapper.cs | 29 ++++++++++++ .../Features/Events/EventWrapperBase.cs | 44 +++++++++++++++++++ .../Features/Events/GlobalEvents.cs | 27 +++--------- .../Features/Run/RunService.cs | 14 +++--- .../Features/CodeEditor/CodeEditorPanel.cs | 2 +- .../Features/Debug_/DebugPanel.cs | 8 ++-- .../Tab/SubTabs/ThreadsVariablesSubTab.cs | 2 +- src/SharpIDE.Godot/Features/Run/RunPanel.cs | 8 ++-- .../Components/Run/RunPanel.razor | 4 +- .../Components/Run/RunPopover.razor | 4 +- src/SharpIDE.Photino/Layout/MainLayout.razor | 2 +- 12 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 src/SharpIDE.Application/Features/Events/EventWrapper.cs create mode 100644 src/SharpIDE.Application/Features/Events/EventWrapperBase.cs diff --git a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs index c04390e..6755621 100644 --- a/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs +++ b/src/SharpIDE.Application/Features/Debugging/DebuggingService.cs @@ -76,7 +76,7 @@ public class DebuggingService var filePath = dict?["source"]?["path"]!.Value()!; var line = (dict?["line"]?.Value()!).Value; var executionStopInfo = new ExecutionStopInfo { FilePath = filePath, Line = line, ThreadId = @event.ThreadId!.Value }; - GlobalEvents.Instance.InvokeDebuggerExecutionStopped(executionStopInfo); + GlobalEvents.Instance.DebuggerExecutionStopped.InvokeParallelFireAndForget(executionStopInfo); if (@event.Reason is StoppedEvent.ReasonValue.Exception) { Console.WriteLine("Stopped due to exception, continuing"); diff --git a/src/SharpIDE.Application/Features/Events/EventWrapper.cs b/src/SharpIDE.Application/Features/Events/EventWrapper.cs new file mode 100644 index 0000000..a84d7c7 --- /dev/null +++ b/src/SharpIDE.Application/Features/Events/EventWrapper.cs @@ -0,0 +1,29 @@ +namespace SharpIDE.Application.Features.Events; + +public class EventWrapper(Func @event) : EventWrapperBase>(@event) where TReturn : Task +{ + public void InvokeParallelFireAndForget() => FireAndForget(() => InvokeParallelAsync()); + + public async Task InvokeParallelAsync() + { + await InvokeDelegatesAsync(Event.GetInvocationList(), del => ((Func)del)()); + } +} + +public class EventWrapper(Func @event) : EventWrapperBase>(@event) where TReturn : Task +{ + public void InvokeParallelFireAndForget(TArg arg) => FireAndForget(() => InvokeParallelAsync(arg)); + public async Task InvokeParallelAsync(TArg arg) + { + await InvokeDelegatesAsync(Event.GetInvocationList(), del => ((Func)del)(arg)); + } +} + +public class EventWrapper(Func @event) : EventWrapperBase>(@event) where TReturn : Task +{ + public void InvokeParallelFireAndForget(TArg1 arg1, TArg2 arg2) => FireAndForget(() => InvokeParallelAsync(arg1, arg2)); + public async Task InvokeParallelAsync(TArg1 arg, TArg2 arg2) + { + await InvokeDelegatesAsync(Event.GetInvocationList(), del => ((Func)del)(arg, arg2)); + } +} diff --git a/src/SharpIDE.Application/Features/Events/EventWrapperBase.cs b/src/SharpIDE.Application/Features/Events/EventWrapperBase.cs new file mode 100644 index 0000000..5bcf666 --- /dev/null +++ b/src/SharpIDE.Application/Features/Events/EventWrapperBase.cs @@ -0,0 +1,44 @@ +namespace SharpIDE.Application.Features.Events; + +public abstract class EventWrapperBase(TDelegate @event) where TDelegate : Delegate +{ + protected TDelegate Event = @event; + + public void Subscribe(TDelegate handler) => Event = (TDelegate)Delegate.Combine(Event, handler); + public void Unsubscribe(TDelegate handler) => Event = (TDelegate)Delegate.Remove(Event, handler)!; + + protected static async void FireAndForget(Func action) + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine($"An exception occurred in an event handler: {ex}"); + } + } + + protected static async Task InvokeDelegatesAsync(IEnumerable invocationList, Func delegateExecutorDelegate) + { + var tasks = invocationList.Select(async del => + { + try + { + await delegateExecutorDelegate(del).ConfigureAwait(false); + return null; + } + catch (Exception ex) + { + return ex; + } + }); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var exceptions = results.Where(r => r is not null).Select(r => r!).ToList(); + if (exceptions.Count != 0) + { + throw new AggregateException(exceptions); + } + } +} diff --git a/src/SharpIDE.Application/Features/Events/GlobalEvents.cs b/src/SharpIDE.Application/Features/Events/GlobalEvents.cs index 9dd687b..8befc23 100644 --- a/src/SharpIDE.Application/Features/Events/GlobalEvents.cs +++ b/src/SharpIDE.Application/Features/Events/GlobalEvents.cs @@ -6,26 +6,13 @@ namespace SharpIDE.Application.Features.Events; public class GlobalEvents { public static GlobalEvents Instance { get; set; } = null!; - public event Func ProjectsRunningChanged = () => Task.CompletedTask; - public void InvokeProjectsRunningChanged() => ProjectsRunningChanged?.InvokeParallelFireAndForget(); - - public event Func StartedRunningProject = () => Task.CompletedTask; - public void InvokeStartedRunningProject() => StartedRunningProject?.InvokeParallelFireAndForget(); - - public event Func ProjectStartedDebugging = _ => Task.CompletedTask; - public void InvokeProjectStartedDebugging(SharpIdeProjectModel project) => ProjectStartedDebugging?.InvokeParallelFireAndForget(project); - - public event Func ProjectStoppedDebugging = _ => Task.CompletedTask; - public void InvokeProjectStoppedDebugging(SharpIdeProjectModel project) => ProjectStoppedDebugging?.InvokeParallelFireAndForget(project); - - public event Func ProjectStartedRunning = _ => Task.CompletedTask; - public void InvokeProjectStartedRunning(SharpIdeProjectModel project) => ProjectStartedRunning?.InvokeParallelFireAndForget(project); - - public event Func ProjectStoppedRunning = _ => Task.CompletedTask; - public void InvokeProjectStoppedRunning(SharpIdeProjectModel project) => ProjectStoppedRunning?.InvokeParallelFireAndForget(project); - - public event Func DebuggerExecutionStopped = _ => Task.CompletedTask; - public void InvokeDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) => DebuggerExecutionStopped?.InvokeParallelFireAndForget(executionStopInfo); + public EventWrapper ProjectsRunningChanged { get; private set; } = new(() => Task.CompletedTask); + public EventWrapper StartedRunningProject { get; private set; } = new(() => Task.CompletedTask); + public EventWrapper ProjectStartedDebugging { get; private set; } = new(_ => Task.CompletedTask); + public EventWrapper ProjectStoppedDebugging { get; private set; } = new(_ => Task.CompletedTask); + public EventWrapper ProjectStartedRunning { get; private set; } = new(_ => Task.CompletedTask); + public EventWrapper ProjectStoppedRunning { get; private set; } = new(_ => Task.CompletedTask); + public EventWrapper DebuggerExecutionStopped { get; private set; } = new(_ => Task.CompletedTask); } public static class AsyncEventExtensions diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index 5bd2b55..ee4c4b2 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -93,13 +93,13 @@ public class RunService project.OpenInRunPanel = true; if (isDebug) { - GlobalEvents.Instance.InvokeProjectStartedDebugging(project); + GlobalEvents.Instance.ProjectStartedDebugging.InvokeParallelFireAndForget(project); } else { - GlobalEvents.Instance.InvokeProjectsRunningChanged(); - GlobalEvents.Instance.InvokeStartedRunningProject(); - GlobalEvents.Instance.InvokeProjectStartedRunning(project); + GlobalEvents.Instance.ProjectsRunningChanged.InvokeParallelFireAndForget(); + GlobalEvents.Instance.StartedRunningProject.InvokeParallelFireAndForget(); + GlobalEvents.Instance.ProjectStartedRunning.InvokeParallelFireAndForget(project); } project.InvokeProjectStartedRunning(); await process.WaitForExitAsync().WaitAsync(project.RunningCancellationTokenSource.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); @@ -115,12 +115,12 @@ public class RunService project.Running = false; if (isDebug) { - GlobalEvents.Instance.InvokeProjectStoppedDebugging(project); + GlobalEvents.Instance.ProjectStoppedDebugging.InvokeParallelFireAndForget(project); } else { - GlobalEvents.Instance.InvokeProjectsRunningChanged(); - GlobalEvents.Instance.InvokeProjectStoppedRunning(project); + GlobalEvents.Instance.ProjectsRunningChanged.InvokeParallelFireAndForget(); + GlobalEvents.Instance.ProjectStoppedRunning.InvokeParallelFireAndForget(project); } project.InvokeProjectStoppedRunning(); diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs index 7591e24..2e8ab3b 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs @@ -25,7 +25,7 @@ public partial class CodeEditorPanel : MarginContainer var tabBar = _tabContainer.GetTabBar(); tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; tabBar.TabClosePressed += OnTabClosePressed; - GlobalEvents.Instance.DebuggerExecutionStopped += OnDebuggerExecutionStopped; + GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped); } public override void _UnhandledKeyInput(InputEvent @event) diff --git a/src/SharpIDE.Godot/Features/Debug_/DebugPanel.cs b/src/SharpIDE.Godot/Features/Debug_/DebugPanel.cs index e55902f..a4d1e6b 100644 --- a/src/SharpIDE.Godot/Features/Debug_/DebugPanel.cs +++ b/src/SharpIDE.Godot/Features/Debug_/DebugPanel.cs @@ -22,14 +22,14 @@ public partial class DebugPanel : Control //_tabBar.TabClosePressed _tabBar.TabClicked += OnTabBarTabClicked; _tabsPanel = GetNode("%TabsPanel"); - GlobalEvents.Instance.ProjectStartedDebugging += async projectModel => + GlobalEvents.Instance.ProjectStartedDebugging.Subscribe(async projectModel => { await this.InvokeAsync(() => ProjectStartedDebugging(projectModel)); - }; - GlobalEvents.Instance.ProjectStoppedDebugging += async projectModel => + }); + GlobalEvents.Instance.ProjectStoppedDebugging.Subscribe(async projectModel => { await this.InvokeAsync(() => ProjectStoppedDebugging(projectModel)); - }; + }); } private void OnTabBarTabClicked(long idx) diff --git a/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs b/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs index b32e99c..c083fa0 100644 --- a/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs +++ b/src/SharpIDE.Godot/Features/Debug_/Tab/SubTabs/ThreadsVariablesSubTab.cs @@ -20,7 +20,7 @@ public partial class ThreadsVariablesSubTab : Control _threadsVboxContainer = GetNode("%ThreadsPanel/VBoxContainer"); _stackFramesVboxContainer = GetNode("%StackFramesPanel/VBoxContainer"); _variablesVboxContainer = GetNode("%VariablesPanel/VBoxContainer"); - GlobalEvents.Instance.DebuggerExecutionStopped += OnDebuggerExecutionStopped; + GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped); } diff --git a/src/SharpIDE.Godot/Features/Run/RunPanel.cs b/src/SharpIDE.Godot/Features/Run/RunPanel.cs index 151a8a8..6aad7ab 100644 --- a/src/SharpIDE.Godot/Features/Run/RunPanel.cs +++ b/src/SharpIDE.Godot/Features/Run/RunPanel.cs @@ -21,14 +21,14 @@ public partial class RunPanel : Control //_tabBar.TabClosePressed _tabBar.TabClicked += OnTabBarTabClicked; _tabsPanel = GetNode("%TabsPanel"); - GlobalEvents.Instance.ProjectStartedRunning += async projectModel => + GlobalEvents.Instance.ProjectStartedRunning.Subscribe(async projectModel => { await this.InvokeAsync(() => ProjectStartedRunning(projectModel)); - }; - GlobalEvents.Instance.ProjectStoppedRunning += async projectModel => + }); + GlobalEvents.Instance.ProjectStoppedRunning.Subscribe(async projectModel => { await this.InvokeAsync(() => ProjectStoppedRunning(projectModel)); - }; + }); } private void OnTabBarTabClicked(long idx) diff --git a/src/SharpIDE.Photino/Components/Run/RunPanel.razor b/src/SharpIDE.Photino/Components/Run/RunPanel.razor index 874d321..ae62660 100644 --- a/src/SharpIDE.Photino/Components/Run/RunPanel.razor +++ b/src/SharpIDE.Photino/Components/Run/RunPanel.razor @@ -54,7 +54,7 @@ { var tasks = SolutionModel.AllProjects.Select(p => p.MsBuildEvaluationProjectTask); await Task.WhenAll(tasks); - GlobalEvents.Instance.ProjectsRunningChanged += OnProjectsRunningChanged; + GlobalEvents.Instance.ProjectsRunningChanged.Subscribe(OnProjectsRunningChanged); } private void CloseTab(SharpIdeProjectModel project) @@ -67,7 +67,7 @@ await InvokeAsync(StateHasChanged); } - public void Dispose() => GlobalEvents.Instance.ProjectsRunningChanged -= OnProjectsRunningChanged; + public void Dispose() => GlobalEvents.Instance.ProjectsRunningChanged.Unsubscribe(OnProjectsRunningChanged); private void SetActiveTab(SharpIdeProjectModel project) { diff --git a/src/SharpIDE.Photino/Components/Run/RunPopover.razor b/src/SharpIDE.Photino/Components/Run/RunPopover.razor index 51d4eb9..79de8d0 100644 --- a/src/SharpIDE.Photino/Components/Run/RunPopover.razor +++ b/src/SharpIDE.Photino/Components/Run/RunPopover.razor @@ -57,7 +57,7 @@ protected override void OnInitialized() { - GlobalEvents.Instance.ProjectsRunningChanged += OnProjectsRunningChanged; + GlobalEvents.Instance.ProjectsRunningChanged.Subscribe(OnProjectsRunningChanged); } protected override async Task OnParametersSetAsync() @@ -73,5 +73,5 @@ await InvokeAsync(StateHasChanged); } - public void Dispose() => GlobalEvents.Instance.ProjectsRunningChanged -= OnProjectsRunningChanged; + public void Dispose() => GlobalEvents.Instance.ProjectsRunningChanged.Unsubscribe(OnProjectsRunningChanged); } diff --git a/src/SharpIDE.Photino/Layout/MainLayout.razor b/src/SharpIDE.Photino/Layout/MainLayout.razor index d77379c..a2fc24f 100644 --- a/src/SharpIDE.Photino/Layout/MainLayout.razor +++ b/src/SharpIDE.Photino/Layout/MainLayout.razor @@ -138,7 +138,7 @@ { GlobalEvents.Instance = new GlobalEvents(); await LoadSolutionFromInteractivePicker(AppState.IdeSettings.AutoOpenLastSolution); - GlobalEvents.Instance.StartedRunningProject += OnStartedRunningProject; + GlobalEvents.Instance.StartedRunningProject.Subscribe(OnStartedRunningProject); } private void OnProblemSelected(SharpIdeFile file)