You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TornadoFX中绑定不同ViewModel脏属性及动态重绑定问题

这个问题在TornadoFX的嵌套ViewModel场景里太常见了——因为你用的是不可变数据类,子ViewModel的脏状态没法自动向上同步到父ViewModel,而且切换选中人员时的状态重置也得手动处理。我给你两种可行的解决方案,还有一些优化点:

方案一:手动监听子ViewModel的脏状态,同步父ViewModel

这种方式最直观,我们在PersonViewModel里监听每个CarViewModel的dirtyProperty,只要有任何一个子ViewModel变脏,就标记父ViewModel为脏;切换人员时清理旧监听,避免内存泄漏。

修正后的代码如下:

class PersonViewModel : ItemViewModel<Person>() {
    val name = bind(Person::name)
    val cars = SimpleListProperty<CarViewModel>()

    // 存储子ViewModel的脏状态监听器,切换人员时移除
    private val dirtyListeners = mutableListOf<InvalidationListener>()

    override fun onItemChange(oldItem: Person?, newItem: Person?) {
        super.onItemChange(oldItem, newItem)
        
        // 清理旧的监听器
        dirtyListeners.forEach { listener ->
            oldItem?.cars?.map { CarViewModel(it) }?.forEach { vm ->
                vm.dirtyProperty().removeListener(listener)
            }
        }
        dirtyListeners.clear()

        // 初始化新的CarViewModel列表
        val newCarViewModels = newItem?.cars?.map { CarViewModel(it) }?.observable() 
            ?: emptyList<CarViewModel>().observable()
        cars.set(newCarViewModels)

        // 给每个子ViewModel添加脏状态监听
        newCarViewModels.forEach { carVm ->
            val listener = InvalidationListener {
                // 只要有一个子ViewModel变脏,父ViewModel就标记为脏
                this.dirty = cars.value.any { it.isDirty }
            }
            carVm.dirtyProperty().addListener(listener)
            dirtyListeners.add(listener)
        }
    }

    override fun onCommit() {
        // 先提交所有子ViewModel的变更,确保Car数据更新
        cars.value.forEach { it.commit() }
        
        // 基于提交后的ViewModel数据创建新的Person实例
        val updatedCars = cars.value.map { it.item }
        val updatedPerson = item?.copy(
            name = name.value,
            cars = updatedCars
        )
        
        // 这里替换成你实际的存储逻辑
        updatedPerson?.let { /* 保存更新后的Person */ }
        
        // 调用父类方法重置脏状态
        super.onCommit()
    }
}

class CarViewModel(item: Car) : ItemViewModel<Car>(item) {
    // 注意你之前写错了,应该绑定Car::brand而不是Car::name
    val brand = bind(Car::brand)
    val model = bind(Car::model)
}

方案二:利用TornadoFX的Binding机制组合脏状态

如果你更喜欢声明式的方式,可以通过自定义dirtyProperty来组合父ViewModel自身的脏状态和所有子ViewModel的脏状态:

class PersonViewModel : ItemViewModel<Person>() {
    val name = bind(Person::name)
    // 绑定item的cars属性,自动转换为CarViewModel列表
    val cars = bind {
        itemProperty().map { person ->
            person?.cars?.map { CarViewModel(it) }?.observable() 
                ?: emptyList<CarViewModel>().observable()
        }
    }

    // 自定义脏状态绑定:只要name变脏,或者任何子ViewModel变脏,父ViewModel就变脏
    override val dirtyProperty = object : BooleanBinding() {
        init {
            // 初始绑定name和cars列表
            bind(name, cars)
            // 绑定当前所有子ViewModel的脏状态
            cars.value.forEach { bind(it.dirtyProperty()) }
            
            // 监听cars列表的变化,动态添加/移除子ViewModel的脏状态绑定
            cars.addListener { _, oldList, newList ->
                oldList.forEach { unbind(it.dirtyProperty()) }
                newList.forEach { bind(it.dirtyProperty()) }
                invalidate()
            }
        }

        override fun computeValue(): Boolean {
            return name.isDirty || cars.value.any { it.isDirty }
        }
    }

    override fun onCommit() {
        // 先提交所有子ViewModel
        cars.value.forEach { it.commit() }
        
        val updatedPerson = item?.copy(
            name = name.value,
            cars = cars.value.map { it.item }
        )
        
        // 执行存储逻辑
        updatedPerson?.let { /* 保存Person */ }
        
        super.onCommit()
    }
}

class CarViewModel(item: Car) : ItemViewModel<Car>(item) {
    val brand = bind(Car::brand)
    val model = bind(Car::model)
}

关键优化点提醒

  • 修正属性绑定笔误:你之前的CarViewModel里写了bind(Car::name),但Car类并没有name属性,应该是bind(Car::brand),这个一定要改,否则绑定会出错。
  • 内存泄漏防范:切换人员时一定要移除旧的监听器(方案一的dirtyListeners、方案二的unbind),否则旧的CarViewModel会被监听器引用,无法被GC回收。
  • 提交顺序:提交PersonViewModel前必须先提交所有CarViewModel,否则子ViewModel的变更不会同步到item,导致生成的Person还是旧的车辆数据。

内容的提问来源于stack exchange,提问作者Sebastian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 03:58:32