Files
FlowScope/docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md
2026-06-18 15:11:42 +08:00

474 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# uFrame MVVM Core 分析
日期2026-06-12
## 结论
uFrame MVVM 的核心价值不在它的 Kernel、IOC 启动或代码生成器,而在运行时 MVVM 的几个机制:
- `ViewModel` 持有可观察状态。
- `P<T>` 把 ViewModel 属性包装成可订阅的数据流。
- `Signal<TCommand>` 把 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<T>
ModelCollection<T>
Signal<TCommand>
ViewModelCommand
View 层
ViewBase
View<TModel>
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<T>
`P<T>` 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property
```csharp
public class P<T> : ISubject<T>, IObservableProperty, ISimpleNotifyPropertyChanged
```
它提供:
- `Value`:当前值。
- `LastValue`:上一次值。
- `ChangedObservable`:只在值变化时通知。
- `ObjectValue`:非泛型访问入口。
- `Owner`:所属 ViewModel。
- `PropertyName`:属性名。
- `ToComputed`:根据其他 observable property 计算派生值。
View 侧可以订阅 `P<T>`,属性变化时刷新 UI。这个机制适合 Unity UI因为它避免了大量手动 `Refresh()` 和每帧轮询。
可取点:
- 属性变化可以显式订阅。
- 派生属性可以声明依赖。
- View 绑定时可以立即应用当前值。
- IDisposable 可以挂到绑定作用域统一释放。
风险点:
- `Value` setter 没有先比较新旧值,任何赋值都会触发通知。
- `Owner` 让 property 反向知道 ViewModel。
- `P<T>` 同时实现 subject、property、notify职责偏多。
- `ChangedObservable` 依赖 `Value``LastValue` 的外部状态,语义不够直观。
FlowScope 如果吸收这个思路,应改成更小的 `ObservableProperty<T>`
```text
只负责保存值、比较值、发布变化、释放订阅。
不要知道全局 Kernel。
不要知道 View。
不要知道资源或 Controller。
```
## Signal<TCommand>
`Signal<TCommand>` 用来表达 ViewModel 命令:
```csharp
public class Signal<TClass> : ISubject<TClass>, ISignal<TClass>
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<TModel>
`ViewBase` 是 View 与 ViewModel 的连接点。它提供:
- `ViewModelObject`:当前绑定的 ViewModel。
- `Bind()`:用户编写订阅逻辑。
- `PreBind()`:生成器插入绑定逻辑。
- `AfterBind()`:绑定完成后的 hook。
- `Unbind()`:释放绑定。
- `AddBinding()`:把 disposable 放到当前 View 的绑定列表。
- `BindOnStart`:是否启动时自动绑定。
- `DisposeViewModelOnDestroy`View 销毁时是否销毁 ViewModel。
`View<TModel>` 只是给 `ViewBase` 加了泛型 Model 访问:
```csharp
public abstract class View<TModel> : 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<Controller>(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<TView, TViewModel>
负责绑定和解绑。
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<T>` 思路。
- `ObservableCollection<T>` 思路。
- UI command / intent stream。
- `Bind / Unbind` 生命周期。
- `IDisposable` binding scope。
- View 订阅 ViewModelViewModel 不直接操作 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<T>
保存值并发布变化。
BindingScope
持有 IDisposable 列表Dispose 时统一释放。
IViewModel
标记 UI 状态对象,不依赖 Kernel。
IViewBinder<TView, TViewModel>
Bind(view, vm, scope)
Unbind 由 scope.Dispose 承担。
UIView<TViewModel>
MonoBehaviour只持有 Unity UI 引用和当前 ViewModel。
UICommand<T>
feature-local command不走全局事件总线。
```
示意:
```csharp
public sealed class BindingScope : IDisposable
{
private readonly List<IDisposable> _items = new();
public T Add<T>(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<string> Title { get; } = new("Main Menu");
public Subject<StartGameIntent> 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、显式绑定、显式资源、显式生命周期。