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
相关产品推荐
相关产品推荐

