提交文档

This commit is contained in:
JSD\13999
2026-06-18 15:11:42 +08:00
parent b87465d80f
commit 491eb27cd2
5 changed files with 3763 additions and 0 deletions

View 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 订阅 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、显式绑定、显式资源、显式生命周期。