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