# uFrame MVVM Core 分析 日期:2026-06-12 ## 结论 uFrame MVVM 的核心价值不在它的 Kernel、IOC 启动或代码生成器,而在运行时 MVVM 的几个机制: - `ViewModel` 持有可观察状态。 - `P` 把 ViewModel 属性包装成可订阅的数据流。 - `Signal` 把 UI 操作表达成命令流。 - `ViewBase` 通过 `Bind / Unbind` 管理订阅生命周期。 - `ViewService` 负责把 View 与 ViewModel 对接。 但这套 MVVM 被 uFrame 自身的全局 Kernel、全局容器、命名约定、`Resources.Load` 和设计器生成代码强绑定。它可以作为反面边界样本和局部机制参考,不适合作为 FlowScope Kernel 或 UI 模块的直接原型。 FlowScope 可以吸收“可观察状态 + 命令流 + 绑定生命周期作用域”,但不应吸收它的全局 IOC 查找、ViewModel 自动解析、Controller 命名约定和资源自动定位。 ## 源码范围 本次分析基于 `uFrame/uFrame.MVVM` 当前公开仓库源码,重点阅读: - [`ViewModel.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ViewModel.cs) - [`ModelPropertyBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ModelPropertyBase.cs) - [`Signal.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/Signal.cs) - [`ViewBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewBase.cs) - [`View.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/View.cs) - [`Controller.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Controllers/Controller.cs) - [`ViewService.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Services/ViewService.cs) - [`MVVMKernelExtensions.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/MVVMKernelExtensions.cs) - [`ViewBindings.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Bindings/ViewBindings.cs) - [`ViewResolver.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewResolver.cs) ## 运行时结构 uFrame MVVM 的运行时可以拆成五组对象: ```text ViewModel 层 ViewModel P ModelCollection Signal ViewModelCommand View 层 ViewBase View ViewComponent ViewBindings 创建与连接层 Controller ViewService ViewResolver 绑定层 Binding ModelPropertyBinding ModelViewPropertyBinding ModelViewModelCollectionBinding 全局基础设施 uFrameKernel.Container IEventAggregator SystemService / SystemServiceMonoBehavior ``` 它不是一个独立、纯粹的 MVVM runtime。`ViewModel`、`View`、`Controller` 和 `ViewService` 都会触碰 uFrame Kernel 或全局容器。 ## ViewModel `ViewModel` 是所有生成 ViewModel 的基类。它承担了状态、绑定、序列化、事件发布、销毁通知等多种职责。 关键职责: - 通过 `FillProperties` 暴露属性元信息。 - 通过 `FillCommands` 暴露命令元信息。 - 持有 `Bindings`,用于释放订阅。 - 持有 `Identifier`,用于全局容器按 id 查找。 - 通过 `PropertyChanged` 通知属性变化。 - `Dispose` 时发布 `ViewModelDestroyedEvent`。 问题是它太重。一个干净的 ViewModel 应该主要表达 UI 状态和 UI 意图,但这里的 `ViewModel` 混入了: - 序列化协议。 - `IBindable`。 - `IDisposable`。 - 全局事件聚合器。 - binding 容器。 - 引用计数。 - controller wiring hook。 这会让 ViewModel 从“状态对象”变成“框架对象”。长期项目里,这种设计会让测试、复用和生命周期判断都变复杂。 ## P `P` 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property: ```csharp public class P : ISubject, IObservableProperty, ISimpleNotifyPropertyChanged ``` 它提供: - `Value`:当前值。 - `LastValue`:上一次值。 - `ChangedObservable`:只在值变化时通知。 - `ObjectValue`:非泛型访问入口。 - `Owner`:所属 ViewModel。 - `PropertyName`:属性名。 - `ToComputed`:根据其他 observable property 计算派生值。 View 侧可以订阅 `P`,属性变化时刷新 UI。这个机制适合 Unity UI,因为它避免了大量手动 `Refresh()` 和每帧轮询。 可取点: - 属性变化可以显式订阅。 - 派生属性可以声明依赖。 - View 绑定时可以立即应用当前值。 - IDisposable 可以挂到绑定作用域统一释放。 风险点: - `Value` setter 没有先比较新旧值,任何赋值都会触发通知。 - `Owner` 让 property 反向知道 ViewModel。 - `P` 同时实现 subject、property、notify,职责偏多。 - `ChangedObservable` 依赖 `Value` 和 `LastValue` 的外部状态,语义不够直观。 FlowScope 如果吸收这个思路,应改成更小的 `ObservableProperty`: ```text 只负责保存值、比较值、发布变化、释放订阅。 不要知道全局 Kernel。 不要知道 View。 不要知道资源或 Controller。 ``` ## Signal `Signal` 用来表达 ViewModel 命令: ```csharp public class Signal : ISubject, ISignal where TClass : IViewModelCommand, new() ``` 它的行为是: 1. View 或绑定层构造 command。 2. 调用 `Signal.OnNext(command)`。 3. `Signal` 把 `Sender` 设置为当前 ViewModel。 4. 执行本地 `Action`。 5. 推送给订阅者。 这个方向是好的:UI 操作不应该直接调用业务系统,而应该变成可观察的 command 或 intent。 可取点: - UI 输入被建模为显式命令。 - 命令可以订阅、组合、测试。 - View 与业务处理之间不需要直接互相引用。 风险点: - command 的 sender 被 `Signal` 自动写入,有隐式副作用。 - 命令流最终仍然容易接到全局 EventAggregator。 - 生成器会为每个命令生成强约定代码,手写维护成本高。 FlowScope 可以吸收 command stream,但建议命名为更中性的 `UIIntent` / `ViewAction` / `FeatureCommand`,并放在 feature scope 内,不进入 Kernel core。 ## ViewBase / View `ViewBase` 是 View 与 ViewModel 的连接点。它提供: - `ViewModelObject`:当前绑定的 ViewModel。 - `Bind()`:用户编写订阅逻辑。 - `PreBind()`:生成器插入绑定逻辑。 - `AfterBind()`:绑定完成后的 hook。 - `Unbind()`:释放绑定。 - `AddBinding()`:把 disposable 放到当前 View 的绑定列表。 - `BindOnStart`:是否启动时自动绑定。 - `DisposeViewModelOnDestroy`:View 销毁时是否销毁 ViewModel。 `View` 只是给 `ViewBase` 加了泛型 Model 访问: ```csharp public abstract class View : ViewBase where TModel : ViewModel, new() ``` 可取点: - View 的绑定逻辑有明确生命周期。 - 订阅统一挂到 binding scope。 - View 只响应 ViewModel 的状态,不需要每帧刷 UI。 - View 销毁时可以集中释放订阅。 问题点: - `ViewBase.KernelLoaded()` 会调用 `uFrameKernel.Container.Inject(this)`。 - `OnDestroy()` 依赖 `uFrameKernel.IsKernelLoaded` 和全局事件。 - View 的 `Bindings` 实际挂在 `ViewModelObject.Bindings[ViewId]` 上,View 和 ViewModel 的生命周期互相缠住。 - `References` 引用计数用于判断 ViewModel 是否销毁,复杂且脆弱。 FlowScope 可以借鉴 `Bind / Unbind / AddBinding`,但绑定容器应属于 View 或 feature-local binding scope,而不是塞进 ViewModel。 ## Controller uFrame 的 `Controller` 是特殊的 `SystemService`,主要负责创建和初始化 ViewModel。 流程大致是: ```text Controller.Create(identifier) -> CreateEmpty(identifier) -> RegisterViewModel(vm, identifier) -> Initialize(vm) -> Publish ViewModelCreatedEvent ``` 问题在于 `CreateEmpty` 内部直接注册到全局容器: ```text uFrameKernel.Container.RegisterViewModel(vm, identifier) ``` `MVVMKernelExtensions.CreateViewModel(type)` 又会通过 ViewModel 类型名去全局容器找 Controller: ```text Resolve(type.Name) ``` 这是强命名约定 + 服务定位器。它表面上把 ViewModel 创建集中到了 Controller,实际上把依赖关系藏进全局容器和类型名字符串。 FlowScope 不应该吸收这套 Controller 模式。更合适的是: ```text Feature 明确创建 ViewModel。 Feature 明确创建/绑定 View。 FeatureContext 提供本 feature 需要的服务。 UI module 不通过全局容器按名字找 Controller。 ``` ## ViewService / ViewResolver `ViewService` 负责: - 监听 `InstantiateViewCommand`。 - 根据 ViewModel 找 View prefab。 - 实例化 View。 - 在 View 创建时补齐 ViewModel。 - View 销毁时移除记录。 - ViewModel 销毁时从全局容器移除实例。 `ViewResolver` 默认根据 ViewModel 类型名找 prefab: ```text PlayerViewModel -> Player -> Resources.Load("Player") ``` 这是老式 Unity 框架常见做法,但不适合 FlowScope: - 资源路径是隐式命名约定。 - 默认绑定到 `Resources.Load`。 - 不支持现代资源后端的显式 handle、group、release。 - 不利于 Addressables / YooAsset / AssetBundle / 自定义资源系统适配。 - View 的创建和 ViewModel 的创建混在同一个服务里。 FlowScope 如果需要 UI 模块,可以把它拆成: ```text IViewFactory 负责创建 View,不负责创建 ViewModel。 IViewBinder 负责绑定和解绑。 IUIViewRegistry 或 IViewCatalog 负责显式注册 view key 到 prefab/address。 IResourceService adapter 负责真实资源加载和释放。 ``` ## Binding uFrame 的 binding 层包括: - `Binding` - `ModelPropertyBinding` - `ModelViewPropertyBinding` - `ModelViewModelCollectionBinding` - `ViewBindings` 扩展方法 最实用的是 `ViewBindings.BindProperty`: ```text 立即用当前值刷新一次 UI。 订阅后续变化。 把 IDisposable 加入 bindable 的绑定列表。 ``` 这是 FlowScope 可以吸收的重点: ```csharp bindingScope.Bind(vm.Title, value => titleText.text = value); ``` 但不要吸收它的反射绑定和生成器绑定作为 core。反射/生成器可以是工具层能力,不应该成为 UI runtime 的基本前提。 ## 与 FlowScope Kernel 的关系 uFrame MVVM 不应该进入 FlowScope Kernel core。 FlowScope Kernel 已经明确应保持机制小核: ```text Container AppKernel StartupPipeline FeatureHost FeatureInstance FeatureContext FeatureLifetime FeatureSlot FeaturePolicy ``` MVVM 属于更高层的 UI module 或 feature-local module。Kernel 最多提供 feature 生命周期和上下文,不提供: - ViewModel 基类。 - View 自动绑定。 - UI prefab 查找。 - 命令总线。 - 全局 ViewService。 - 全局事件聚合器。 - 全局 IOC 注入 View。 推荐边界: ```text FlowScope.Kernel 只负责 Feature 生命周期和 Slot/Policy 机制。 FlowScope.UI 提供 ViewModel、ObservableProperty、BindingScope、ViewBinder。 FlowScope.Resources 提供资源加载接口和后端适配。 Game Feature 自己声明 ViewModel、View、Binder、Command handler。 ``` ## 可吸收清单 可以吸收: - `ObservableProperty` 思路。 - `ObservableCollection` 思路。 - UI command / intent stream。 - `Bind / Unbind` 生命周期。 - `IDisposable` binding scope。 - View 订阅 ViewModel,ViewModel 不直接操作 View。 - View 创建和 ViewModel 绑定分离。 - 集合绑定生成子 View 的思路,但要显式管理资源释放。 不应吸收: - `uFrameKernel.Container` 全局创建 ViewModel。 - `Container.Inject(view)`。 - 按 `ViewModel` 类型名查找 `Controller`。 - 按 `ViewModel` 类型名 `Resources.Load` prefab。 - ViewModel 持有 View 绑定列表。 - ViewModel 引用计数决定销毁。 - 全局 EventAggregator 作为 UI command 默认通道。 - 代码生成器作为 runtime 必需条件。 - ViewModel 同时承担序列化、绑定、事件、销毁、controller hook。 ## FlowScope 可采用的最小 UI MVVM 形态 建议的最小形态: ```text ObservableProperty 保存值并发布变化。 BindingScope 持有 IDisposable 列表,Dispose 时统一释放。 IViewModel 标记 UI 状态对象,不依赖 Kernel。 IViewBinder Bind(view, vm, scope) Unbind 由 scope.Dispose 承担。 UIView MonoBehaviour,只持有 Unity UI 引用和当前 ViewModel。 UICommand feature-local command,不走全局事件总线。 ``` 示意: ```csharp public sealed class BindingScope : IDisposable { private readonly List _items = new(); public T Add(T item) where T : IDisposable { _items.Add(item); return item; } public void Dispose() { foreach (var item in _items) item.Dispose(); _items.Clear(); } } ``` ```csharp public sealed class MainMenuViewModel { public ObservableProperty Title { get; } = new("Main Menu"); public Subject StartGame { get; } = new(); } ``` ```csharp public sealed class MainMenuBinder { public void Bind(MainMenuView view, MainMenuViewModel vm, BindingScope scope) { scope.Bind(vm.Title, value => view.TitleText.text = value); scope.Add(view.StartButton.OnClickAsObservable() .Subscribe(_ => vm.StartGame.OnNext(new StartGameIntent()))); } } ``` 关键点是:这里没有全局容器查找,没有 `Resources.Load` 约定,没有 Kernel 注入 View,也没有 ViewModel 引用计数。 ## 最终判断 uFrame MVVM 的局部机制比它的 Kernel 更有参考价值,但必须拆开看。 真正值得保留的是: ```text 状态可观察 操作命令化 绑定有生命周期 View 不直接写业务 ``` 必须避免的是: ```text 全局容器创建一切 命名约定连接一切 ViewModel 变成框架对象 资源加载隐藏在 ViewResolver UI 模块污染 Kernel core ``` 对 FlowScope 来说,uFrame MVVM 最好的用途不是成为模板,而是提醒我们:UI MVVM 应该是 Kernel 之上的可选模块,且必须保持 feature-local、显式绑定、显式资源、显式生命周期。