# State Management 包使用说明 ## 概述 State Management 提供一个可选的集中式状态容器方案,用于补足 `BindableProperty` 在复杂状态树场景下的能力。 当你的状态具有以下特征时,推荐使用 `Store`: - 多个字段需要在一次业务操作中协同更新 - 多个模块或 UI 片段共享同一聚合状态 - 希望所有状态写入都经过统一的 action / reducer 入口 - 需要对整棵状态树做局部选择和按片段订阅 这套能力不会替代现有 Property 机制,而是与其并存: - `BindableProperty`:字段级响应式值 - `Store`:聚合状态容器 - `StateMachine`:流程状态切换 ## 核心接口 ### IReadonlyStore`` 只读状态容器接口,提供: - `State`:读取当前状态快照 - `Subscribe()`:订阅状态变化 - `SubscribeWithInitValue()`:订阅并立即回放当前状态 - `UnSubscribe()`:取消订阅 ### IStore`` 在只读能力上增加: - `Dispatch()`:统一分发 action ### IReducer`` 定义状态归约逻辑: ```csharp public interface IReducer { TState Reduce(TState currentState, TAction action); } ``` ### IStateSelector`` 从整棵状态树中投影局部视图,便于 UI 和 Controller 复用选择逻辑。 ## Store`` `Store` 是默认实现,支持: - 初始状态快照 - reducer 注册 - middleware 分发管线 - 只在状态真正变化时通知订阅者 - 基础诊断信息(最近一次 action、最近一次分发记录、最近一次状态变化时间) ## 基本示例 ```csharp using GFramework.Core.StateManagement; public sealed record PlayerState(int Health, string Name); public sealed record DamageAction(int Amount); public sealed record RenameAction(string Name); var store = new Store(new PlayerState(100, "Player")) .RegisterReducer((state, action) => state with { Health = Math.Max(0, state.Health - action.Amount) }) .RegisterReducer((state, action) => state with { Name = action.Name }); store.SubscribeWithInitValue(state => { Console.WriteLine($"{state.Name}: {state.Health}"); }); store.Dispatch(new DamageAction(25)); store.Dispatch(new RenameAction("Knight")); ``` ## 选择器和 Bindable 风格桥接 Store 可以通过扩展方法把聚合状态投影成局部只读绑定视图: ```csharp using GFramework.Core.Extensions; var healthSelection = store.Select(state => state.Health); healthSelection.RegisterWithInitValue(health => { Console.WriteLine($"Current HP: {health}"); }); ``` 如果现有 UI 代码已经依赖 `IReadonlyBindableProperty`,可以直接桥接: ```csharp IReadonlyBindableProperty healthProperty = store.ToBindableProperty(state => state.Health); ``` ## 在 Model 中使用 推荐把 Store 作为 Model 的内部状态容器,由 Model 暴露领域友好的业务方法: ```csharp public class PlayerStateModel : AbstractModel { public Store Store { get; } = new(new PlayerState(100, "Player")); protected override void OnInit() { Store.RegisterReducer((state, action) => state with { Health = Math.Max(0, state.Health - action.Amount) }); } public void TakeDamage(int amount) { Store.Dispatch(new DamageAction(amount)); } } ``` 这样可以保留 Model 的生命周期和领域边界,同时获得统一状态入口。 ## 使用 StoreBuilder 组织配置 当一个 Store 需要在模块安装、测试工厂或 DI 装配阶段统一配置时,可以使用 `StoreBuilder`: ```csharp var store = (Store)Store .CreateBuilder() .AddReducer((state, action) => state with { Health = Math.Max(0, state.Health - action.Amount) }) .Build(new PlayerState(100, "Player")); ``` 适合以下场景: - 模块启动时集中注册 reducer 和 middleware - 测试里快速组装不同配置的 Store - 不希望把 Store 的装配细节散落在多个调用点 ## 运行时临时注册与注销 如果某个 reducer 或 middleware 只需要在一段生命周期内生效,例如调试探针、临时玩法规则、 或场景级模块扩展,可以直接使用 `Store` 提供的句柄式注册 API: ```csharp public sealed class LoggingMiddleware : IStoreMiddleware { public void Invoke(StoreDispatchContext context, Action next) { Console.WriteLine($"Dispatching: {context.ActionType.Name}"); next(); } } var store = new Store(new PlayerState(100, "Player")); var reducerHandle = store.RegisterReducerHandle((state, action) => state with { Health = Math.Max(0, state.Health - action.Amount) }); var middlewareHandle = store.RegisterMiddleware(new LoggingMiddleware()); store.Dispatch(new DamageAction(10)); reducerHandle.UnRegister(); middlewareHandle.UnRegister(); ``` 这里有两个重要约束: - `RegisterReducerHandle()` 和 `RegisterMiddleware()` 返回的是当前这一次注册的精确注销句柄 - 如果在某次 `Dispatch()` 已经开始后再调用 `UnRegister()`,当前这次 dispatch 仍会继续使用开始时抓取的快照,注销只影响后续新的 dispatch 如果你只需要初始化阶段的链式配置,继续使用 `RegisterReducer()` 和 `UseMiddleware()` 即可; 如果你需要运行时清理,就使用上面的句柄式 API。 ## 官方示例:角色面板状态 下面给出一个更贴近 GFramework 实战的完整示例,展示如何把 `Store` 放进 Model, 再通过 Command 修改状态,并在 Controller 中使用 selector 做 UI 绑定。 ### 1. 定义状态和 action ```csharp public sealed record PlayerPanelState( string Name, int Health, int MaxHealth, int Level); public sealed record DamagePlayerAction(int Amount); public sealed record HealPlayerAction(int Amount); public sealed record RenamePlayerAction(string Name); ``` ### 2. 在 Model 中承载 Store ```csharp using GFramework.Core.Abstractions.Property; using GFramework.Core.Model; using GFramework.Core.Extensions; using GFramework.Core.StateManagement; public class PlayerPanelModel : AbstractModel { public Store Store { get; } = new(new PlayerPanelState("Player", 100, 100, 1)); // 使用带缓存的选择视图,避免属性 getter 每次访问都创建新的 StoreSelection 实例。 public IReadonlyBindableProperty Health => Store.GetOrCreateBindableProperty("health", state => state.Health); public IReadonlyBindableProperty Name => Store.GetOrCreateBindableProperty("name", state => state.Name); public IReadonlyBindableProperty HealthPercent => Store.GetOrCreateBindableProperty("health_percent", state => (float)state.Health / state.MaxHealth); protected override void OnInit() { Store .RegisterReducer((state, action) => state with { Health = Math.Max(0, state.Health - action.Amount) }) .RegisterReducer((state, action) => state with { Health = Math.Min(state.MaxHealth, state.Health + action.Amount) }) .RegisterReducer((state, action) => state with { Name = action.Name }); } } ``` 这个写法的关键点是: - 状态结构集中定义在 `PlayerPanelState` - 所有状态修改都经过 reducer - 高频访问的局部状态通过缓存选择视图复用实例 - Controller 只消费局部只读视图,不直接修改 Store ### 3. 通过 Command 修改状态 ```csharp using GFramework.Core.Command; public sealed class DamagePlayerCommand(int amount) : AbstractCommand { protected override void OnExecute() { var model = this.GetModel(); model.Store.Dispatch(new DamagePlayerAction(amount)); } } public sealed class RenamePlayerCommand(string name) : AbstractCommand { protected override void OnExecute() { var model = this.GetModel(); model.Store.Dispatch(new RenamePlayerAction(name)); } } ``` 这里仍然遵循 GFramework 现有分层: - Controller 负责转发用户意图 - Command 负责执行业务操作 - Model 持有状态 - Store 负责统一归约状态变化 ### 4. 在 Controller 中绑定局部状态 ```csharp using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Events; using GFramework.Core.Events; using GFramework.Core.Extensions; using GFramework.SourceGenerators.Abstractions.Rule; [ContextAware] public partial class PlayerPanelController : IController { private readonly IUnRegisterList _unRegisterList = new UnRegisterList(); public void Initialize() { var model = this.GetModel(); model.Name .RegisterWithInitValue(name => { Console.WriteLine($"Player Name: {name}"); }) .AddToUnregisterList(_unRegisterList); model.Health .RegisterWithInitValue(health => { Console.WriteLine($"Health: {health}"); }) .AddToUnregisterList(_unRegisterList); model.HealthPercent .RegisterWithInitValue(percent => { Console.WriteLine($"Health Percent: {percent:P0}"); }) .AddToUnregisterList(_unRegisterList); } public void OnDamageButtonClicked() { this.SendCommand(new DamagePlayerCommand(15)); } public void OnRenameButtonClicked(string newName) { this.SendCommand(new RenamePlayerCommand(newName)); } } ``` ### 5. 什么时候这个示例比 BindableProperty 更合适 如果你只需要: - `Health` - `Name` - `Level` 分别独立通知,那么多个 `BindableProperty` 就足够了。 如果你很快会遇到以下问题,这个 Store 方案会更稳: - 一次操作要同时修改多个字段 - 同一个业务操作要在多个界面复用 - 希望把“状态结构”和“状态变化规则”集中在一起 - 未来要加入 middleware、调试记录或撤销/重做能力 ### 6. 推荐的落地方式 在实际项目里,建议按这个顺序引入: 1. 先把复杂聚合状态封装到某个 Model 内部 2. 再把修改入口逐步迁移到 Command 3. 最后在 Controller 层使用 selector 或 `ToBindableProperty()` 做局部绑定 这样不会破坏现有 `BindableProperty` 的轻量工作流,也能让复杂状态逐步收敛到统一入口。 ## 什么时候不用 Store 以下情况继续优先使用 `BindableProperty`: - 单一字段直接绑定 UI - 状态规模很小,不需要聚合归约 - 没有跨模块共享状态树的需求 - 你只需要“值变化通知”,不需要“统一状态演进入口” ## 最佳实践 1. 优先把 `TState` 设计为不可变状态(如 `record`) 2. 让 reducer 保持纯函数风格,不在 reducer 内执行副作用 3. 使用 selector 暴露局部状态,而不是让 UI 自己解析整棵状态树 4. 需要日志或诊断时,优先通过 middleware 扩展,而不是把横切逻辑塞进 reducer ## 相关文档 - [`property`](./property) - 字段级响应式属性 - [`model`](./model) - Store 常见承载位置 - [`events`](./events) - 组件间事件通信 - [`state-machine-tutorial`](../tutorials/state-machine-tutorial) - 流程状态切换能力