如何正确扩展现有的 MVVM UI 组件?
How to correctly extend an existing MVVM UI component?
在我使用 TornadoFX 的 Kotlin 桌面应用程序中,我创建了一个 AudioCard
布局(VBox
的子class),其中有一些标签和基本的音频播放器控件。 AudioCard
有一个 AudioCardViewModel
用于处理来自 UI 的事件,还有一个 AudioCardModel
用于保存标题、副标题、音频文件路径等信息。显示了一个简化版本下面。
data class AudioCardModel(
var title: String,
var audioFile: File
)
class AudioCardViewModel(title: String, audioFile: File) {
val model = AudioCardModel(title, audioFile)
var titleProperty = SimpleStringProperty(model.title)
fun playButtonPressed() {
// play the audio file from the model
}
}
class AudioCard(title: String, audioFile: File) : VBox() {
val viewModel = AudioCardViewModel(title, audioFile)
init {
// create the UI
label(title) {
bind(viewModel.titleProperty)
}
button("Play") {
viewModel.playButtonPressed()
}
}
}
到目前为止,我一直试图让代码尽可能通用,让我自己或其他人在未来需要播放音频的应用程序中重用这个 UI 组件。但是,对于我当前的应用程序,拥有此 UI 组件的更专业版本是最有意义的,它直接从我的数据模型 class 初始化自身并可以扩展一些操作。我试过这样的事情(前一个代码块中的必填字段和 classes 已切换为 open
):
data class CustomAudioCardModel(
var customData: CustomData
)
class CustomAudioCardViewModel(customData: CustomData)
: AudioCardViewModel(customData.name, customData.file) {
val model = CustomAudioCardModel(customData)
override fun playButtonPressed() {
super.playButtonPressed()
// do secondary things only needed by CustomAudioCardViewModel
}
}
class CustomAudioCard(customData: CustomData): AudioCard(customData.name, customData.file) {
override val viewModel = CustomAudioCardViewModel(customData)
}
不幸的是,这并不是那么简单。通过覆盖 CustomAudioCard
中的 viewModel
,viewModel
属性 不再是最终的,当 AudioCard
superclass 的初始化函数时导致 NullPointerException尝试在 child class 初始化视图模型之前使用视图模型设置标题标签。
我怀疑可能有一种方法可以通过定义一个 AudioCardViewModel
接口 and/or 使用 Kotlin 的 by
关键字委托能力来解决这个问题,但我的印象是MVVM 不需要定义接口(如在 MVP 中)。
总结:扩展现有 MVVM 控件的正确方法是什么,特别是在 Kotlin TornadoFX 库的上下文中?
这是我从 Paul Stovell. Instead of creating the view model within the view (Option 1 in Stovell's article), I switched to injecting the view model into the view (Option 2). I also refactored for better MVVM adherence with help from the TornadoFX documentation and this answer regarding where business logic should go 得到的解决方案。我的 AudioCard 代码现在如下所示:
open class AudioCardModel(title: String, audioFile: File) {
var title: String by property(title)
val titleProperty = getProperty(AudioCardModel::title)
var audioFile: File by property(audioFile)
val audioFileProperty = getProperty(AudioCardModel::audioFile)
open fun play() {
// play the audio file
}
}
open class AudioCardViewModel(private val model: AudioCardModel) {
var titleProperty = bind { model.titleProperty }
fun playButtonPressed() {
model.play()
}
}
open class AudioCard(private val viewModel: AudioCardViewModel) : VBox() {
init {
// create the UI
label(viewModel.titleProperty.get()) {
bind(viewModel.titleProperty)
}
button("Play") {
viewModel.playButtonPressed()
}
}
}
扩展视图现在看起来像:
class CustomAudioCardModel(
var customData: CustomData
) : AudioCardModel(customData.name, customData.file) {
var didPlay by property(false)
val didPlayProperty = getProperty(CustomAudioCardModel::didPlay)
override fun play() {
super.play()
// do extra business logic
didPlay = true
}
}
class CustomAudioCardViewModel(
private val model: CustomAudioCardModel
) : AudioCardViewModel(model) {
val didPlayProperty = bind { model.didPlayProperty }
}
class CustomAudioCard(
private val viewModel: CustomAudioCardViewModel
) : AudioCard(customViewModel) {
init {
model.didPlayProperty.onChange { newValue ->
// change UI when audio has been played
}
}
}
我看到了一些清理它的方法,尤其是关于模型,但这个选项似乎在我的场景中很有效。
在我使用 TornadoFX 的 Kotlin 桌面应用程序中,我创建了一个 AudioCard
布局(VBox
的子class),其中有一些标签和基本的音频播放器控件。 AudioCard
有一个 AudioCardViewModel
用于处理来自 UI 的事件,还有一个 AudioCardModel
用于保存标题、副标题、音频文件路径等信息。显示了一个简化版本下面。
data class AudioCardModel(
var title: String,
var audioFile: File
)
class AudioCardViewModel(title: String, audioFile: File) {
val model = AudioCardModel(title, audioFile)
var titleProperty = SimpleStringProperty(model.title)
fun playButtonPressed() {
// play the audio file from the model
}
}
class AudioCard(title: String, audioFile: File) : VBox() {
val viewModel = AudioCardViewModel(title, audioFile)
init {
// create the UI
label(title) {
bind(viewModel.titleProperty)
}
button("Play") {
viewModel.playButtonPressed()
}
}
}
到目前为止,我一直试图让代码尽可能通用,让我自己或其他人在未来需要播放音频的应用程序中重用这个 UI 组件。但是,对于我当前的应用程序,拥有此 UI 组件的更专业版本是最有意义的,它直接从我的数据模型 class 初始化自身并可以扩展一些操作。我试过这样的事情(前一个代码块中的必填字段和 classes 已切换为 open
):
data class CustomAudioCardModel(
var customData: CustomData
)
class CustomAudioCardViewModel(customData: CustomData)
: AudioCardViewModel(customData.name, customData.file) {
val model = CustomAudioCardModel(customData)
override fun playButtonPressed() {
super.playButtonPressed()
// do secondary things only needed by CustomAudioCardViewModel
}
}
class CustomAudioCard(customData: CustomData): AudioCard(customData.name, customData.file) {
override val viewModel = CustomAudioCardViewModel(customData)
}
不幸的是,这并不是那么简单。通过覆盖 CustomAudioCard
中的 viewModel
,viewModel
属性 不再是最终的,当 AudioCard
superclass 的初始化函数时导致 NullPointerException尝试在 child class 初始化视图模型之前使用视图模型设置标题标签。
我怀疑可能有一种方法可以通过定义一个 AudioCardViewModel
接口 and/or 使用 Kotlin 的 by
关键字委托能力来解决这个问题,但我的印象是MVVM 不需要定义接口(如在 MVP 中)。
总结:扩展现有 MVVM 控件的正确方法是什么,特别是在 Kotlin TornadoFX 库的上下文中?
这是我从 Paul Stovell. Instead of creating the view model within the view (Option 1 in Stovell's article), I switched to injecting the view model into the view (Option 2). I also refactored for better MVVM adherence with help from the TornadoFX documentation and this answer regarding where business logic should go 得到的解决方案。我的 AudioCard 代码现在如下所示:
open class AudioCardModel(title: String, audioFile: File) {
var title: String by property(title)
val titleProperty = getProperty(AudioCardModel::title)
var audioFile: File by property(audioFile)
val audioFileProperty = getProperty(AudioCardModel::audioFile)
open fun play() {
// play the audio file
}
}
open class AudioCardViewModel(private val model: AudioCardModel) {
var titleProperty = bind { model.titleProperty }
fun playButtonPressed() {
model.play()
}
}
open class AudioCard(private val viewModel: AudioCardViewModel) : VBox() {
init {
// create the UI
label(viewModel.titleProperty.get()) {
bind(viewModel.titleProperty)
}
button("Play") {
viewModel.playButtonPressed()
}
}
}
扩展视图现在看起来像:
class CustomAudioCardModel(
var customData: CustomData
) : AudioCardModel(customData.name, customData.file) {
var didPlay by property(false)
val didPlayProperty = getProperty(CustomAudioCardModel::didPlay)
override fun play() {
super.play()
// do extra business logic
didPlay = true
}
}
class CustomAudioCardViewModel(
private val model: CustomAudioCardModel
) : AudioCardViewModel(model) {
val didPlayProperty = bind { model.didPlayProperty }
}
class CustomAudioCard(
private val viewModel: CustomAudioCardViewModel
) : AudioCard(customViewModel) {
init {
model.didPlayProperty.onChange { newValue ->
// change UI when audio has been played
}
}
}
我看到了一些清理它的方法,尤其是关于模型,但这个选项似乎在我的场景中很有效。