diff --git a/Directory.Packages.props b/Directory.Packages.props index 4288dc4ce..a81246684 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,9 +36,9 @@ - - - - + + + + \ No newline at end of file diff --git a/src/InfiniFrame.Shared/BuilderSnapshots/InfiniFrameWindowEventsSnapshot.cs b/src/InfiniFrame.Shared/BuilderSnapshots/InfiniFrameWindowEventsSnapshot.cs index 4951b4d8b..e474a4ec5 100644 --- a/src/InfiniFrame.Shared/BuilderSnapshots/InfiniFrameWindowEventsSnapshot.cs +++ b/src/InfiniFrame.Shared/BuilderSnapshots/InfiniFrameWindowEventsSnapshot.cs @@ -17,7 +17,7 @@ internal readonly record struct InfiniFrameWindowEventsSnapshot( Action[] WindowMinimized, Action[] WebMessageReceived, Action[] WindowClosingRequested, - NetClosingDelegate[] WindowClosing, + Func[] WindowClosing, Action[] WindowCreating, Action[] WindowCreated ); diff --git a/src/InfiniFrame.Shared/Delegates/NetClosingDelegate.cs b/src/InfiniFrame.Shared/Delegates/NetClosingDelegate.cs deleted file mode 100644 index cd916461d..000000000 --- a/src/InfiniFrame.Shared/Delegates/NetClosingDelegate.cs +++ /dev/null @@ -1,8 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -namespace InfiniFrame; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public delegate bool NetClosingDelegate(object sender, EventArgs? e); diff --git a/src/InfiniFrame.Shared/Delegates/NetCustomSchemeDelegate.cs b/src/InfiniFrame.Shared/Delegates/NetCustomSchemeDelegate.cs index 4f8bec779..a447cc3ef 100644 --- a/src/InfiniFrame.Shared/Delegates/NetCustomSchemeDelegate.cs +++ b/src/InfiniFrame.Shared/Delegates/NetCustomSchemeDelegate.cs @@ -5,4 +5,4 @@ namespace InfiniFrame; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public delegate Stream? NetCustomSchemeDelegate(object sender, string scheme, string url, out string? contentType); +public delegate Stream? NetCustomSchemeDelegate(IInfiniFrameWindow sender, string scheme, string url, out string? contentType); diff --git a/src/InfiniFrame.Shared/FluentApi/InfiniWindowEventsExtensions.cs b/src/InfiniFrame.Shared/FluentApi/InfiniWindowEventsExtensions.cs index 643fb96c8..9a2240c06 100644 --- a/src/InfiniFrame.Shared/FluentApi/InfiniWindowEventsExtensions.cs +++ b/src/InfiniFrame.Shared/FluentApi/InfiniWindowEventsExtensions.cs @@ -176,11 +176,9 @@ public static T RegisterWindowClosingRequestedHandler(this T builder, Action< /// /// Returns the current instance. /// - /// - /// - /// /// The builder to register the handler for. - public static T RegisterWindowClosingHandler(this T builder, NetClosingDelegate handler) where T : IHasInfiniFrameEvents { + /// The handler that will be invoked + public static T RegisterWindowClosingHandler(this T builder, Func handler) where T : IHasInfiniFrameEvents { builder.Events.WindowClosing.Add(handler); return builder; } diff --git a/src/InfiniFrame.Shared/IInfiniFrameWindowEvents.cs b/src/InfiniFrame.Shared/IInfiniFrameWindowEvents.cs index b80bc6593..a0afb031d 100644 --- a/src/InfiniFrame.Shared/IInfiniFrameWindowEvents.cs +++ b/src/InfiniFrame.Shared/IInfiniFrameWindowEvents.cs @@ -9,18 +9,18 @@ namespace InfiniFrame; // Code // --------------------------------------------------------------------------------------------------------------------- public interface IInfiniFrameWindowEvents { - InfiniFrameOrderedEvent WindowLocationChanged { get; } - InfiniFrameOrderedEvent WindowSizeChanged { get; } - InfiniFrameOrderedEvent WindowFocusIn { get; } - InfiniFrameOrderedEvent WindowMaximized { get; } - InfiniFrameOrderedEvent WindowRestored { get; } - InfiniFrameOrderedEvent WindowFocusOut { get; } - InfiniFrameOrderedEvent WindowMinimized { get; } - InfiniFrameOrderedEvent WebMessageReceived { get; } - InfiniFrameOrderedEvent WindowClosingRequested { get; } - InfiniFrameOrderedClosingEvent WindowClosing { get; } - InfiniFrameOrderedEvent WindowCreating { get; } - InfiniFrameOrderedEvent WindowCreated { get; } + OrderedEvent WindowLocationChanged { get; } + OrderedEvent WindowSizeChanged { get; } + OrderedEvent WindowFocusIn { get; } + OrderedEvent WindowMaximized { get; } + OrderedEvent WindowRestored { get; } + OrderedEvent WindowFocusOut { get; } + OrderedEvent WindowMinimized { get; } + OrderedEvent WebMessageReceived { get; } + OrderedEvent WindowClosingRequested { get; } + OrderedResultEvent WindowClosing { get; } + OrderedEvent WindowCreating { get; } + OrderedEvent WindowCreated { get; } void CompleteSetup(IInfiniFrameWindow sender); diff --git a/src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedClosingEvent.cs b/src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedClosingEvent.cs deleted file mode 100644 index 08fbe170e..000000000 --- a/src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedClosingEvent.cs +++ /dev/null @@ -1,43 +0,0 @@ -// --------------------------------------------------------------------------------------------------------------------- -// Imports -// --------------------------------------------------------------------------------------------------------------------- -using System.Collections.Immutable; - -namespace InfiniFrame.Utilities; -// --------------------------------------------------------------------------------------------------------------------- -// Code -// --------------------------------------------------------------------------------------------------------------------- -public class InfiniFrameOrderedClosingEvent { - private ImmutableArray _handlers = ImmutableArray.Empty; - public ImmutableArray Snapshot => _handlers; - - // ----------------------------------------------------------------------------------------------------------------- - // Methods - // ----------------------------------------------------------------------------------------------------------------- - public void Add(NetClosingDelegate handler) { - ArgumentNullException.ThrowIfNull(handler); - ImmutableInterlocked.Update( - ref _handlers, - transformer: static (current, item) => current.Add(item), - handler - ); - } - - public void Remove(NetClosingDelegate handler) { - ArgumentNullException.ThrowIfNull(handler); - ImmutableInterlocked.Update( - ref _handlers, - transformer: static (current, item) => current.Remove(item), - handler - ); - } - - public bool? Invoke(IInfiniFrameWindow window) { - bool? result = null; - foreach (NetClosingDelegate handler in _handlers) { - result = handler(window, null); - } - - return result; - } -} diff --git a/src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedEvent.cs b/src/InfiniFrame.Shared/Utilities/OrderedEvent.cs similarity index 89% rename from src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedEvent.cs rename to src/InfiniFrame.Shared/Utilities/OrderedEvent.cs index 6e7a0b788..9835420eb 100644 --- a/src/InfiniFrame.Shared/Utilities/InfiniFrameOrderedEvent.cs +++ b/src/InfiniFrame.Shared/Utilities/OrderedEvent.cs @@ -7,7 +7,7 @@ namespace InfiniFrame.Utilities; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public class InfiniFrameOrderedEvent { +public class OrderedEvent { private ImmutableArray> _handlers = ImmutableArray>.Empty; public ImmutableArray> Snapshot => _handlers; @@ -39,10 +39,13 @@ public void Invoke(IInfiniFrameWindow window) { } } -public class InfiniFrameOrderedEvent { +public class OrderedEvent { private ImmutableArray> _handlers = ImmutableArray>.Empty; public ImmutableArray> Snapshot => _handlers; + // ----------------------------------------------------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------------------------------------------------- public void Add(Action handler) { ArgumentNullException.ThrowIfNull(handler); ImmutableInterlocked.Update(ref _handlers, transformer: static (current, item) => current.Add(item), handler); @@ -58,4 +61,4 @@ public void Invoke(IInfiniFrameWindow window, TPayload payload) { handler(window, payload); } } -} +} \ No newline at end of file diff --git a/src/InfiniFrame.Shared/Utilities/OrderedResultEvent.cs b/src/InfiniFrame.Shared/Utilities/OrderedResultEvent.cs new file mode 100644 index 000000000..1870cd430 --- /dev/null +++ b/src/InfiniFrame.Shared/Utilities/OrderedResultEvent.cs @@ -0,0 +1,42 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using System.Collections.Immutable; + +namespace InfiniFrame.Utilities; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +public class OrderedResultEvent { + private ImmutableArray> _handlers = ImmutableArray>.Empty; + public ImmutableArray> Snapshot => _handlers; + + // ----------------------------------------------------------------------------------------------------------------- + // Methods + // ----------------------------------------------------------------------------------------------------------------- + public void Add(Func handler) { + ArgumentNullException.ThrowIfNull(handler); + ImmutableInterlocked.Update(ref _handlers, transformer: static (current, item) => current.Add(item), handler); + } + + public void Remove(Func handler) { + ArgumentNullException.ThrowIfNull(handler); + ImmutableInterlocked.Update(ref _handlers, transformer: static (current, item) => current.Remove(item), handler); + } + + public TResult?[] Invoke(IInfiniFrameWindow window, TPayload payload) { + var results = new TResult?[_handlers.Length]; + for (int i = 0; i < _handlers.Length; i++) { + Func handler = _handlers[i]; + + try { + results[i] = handler(window, payload); + } + catch (Exception ex) when (ex is not OperationCanceledException) { + results[i] = default; + } + } + + return results.ToArray(); + } +} diff --git a/src/InfiniFrame/InfiniFrameWindowEvents.cs b/src/InfiniFrame/InfiniFrameWindowEvents.cs index 2efc4c63c..faf7f802c 100644 --- a/src/InfiniFrame/InfiniFrameWindowEvents.cs +++ b/src/InfiniFrame/InfiniFrameWindowEvents.cs @@ -12,18 +12,18 @@ namespace InfiniFrame; public class InfiniFrameWindowEvents : IInfiniFrameWindowEvents { private IInfiniFrameWindow Sender { get; set; } = null!; - public InfiniFrameOrderedEvent WindowLocationChanged { get; } = new(); - public InfiniFrameOrderedEvent WindowSizeChanged { get; } = new(); - public InfiniFrameOrderedEvent WindowFocusIn { get; } = new(); - public InfiniFrameOrderedEvent WindowMaximized { get; } = new(); - public InfiniFrameOrderedEvent WindowRestored { get; } = new(); - public InfiniFrameOrderedEvent WindowFocusOut { get; } = new(); - public InfiniFrameOrderedEvent WindowMinimized { get; } = new(); - public InfiniFrameOrderedEvent WebMessageReceived { get; } = new(); - public InfiniFrameOrderedEvent WindowClosingRequested { get; } = new(); - public InfiniFrameOrderedClosingEvent WindowClosing { get; } = new(); - public InfiniFrameOrderedEvent WindowCreating { get; } = new(); - public InfiniFrameOrderedEvent WindowCreated { get; } = new(); + public OrderedEvent WindowLocationChanged { get; } = new(); + public OrderedEvent WindowSizeChanged { get; } = new(); + public OrderedEvent WindowFocusIn { get; } = new(); + public OrderedEvent WindowMaximized { get; } = new(); + public OrderedEvent WindowRestored { get; } = new(); + public OrderedEvent WindowFocusOut { get; } = new(); + public OrderedEvent WindowMinimized { get; } = new(); + public OrderedEvent WebMessageReceived { get; } = new(); + public OrderedEvent WindowClosingRequested { get; } = new(); + public OrderedResultEvent WindowClosing { get; } = new(); + public OrderedEvent WindowCreating { get; } = new(); + public OrderedEvent WindowCreated { get; } = new(); // ----------------------------------------------------------------------------------------------------------------- // Methods @@ -103,10 +103,11 @@ public void OnWindowClosingRequested() { public byte OnWindowClosing() { //C++ handles bool values as a single byte, C# uses 4 bytes byte noClose = 0; - bool? doNotClose = WindowClosing.Invoke(Sender); - if (doNotClose ?? false) + bool[] doNotClose = WindowClosing.Invoke(Sender, null); + if (doNotClose.Any(r => r)) { noClose = 1; - + } + return noClose; } diff --git a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs index dcf73a8e5..86316347c 100644 --- a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs +++ b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs @@ -16,25 +16,24 @@ public static NetCustomSchemeDelegate Create(IFileProvider fileProvider, string // yes, C# 14 and such have out parameters in their lambas, but we need to support .NET 8.0 which does not natively have this yet return NetCustomSchemeDelegateWrapper; - Stream? NetCustomSchemeDelegateWrapper(object sender, string scheme, string url, out string? contentType) { + Stream? NetCustomSchemeDelegateWrapper(IInfiniFrameWindow sender, string scheme, string url, out string? contentType) { #endif contentType = null; - if (sender is not IInfiniFrameWindow { Logger: var logger }) return null; if (!TryGetAssetPath(url, defaultDocument, out string assetPath)) { - logger.LogDebug("Rejected custom scheme path for {Scheme}: {Url}", scheme, url); + sender.Logger.LogDebug("Rejected custom scheme path for {Scheme}: {Url}", scheme, url); return null; } IFileInfo file = fileProvider.GetFileInfo(assetPath); if (!file.Exists || file.IsDirectory) { - logger.LogDebug("Custom scheme miss for {Scheme}: {AssetPath} (from {Url})", scheme, assetPath, + sender.Logger.LogDebug("Custom scheme miss for {Scheme}: {AssetPath} (from {Url})", scheme, assetPath, url); return null; } contentType = GetContentType(assetPath); - logger.LogDebug("Custom scheme hit for {Scheme}: {AssetPath} ({ContentType})", scheme, assetPath, + sender.Logger.LogDebug("Custom scheme hit for {Scheme}: {AssetPath} ({ContentType})", scheme, assetPath, contentType); return file.CreateReadStream(); diff --git a/tests/InfiniFrameTests.WebServer/InfiniFrameWebApplicationTests.cs b/tests/InfiniFrameTests.WebServer/InfiniFrameWebApplicationTests.cs index e7dc87194..e2be775e1 100644 --- a/tests/InfiniFrameTests.WebServer/InfiniFrameWebApplicationTests.cs +++ b/tests/InfiniFrameTests.WebServer/InfiniFrameWebApplicationTests.cs @@ -124,8 +124,8 @@ public async Task UseAutoServerClose_ClosingHandler_ShouldReturnFalse() { // Act app.UseAutoServerClose(); - NetClosingDelegate? capturedHandler = mockEvents.WindowClosing.Snapshot.LastOrDefault(); - bool? result = capturedHandler?.Invoke(new object(), EventArgs.Empty); + Func? capturedHandler = mockEvents.WindowClosing.Snapshot.LastOrDefault(); + bool? result = capturedHandler?.Invoke(mockWindow, EventArgs.Empty); // Assert await Assert.That(capturedHandler).IsNotNull(); @@ -152,8 +152,8 @@ public async Task UseAutoServerClose_ClosingHandler_ShouldInitiateStopAsync() { app.UseAutoServerClose(); // Act - NetClosingDelegate? capturedHandler = mockEvents.WindowClosing.Snapshot.LastOrDefault(); - capturedHandler?.Invoke(new object(), EventArgs.Empty); + Func? capturedHandler = mockEvents.WindowClosing.Snapshot.LastOrDefault(); + capturedHandler?.Invoke(mockWindow, EventArgs.Empty); var appLifetime = webApp.Services.GetRequiredService(); DateTime deadline = DateTime.UtcNow.AddSeconds(2); diff --git a/tests/InfiniFrameTests/InfiniFrameWindowBuilderTests.cs b/tests/InfiniFrameTests/InfiniFrameWindowBuilderTests.cs index badb06c1a..a0fc39f94 100644 --- a/tests/InfiniFrameTests/InfiniFrameWindowBuilderTests.cs +++ b/tests/InfiniFrameTests/InfiniFrameWindowBuilderTests.cs @@ -7,13 +7,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; namespace InfiniFrameTests; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- public class InfiniFrameWindowBuilderTests { - private static Stream? EmptyHandler(object sender, string scheme, string url, out string? contentType) { + private static Stream? EmptyHandler(IInfiniFrameWindow sender, string scheme, string url, out string? contentType) { _ = sender; _ = scheme; _ = url; @@ -148,7 +149,7 @@ public async Task CreateSnapshot_ReRegisteringSameScheme_DoesNotMultiplyDelegate .Where(static item => item.Key == "app") .Select(static item => item.Value) .FirstOrDefault(); - copiedHandler?.Invoke(this, "app", "app://resource", out string? _); + copiedHandler?.Invoke(Substitute.For(), "app", "app://resource", out string? _); // Assert await Assert.That(found).IsTrue(); @@ -156,7 +157,7 @@ public async Task CreateSnapshot_ReRegisteringSameScheme_DoesNotMultiplyDelegate await Assert.That(callCount).IsEqualTo(2); return; - Stream? CountingHandler(object sender, string scheme, string url, out string? contentType) { + Stream? CountingHandler(IInfiniFrameWindow sender, string scheme, string url, out string? contentType) { _ = sender; _ = scheme; _ = url; @@ -186,8 +187,8 @@ public async Task CreateSnapshot_ReRegisteringSameScheme_RemainsStableAcrossRepe int firstRegisteredCount = first.CustomSchemes.OrderedSchemeNames.Distinct(StringComparer.Ordinal).Count(); int secondRegisteredCount = second.CustomSchemes.OrderedSchemeNames.Distinct(StringComparer.Ordinal).Count(); - firstHandler?.Invoke(this, "app", "app://resource1", out string? _); - secondHandler?.Invoke(this, "app", "app://resource2", out string? _); + firstHandler?.Invoke(Substitute.For(), "app", "app://resource1", out string? _); + secondHandler?.Invoke(Substitute.For(), "app", "app://resource2", out string? _); // Assert await Assert.That(foundFirst).IsTrue(); diff --git a/tests/InfiniFrameTests/Utilities/OrderedEventTests.cs b/tests/InfiniFrameTests/Utilities/OrderedEventTests.cs index d157d60ce..0fb38f1fd 100644 --- a/tests/InfiniFrameTests/Utilities/OrderedEventTests.cs +++ b/tests/InfiniFrameTests/Utilities/OrderedEventTests.cs @@ -29,7 +29,7 @@ private static InfiniFrameWindow CreateWindow() { [Test] public async Task OrderedEvent_InvokesInRegistrationOrder() { // Arrange - var orderedEvent = new InfiniFrameOrderedEvent(); + var orderedEvent = new OrderedEvent(); InfiniFrameWindow window = CreateWindow(); var calls = new List(); @@ -48,7 +48,7 @@ public async Task OrderedEvent_InvokesInRegistrationOrder() { [Test] public async Task OrderedEvent_RemoveStopsInvocation() { // Arrange - var orderedEvent = new InfiniFrameOrderedEvent(); + var orderedEvent = new OrderedEvent(); InfiniFrameWindow window = CreateWindow(); var calls = new List(); Action handler = _ => calls.Add(1); @@ -66,7 +66,7 @@ public async Task OrderedEvent_RemoveStopsInvocation() { [Test] public async Task OrderedEvent_SnapshotIsImmutable() { // Arrange - var orderedEvent = new InfiniFrameOrderedEvent(); + var orderedEvent = new OrderedEvent(); Action handler1 = _ => { }; Action handler2 = _ => { }; @@ -84,7 +84,7 @@ public async Task OrderedEvent_SnapshotIsImmutable() { [Test] public async Task OrderedEvent_OperatorsAddAndRemove() { // Arrange - var orderedEvent = new InfiniFrameOrderedEvent(); + var orderedEvent = new OrderedEvent(); InfiniFrameWindow window = CreateWindow(); var calls = new List(); Action handler = _ => calls.Add(1); @@ -102,7 +102,7 @@ public async Task OrderedEvent_OperatorsAddAndRemove() { [Test] public async Task OrderedEventWithPayload_InvokesWithPayload() { // Arrange - var orderedEvent = new InfiniFrameOrderedEvent(); + var orderedEvent = new OrderedEvent(); InfiniFrameWindow window = CreateWindow(); var calls = new List(); @@ -121,29 +121,31 @@ public async Task OrderedEventWithPayload_InvokesWithPayload() { [Test] public async Task ClosingEvent_ReturnsLastResult() { // Arrange - var closingEvent = new InfiniFrameOrderedClosingEvent(); + var closingEvent = new OrderedResultEvent(); InfiniFrameWindow window = CreateWindow(); closingEvent.Add((_, _) => false); closingEvent.Add((_, _) => true); // Act - bool? result = closingEvent.Invoke(window); + bool[] result = closingEvent.Invoke(window, EventArgs.Empty); // Assert - await Assert.That(result).IsTrue(); + await Assert.That(result).Count().IsEqualTo(2); + await Assert.That(result.First()).IsFalse(); + await Assert.That(result.Last()).IsTrue(); } [Test] public async Task ClosingEvent_ReturnsNullWhenEmpty() { // Arrange - var closingEvent = new InfiniFrameOrderedClosingEvent(); + var closingEvent = new OrderedResultEvent(); InfiniFrameWindow window = CreateWindow(); // Act - bool? result = closingEvent.Invoke(window); + bool[] result = closingEvent.Invoke(window, EventArgs.Empty); // Assert - await Assert.That(result).IsNull(); + await Assert.That(result).IsEmpty(); } }