14 KiB
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.csModelPropertyBase.csSignal.csViewBase.csView.csController.csViewService.csMVVMKernelExtensions.csViewBindings.csViewResolver.cs
运行时结构
uFrame MVVM 的运行时可以拆成五组对象:
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
P<T> 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property:
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 可以挂到绑定作用域统一释放。
风险点:
Valuesetter 没有先比较新旧值,任何赋值都会触发通知。Owner让 property 反向知道 ViewModel。P<T>同时实现 subject、property、notify,职责偏多。ChangedObservable依赖Value和LastValue的外部状态,语义不够直观。
FlowScope 如果吸收这个思路,应改成更小的 ObservableProperty<T>:
只负责保存值、比较值、发布变化、释放订阅。
不要知道全局 Kernel。
不要知道 View。
不要知道资源或 Controller。
Signal
Signal<TCommand> 用来表达 ViewModel 命令:
public class Signal<TClass> : ISubject<TClass>, ISignal<TClass>
where TClass : IViewModelCommand, new()
它的行为是:
- View 或绑定层构造 command。
- 调用
Signal.OnNext(command)。 Signal把Sender设置为当前 ViewModel。- 执行本地
Action。 - 推送给订阅者。
这个方向是好的: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<TModel> 只是给 ViewBase 加了泛型 Model 访问:
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。
流程大致是:
Controller.Create(identifier)
-> CreateEmpty(identifier)
-> RegisterViewModel(vm, identifier)
-> Initialize(vm)
-> Publish ViewModelCreatedEvent
问题在于 CreateEmpty 内部直接注册到全局容器:
uFrameKernel.Container.RegisterViewModel(vm, identifier)
MVVMKernelExtensions.CreateViewModel(type) 又会通过 ViewModel 类型名去全局容器找 Controller:
Resolve<Controller>(type.Name)
这是强命名约定 + 服务定位器。它表面上把 ViewModel 创建集中到了 Controller,实际上把依赖关系藏进全局容器和类型名字符串。
FlowScope 不应该吸收这套 Controller 模式。更合适的是:
Feature 明确创建 ViewModel。
Feature 明确创建/绑定 View。
FeatureContext 提供本 feature 需要的服务。
UI module 不通过全局容器按名字找 Controller。
ViewService / ViewResolver
ViewService 负责:
- 监听
InstantiateViewCommand。 - 根据 ViewModel 找 View prefab。
- 实例化 View。
- 在 View 创建时补齐 ViewModel。
- View 销毁时移除记录。
- ViewModel 销毁时从全局容器移除实例。
ViewResolver 默认根据 ViewModel 类型名找 prefab:
PlayerViewModel -> Player -> Resources.Load("Player")
这是老式 Unity 框架常见做法,但不适合 FlowScope:
- 资源路径是隐式命名约定。
- 默认绑定到
Resources.Load。 - 不支持现代资源后端的显式 handle、group、release。
- 不利于 Addressables / YooAsset / AssetBundle / 自定义资源系统适配。
- View 的创建和 ViewModel 的创建混在同一个服务里。
FlowScope 如果需要 UI 模块,可以把它拆成:
IViewFactory
负责创建 View,不负责创建 ViewModel。
IViewBinder<TView, TViewModel>
负责绑定和解绑。
IUIViewRegistry 或 IViewCatalog
负责显式注册 view key 到 prefab/address。
IResourceService adapter
负责真实资源加载和释放。
Binding
uFrame 的 binding 层包括:
BindingModelPropertyBindingModelViewPropertyBindingModelViewModelCollectionBindingViewBindings扩展方法
最实用的是 ViewBindings.BindProperty:
立即用当前值刷新一次 UI。
订阅后续变化。
把 IDisposable 加入 bindable 的绑定列表。
这是 FlowScope 可以吸收的重点:
bindingScope.Bind(vm.Title, value => titleText.text = value);
但不要吸收它的反射绑定和生成器绑定作为 core。反射/生成器可以是工具层能力,不应该成为 UI runtime 的基本前提。
与 FlowScope Kernel 的关系
uFrame MVVM 不应该进入 FlowScope Kernel core。
FlowScope Kernel 已经明确应保持机制小核:
Container
AppKernel
StartupPipeline
FeatureHost
FeatureInstance
FeatureContext
FeatureLifetime
FeatureSlot
FeaturePolicy
MVVM 属于更高层的 UI module 或 feature-local module。Kernel 最多提供 feature 生命周期和上下文,不提供:
- ViewModel 基类。
- View 自动绑定。
- UI prefab 查找。
- 命令总线。
- 全局 ViewService。
- 全局事件聚合器。
- 全局 IOC 注入 View。
推荐边界:
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生命周期。IDisposablebinding scope。- View 订阅 ViewModel,ViewModel 不直接操作 View。
- View 创建和 ViewModel 绑定分离。
- 集合绑定生成子 View 的思路,但要显式管理资源释放。
不应吸收:
uFrameKernel.Container全局创建 ViewModel。Container.Inject(view)。- 按
ViewModel类型名查找Controller。 - 按
ViewModel类型名Resources.Loadprefab。 - ViewModel 持有 View 绑定列表。
- ViewModel 引用计数决定销毁。
- 全局 EventAggregator 作为 UI command 默认通道。
- 代码生成器作为 runtime 必需条件。
- ViewModel 同时承担序列化、绑定、事件、销毁、controller hook。
FlowScope 可采用的最小 UI MVVM 形态
建议的最小形态:
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,不走全局事件总线。
示意:
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();
}
}
public sealed class MainMenuViewModel
{
public ObservableProperty<string> Title { get; } = new("Main Menu");
public Subject<StartGameIntent> StartGame { get; } = new();
}
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 更有参考价值,但必须拆开看。
真正值得保留的是:
状态可观察
操作命令化
绑定有生命周期
View 不直接写业务
必须避免的是:
全局容器创建一切
命名约定连接一切
ViewModel 变成框架对象
资源加载隐藏在 ViewResolver
UI 模块污染 Kernel core
对 FlowScope 来说,uFrame MVVM 最好的用途不是成为模板,而是提醒我们:UI MVVM 应该是 Kernel 之上的可选模块,且必须保持 feature-local、显式绑定、显式资源、显式生命周期。