提交文档
This commit is contained in:
473
docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md
Normal file
473
docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 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 订阅 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<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、显式绑定、显式资源、显式生命周期。
|
||||
Reference in New Issue
Block a user