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

14 KiB
Raw Permalink Blame History

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 当前公开仓库源码,重点阅读:

运行时结构

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。ViewModelViewControllerViewService 都会触碰 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 可以挂到绑定作用域统一释放。

风险点:

  • Value setter 没有先比较新旧值,任何赋值都会触发通知。
  • Owner 让 property 反向知道 ViewModel。
  • P<T> 同时实现 subject、property、notify职责偏多。
  • ChangedObservable 依赖 ValueLastValue 的外部状态,语义不够直观。

FlowScope 如果吸收这个思路,应改成更小的 ObservableProperty<T>

只负责保存值、比较值、发布变化、释放订阅。
不要知道全局 Kernel。
不要知道 View。
不要知道资源或 Controller。

Signal

Signal<TCommand> 用来表达 ViewModel 命令:

public class Signal<TClass> : ISubject<TClass>, ISignal<TClass>
    where TClass : IViewModelCommand, new()

它的行为是:

  1. View 或绑定层构造 command。
  2. 调用 Signal.OnNext(command)
  3. SignalSender 设置为当前 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:是否启动时自动绑定。
  • DisposeViewModelOnDestroyView 销毁时是否销毁 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 层包括:

  • Binding
  • ModelPropertyBinding
  • ModelViewPropertyBinding
  • ModelViewModelCollectionBinding
  • ViewBindings 扩展方法

最实用的是 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 生命周期。
  • 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 形态

建议的最小形态:

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