From 53c2ee4ef3e0491a29bf47f56efe3d9fbfddfc2e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:22:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(router):=20=E6=B7=BB=E5=8A=A0=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E5=92=8CUI=E8=B7=AF=E7=94=B1=E7=9A=84Around=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在SceneRouterBase和UiRouterBase中集成管道执行Around处理器 - 实现场景过渡管道SceneTransitionPipeline的Around处理器注册和执行功能 - 实现UI过渡管道UiTransitionPipeline的Around处理器注册和执行功能 - 添加ISceneAroundTransitionHandler和IUiAroundTransitionHandler接口定义 - 扩展SceneTransitionPhases和UITransitionPhases枚举支持Around阶段 - 实现Around处理器的优先级排序和中间件链构建机制 - 添加Around处理器的超时控制和异常处理机制 --- .../enums/SceneTransitionPhases.cs | 9 +- .../enums/UITransitionPhases.cs | 11 +- .../scene/ISceneAroundTransitionHandler.cs | 48 +++++++ .../ui/IUiAroundTransitionHandler.cs | 34 +++++ GFramework.Game/scene/SceneRouterBase.cs | 38 ++++-- .../scene/SceneTransitionPipeline.cs | 120 ++++++++++++++++++ GFramework.Game/ui/UiRouterBase.cs | 64 ++++++---- GFramework.Game/ui/UiTransitionPipeline.cs | 119 +++++++++++++++++ 8 files changed, 404 insertions(+), 39 deletions(-) create mode 100644 GFramework.Game.Abstractions/scene/ISceneAroundTransitionHandler.cs create mode 100644 GFramework.Game.Abstractions/ui/IUiAroundTransitionHandler.cs diff --git a/GFramework.Game.Abstractions/enums/SceneTransitionPhases.cs b/GFramework.Game.Abstractions/enums/SceneTransitionPhases.cs index 8745188..cfaf962 100644 --- a/GFramework.Game.Abstractions/enums/SceneTransitionPhases.cs +++ b/GFramework.Game.Abstractions/enums/SceneTransitionPhases.cs @@ -34,9 +34,16 @@ public enum SceneTransitionPhases /// AfterChange = 2, + /// + /// 中间件阶段(阻塞执行)。 + /// 用于包裹整个场景切换过程的逻辑,如性能监控、事务管理、权限验证等。 + /// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。 + /// + Around = 4, + /// /// 所有阶段的组合标志。 /// 表示处理器适用于场景切换的所有阶段。 /// - All = BeforeChange | AfterChange + All = BeforeChange | AfterChange | Around } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/enums/UITransitionPhases.cs b/GFramework.Game.Abstractions/enums/UITransitionPhases.cs index c87bf72..3d2ff6e 100644 --- a/GFramework.Game.Abstractions/enums/UITransitionPhases.cs +++ b/GFramework.Game.Abstractions/enums/UITransitionPhases.cs @@ -19,7 +19,14 @@ public enum UiTransitionPhases AfterChange = 2, /// - /// 所有阶段,Handler将在BeforeChange和AfterChange阶段都执行 + /// 中间件阶段,支持包裹整个变更过程的逻辑(阻塞执行) + /// 适用于:性能监控、事务管理、权限验证、日志记录开始/结束等需要控制流程的操作 + /// Around 处理器在变更前后都会执行,可以决定是否继续执行后续逻辑 /// - All = BeforeChange | AfterChange + Around = 4, + + /// + /// 所有阶段,Handler将在BeforeChange、AfterChange和Around阶段都执行 + /// + All = BeforeChange | AfterChange | Around } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/scene/ISceneAroundTransitionHandler.cs b/GFramework.Game.Abstractions/scene/ISceneAroundTransitionHandler.cs new file mode 100644 index 0000000..e73d7c7 --- /dev/null +++ b/GFramework.Game.Abstractions/scene/ISceneAroundTransitionHandler.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace GFramework.Game.Abstractions.scene; + +/// +/// 场景切换中间件处理器接口,支持包裹整个变更过程的逻辑。 +/// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。 +/// 适用于:性能监控、事务管理、权限验证、日志记录等横切关注点。 +/// +public interface ISceneAroundTransitionHandler +{ + /// + /// 获取处理器的执行优先级。 + /// 数值越小优先级越高,越先执行(外层)。 + /// 建议范围:-1000 到 1000。 + /// + int Priority { get; } + + /// + /// 判断处理器是否应该处理当前场景过渡事件。 + /// + /// 场景过渡事件。 + /// 如果应该处理则返回 true,否则返回 false。 + bool ShouldHandle(SceneTransitionEvent @event); + + /// + /// 执行中间件逻辑。 + /// + /// 场景过渡事件,包含切换的上下文信息。 + /// 下一个中间件或实际操作的委托。调用此委托以继续执行流程。 + /// 取消令牌,用于支持操作取消。 + /// 表示处理操作完成的异步任务。 + Task HandleAsync( + SceneTransitionEvent @event, + Func next, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiAroundTransitionHandler.cs b/GFramework.Game.Abstractions/ui/IUiAroundTransitionHandler.cs new file mode 100644 index 0000000..9d06634 --- /dev/null +++ b/GFramework.Game.Abstractions/ui/IUiAroundTransitionHandler.cs @@ -0,0 +1,34 @@ +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI切换中间件处理器接口,支持包裹整个变更过程的逻辑。 +/// Around 处理器在变更前后都会执行,可以控制是否继续执行变更。 +/// 适用于:性能监控、事务管理、权限验证、日志记录等横切关注点。 +/// +public interface IUiAroundTransitionHandler +{ + /// + /// 处理器优先级,数值越小越先执行(外层)。 + /// 建议范围:-1000 到 1000。 + /// + int Priority { get; } + + /// + /// 判断是否应该处理当前事件。 + /// + /// UI切换事件。 + /// 如果应该处理则返回 true,否则返回 false。 + bool ShouldHandle(UiTransitionEvent @event); + + /// + /// 执行中间件逻辑。 + /// + /// UI切换事件。 + /// 下一个中间件或实际操作的委托。调用此委托以继续执行流程。 + /// 取消令牌。 + /// 异步任务。 + Task HandleAsync( + UiTransitionEvent @event, + Func next, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/GFramework.Game/scene/SceneRouterBase.cs b/GFramework.Game/scene/SceneRouterBase.cs index a557845..62de16f 100644 --- a/GFramework.Game/scene/SceneRouterBase.cs +++ b/GFramework.Game/scene/SceneRouterBase.cs @@ -92,10 +92,13 @@ public abstract class SceneRouterBase var @event = CreateEvent(sceneKey, SceneTransitionType.Replace, param); - await BeforeChangeAsync(@event); - await ClearInternalAsync(); - await PushInternalAsync(sceneKey, param); - AfterChange(@event); + await _pipeline.ExecuteAroundAsync(@event, async () => + { + await BeforeChangeAsync(@event); + await ClearInternalAsync(); + await PushInternalAsync(sceneKey, param); + AfterChange(@event); + }); } finally { @@ -209,9 +212,12 @@ public abstract class SceneRouterBase var @event = CreateEvent(sceneKey, SceneTransitionType.Push, param); - await BeforeChangeAsync(@event); - await PushInternalAsync(sceneKey, param); - AfterChange(@event); + await _pipeline.ExecuteAroundAsync(@event, async () => + { + await BeforeChangeAsync(@event); + await PushInternalAsync(sceneKey, param); + AfterChange(@event); + }); } finally { @@ -287,9 +293,12 @@ public abstract class SceneRouterBase var @event = CreateEvent(null, SceneTransitionType.Pop); - await BeforeChangeAsync(@event); - await PopInternalAsync(); - AfterChange(@event); + await _pipeline.ExecuteAroundAsync(@event, async () => + { + await BeforeChangeAsync(@event); + await PopInternalAsync(); + AfterChange(@event); + }); } finally { @@ -355,9 +364,12 @@ public abstract class SceneRouterBase var @event = CreateEvent(null, SceneTransitionType.Clear); - await BeforeChangeAsync(@event); - await ClearInternalAsync(); - AfterChange(@event); + await _pipeline.ExecuteAroundAsync(@event, async () => + { + await BeforeChangeAsync(@event); + await ClearInternalAsync(); + AfterChange(@event); + }); } finally { diff --git a/GFramework.Game/scene/SceneTransitionPipeline.cs b/GFramework.Game/scene/SceneTransitionPipeline.cs index 3c0e741..f78594c 100644 --- a/GFramework.Game/scene/SceneTransitionPipeline.cs +++ b/GFramework.Game/scene/SceneTransitionPipeline.cs @@ -25,6 +25,8 @@ namespace GFramework.Game.scene; public class SceneTransitionPipeline { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneTransitionPipeline)); + private readonly List _aroundHandlers = []; + private readonly Dictionary _aroundOptions = new(); private readonly List _handlers = []; private readonly Dictionary _options = new(); @@ -67,6 +69,44 @@ public class SceneTransitionPipeline Log.Debug("Handler unregistered: {0}", handler.GetType().Name); } + /// + /// 注册 Around 中间件处理器。 + /// + /// 处理器实例。 + /// 执行选项,如果为 null 则使用默认选项。 + public void RegisterAroundHandler(ISceneAroundTransitionHandler handler, + SceneTransitionHandlerOptions? options = null) + { + ArgumentNullException.ThrowIfNull(handler); + + if (_aroundHandlers.Contains(handler)) + { + Log.Debug("Around handler already registered: {0}", handler.GetType().Name); + return; + } + + _aroundHandlers.Add(handler); + _aroundOptions[handler] = options ?? new SceneTransitionHandlerOptions(); + Log.Debug( + "Around handler registered: {0}, Priority={1}", + handler.GetType().Name, + handler.Priority + ); + } + + /// + /// 注销 Around 中间件处理器。 + /// + /// 处理器实例。 + public void UnregisterAroundHandler(ISceneAroundTransitionHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + + if (!_aroundHandlers.Remove(handler)) return; + _aroundOptions.Remove(handler); + Log.Debug("Around handler unregistered: {0}", handler.GetType().Name); + } + /// /// 执行指定阶段的所有处理器。 /// @@ -114,6 +154,53 @@ public class SceneTransitionPipeline Log.Debug("Pipeline execution completed for phases: {0}", phases); } + /// + /// 执行 Around 中间件处理器,包裹核心操作。 + /// + /// 场景过渡事件。 + /// 核心操作委托。 + /// 取消令牌。 + /// 异步任务。 + public async Task ExecuteAroundAsync( + SceneTransitionEvent @event, + Func coreAction, + CancellationToken cancellationToken = default) + { + var handlers = _aroundHandlers + .Where(h => h.ShouldHandle(@event)) + .OrderBy(h => h.Priority) + .ToList(); + + if (handlers.Count == 0) + { + await coreAction(); + return; + } + + Log.Debug( + "Executing {0} around handlers for event: {1}", + handlers.Count, + @event.TransitionType + ); + + // 构建中间件链 + Func pipeline = coreAction; + for (int i = handlers.Count - 1; i >= 0; i--) + { + var handler = handlers[i]; + var options = _aroundOptions[handler]; + var next = pipeline; + + pipeline = async () => + { + await ExecuteSingleAroundHandlerAsync( + handler, options, @event, next, cancellationToken); + }; + } + + await pipeline(); + } + private List FilterAndSortHandlers( SceneTransitionEvent @event, SceneTransitionPhases phases) @@ -179,4 +266,37 @@ public class SceneTransitionPipeline throw; } } + + private static async Task ExecuteSingleAroundHandlerAsync( + ISceneAroundTransitionHandler handler, + SceneTransitionHandlerOptions options, + SceneTransitionEvent @event, + Func next, + CancellationToken cancellationToken) + { + Log.Debug("Executing around handler: {0}", handler.GetType().Name); + + try + { + using var timeoutCts = options.TimeoutMs > 0 + ? new CancellationTokenSource(options.TimeoutMs) + : null; + + using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token) + : null; + + await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken); + + Log.Debug("Around handler completed: {0}", handler.GetType().Name); + } + catch (Exception ex) + { + Log.Error("Around handler failed: {0}, Error: {1}", + handler.GetType().Name, ex.Message); + + if (!options.ContinueOnError) + throw; + } + } } \ No newline at end of file diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs index 7be98ee..b860cee 100644 --- a/GFramework.Game/ui/UiRouterBase.cs +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -89,9 +89,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count); - BeforeChange(@event); - DoPushPageInternal(uiKey, param, policy); - AfterChange(@event); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoPushPageInternal(uiKey, param, policy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// @@ -111,9 +114,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count); - BeforeChange(@event); - DoPushPageInternal(page, param, policy); - AfterChange(@event); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoPushPageInternal(page, param, policy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// @@ -138,9 +144,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var nextUiKey = _stack.Count > 1 ? _stack.ElementAt(1).Key : null; var @event = CreateEvent(nextUiKey, UiTransitionType.Pop); - BeforeChange(@event); - DoPopInternal(policy); - AfterChange(@event); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoPopInternal(policy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// @@ -153,14 +162,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); Log.Debug("Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy); - BeforeChange(@event); - DoClearInternal(popPolicy); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoClearInternal(popPolicy); - var page = _factory.Create(uiKey); - Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name); + var page = _factory.Create(uiKey); + Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name); - DoPushPageInternal(page, param, pushPolicy); - AfterChange(@event); + DoPushPageInternal(page, param, pushPolicy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// @@ -175,11 +187,14 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter Log.Debug("Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy); - BeforeChange(@event); - DoClearInternal(popPolicy); - Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name); - DoPushPageInternal(page, param, pushPolicy); - AfterChange(@event); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoClearInternal(popPolicy); + Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name); + DoPushPageInternal(page, param, pushPolicy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// @@ -190,9 +205,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var @event = CreateEvent(string.Empty, UiTransitionType.Clear); Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count); - BeforeChange(@event); - DoClearInternal(UiPopPolicy.Destroy); - AfterChange(@event); + _pipeline.ExecuteAroundAsync(@event, async () => + { + BeforeChange(@event); + DoClearInternal(UiPopPolicy.Destroy); + AfterChange(@event); + }).GetAwaiter().GetResult(); } /// diff --git a/GFramework.Game/ui/UiTransitionPipeline.cs b/GFramework.Game/ui/UiTransitionPipeline.cs index c5bd9e4..00605f1 100644 --- a/GFramework.Game/ui/UiTransitionPipeline.cs +++ b/GFramework.Game/ui/UiTransitionPipeline.cs @@ -11,6 +11,8 @@ namespace GFramework.Game.ui; public class UiTransitionPipeline { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("UiTransitionPipeline"); + private readonly List _aroundHandlers = []; + private readonly Dictionary _aroundOptions = new(); private readonly List _handlers = []; private readonly Dictionary _options = new(); @@ -53,6 +55,43 @@ public class UiTransitionPipeline Log.Debug("Handler unregistered: {0}", handler.GetType().Name); } + /// + /// 注册 Around 中间件处理器 + /// + /// 处理器实例 + /// 执行选项 + public void RegisterAroundHandler(IUiAroundTransitionHandler handler, UiTransitionHandlerOptions? options = null) + { + ArgumentNullException.ThrowIfNull(handler); + + if (_aroundHandlers.Contains(handler)) + { + Log.Debug("Around handler already registered: {0}", handler.GetType().Name); + return; + } + + _aroundHandlers.Add(handler); + _aroundOptions[handler] = options ?? new UiTransitionHandlerOptions(); + Log.Debug( + "Around handler registered: {0}, Priority={1}", + handler.GetType().Name, + handler.Priority + ); + } + + /// + /// 注销 Around 中间件处理器 + /// + /// 处理器实例 + public void UnregisterAroundHandler(IUiAroundTransitionHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + + if (!_aroundHandlers.Remove(handler)) return; + _aroundOptions.Remove(handler); + Log.Debug("Around handler unregistered: {0}", handler.GetType().Name); + } + /// /// 执行指定阶段的所有Handler /// @@ -100,6 +139,53 @@ public class UiTransitionPipeline Log.Debug("Pipeline execution completed for phases: {0}", phases); } + /// + /// 执行 Around 中间件处理器,包裹核心操作 + /// + /// UI切换事件 + /// 核心操作委托 + /// 取消令牌 + /// 异步任务 + public async Task ExecuteAroundAsync( + UiTransitionEvent @event, + Func coreAction, + CancellationToken cancellationToken = default) + { + var handlers = _aroundHandlers + .Where(h => h.ShouldHandle(@event)) + .OrderBy(h => h.Priority) + .ToList(); + + if (handlers.Count == 0) + { + await coreAction(); + return; + } + + Log.Debug( + "Executing {0} around handlers for event: {1}", + handlers.Count, + @event.TransitionType + ); + + // 构建中间件链 + Func pipeline = coreAction; + for (int i = handlers.Count - 1; i >= 0; i--) + { + var handler = handlers[i]; + var options = _aroundOptions[handler]; + var next = pipeline; + + pipeline = async () => + { + await ExecuteSingleAroundHandlerAsync( + handler, options, @event, next, cancellationToken); + }; + } + + await pipeline(); + } + private List FilterAndSortHandlers( UiTransitionEvent @event, UiTransitionPhases phases) @@ -165,4 +251,37 @@ public class UiTransitionPipeline throw; } } + + private static async Task ExecuteSingleAroundHandlerAsync( + IUiAroundTransitionHandler handler, + UiTransitionHandlerOptions options, + UiTransitionEvent @event, + Func next, + CancellationToken cancellationToken) + { + Log.Debug("Executing around handler: {0}", handler.GetType().Name); + + try + { + using var timeoutCts = options.TimeoutMs > 0 + ? new CancellationTokenSource(options.TimeoutMs) + : null; + + using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token) + : null; + + await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken); + + Log.Debug("Around handler completed: {0}", handler.GetType().Name); + } + catch (Exception ex) + { + Log.Error("Around handler failed: {0}, Error: {1}", + handler.GetType().Name, ex.Message); + + if (!options.ContinueOnError) + throw; + } + } } \ No newline at end of file