Open DeMol-EE opened 5 years ago
I think the best solution is to actually replace the TableView with a new one like you're already doing, but you should also be able to remove all the columns and add new ones. However, due to the TableView is typed, you'd probably have to have a common super class for your data set, and I don't think that's worth it just for this. Honestly I don't think your approach is bad, given the nature of what you're doing.
Thanks for the reply @edvin. Though everything is working, it feels like I'm breaking the intended singleton nature of Views.
I suppose to get what I want, there would have to be some UI-templating mechanism like Thymeleaf and such, and angular, react, vue... but that's quite fundamentally different from how JavaFX works.
I've achieved something that imitates that behavior by creating a model which keeps a map of nodes wrapped in observables, and by using borderpanes in my code that have their centerproperty bound to these observables. I can get everything to work the way I want using plain JavaFX code (purely due to experience) but the code looks messy and I would really like to tap into the power of TornadoFX, I'm just struggling to achieve some behaviorisms. Would you mind taking a few minutes of your time to propose a solution for a design challenge I'm facing?
I see :) I think to be successful with JavaFX you have to accept certain design decisions and stop fighting it to be productive. JavaFX is made in a fashion such that it makes more sense to bind state to properties of the controls, instead of switching out the complete control when state changes, though it is possible, and we do have some features to help with that (bindChildren etc). Your TableView requirements do pose some issues, as the control isn't meant to support different datasets in it's life time. Sure, I'd be happy to look at your design challenge, bring it on!
Okay, awesome!
Edit: the post turned out longer than I had anticipated... I hope you don't mind.
Short backstory: every year me and four colleagues have to grade hundreds of exams (we work at a university). For reasons, the grades are stored in a database, but there is no frontend so inserting grades is really a hassle. I figured I could write an application to make this easier. Usability is really critical here, and the way I envisioned it was as follows:
With this workflow, an examinator can enter grades with a minimal amount of keystrokes and without using the mouse. Most importantly, you don't need to navigate the cursor around a text statement to select and edit particular parts, you don't need to enter strange magic numbers representing question ids and you don't need to submit your query multiple times with more extensive search terms until the database finally accepts your input.
Initially, I wanted to use a tableview. Despite its perks, I found it difficult to achieve the desired behavior (vertical column headers, tab to a particular cell which is intantly put into edit mode, ...). Instead, I created a custom solution with a gridpane and a filteredlist. Based on the exam, a fixed header row is created and the application listens to changes in the filteredlist to dynamically rebuild the data rows part of the gridpane each time the search term is modified. The gridpane ensures a nice tabular layout and gives me full control over how the rows are rendered, making it possible to achieve the exact behavior I want.
Maybe it's stupid, but I wanted it to work so that a user can select columns after applying a filter as well, so I also listen to changes in the column header checkboxes, not just the search term. Though this flow might only happen once in the entire application, it feels like a bug when toggling columns doesn't change the form row unless you explicitly trigger a re-render by changing the filter.
To achieve this, my current code does the following: when the filters result in exactly 1 row, a model is instantiated to represent the form. The model has access to properties representing the column's selection state and builds a map of column indices to observable nodes. The model uses bindings to dynamically update the node from label to textfield (and back) depending on the selectedProperty of the column matching the question with Bindings#when#otherwise. The row in the grid is built using borderpanes whose centerproperty is bound to the observable of the corresponding column index.
It's probably something simple, but the way it's implemented now does not allow me to use the tornadofx builder functions when constructing the dynamically bound textfields, which means I am missing out on the super useful builders which allow me to easily add validation and a property to bind the textfield to. As mentioned, I can get around this by coding plain JavaFX, but I really think I'm just missing something. If I cut out the model object and inline the logic in the view code, I can use the builders, but that makes the code look even messier!
So what do you think? About the app in general, about the decisions I've made... I would love to hear your comments!
It's a little hard to grasp exactly how this looks and how you want it to function, so my comments are general.
I'd use Views or Fragments for the parts that change substantially and swap them out as necessary. I'd then use Form
extensively, and add textfields which would have their enabled
state bound to reflect editing possibility. You could also add css to render readonly textfields as labels, but this would then be very easily encapsulated, something like:
field("Some field") {
textfield(model.property) {
enabledWhen(user.hasWriteAccess(model.property)
}
}
This should be pretty easy to both write and maintain.
I know this is all too general, maybe if you could show me the ui I could give some more input :)
I might have gone a little overboard with initial information, hehe. By now I've had some time to grow into the framework and I think I can explain it on a higher, more abstract level:
Usually, a user will stay in the exam editor for the entire session, but nonetheless it should be possible for them to return to the overview and select a different exam.
Here are some screenshots showcasing what I have right now.
Intial view:
Note I've programmatically removed the navigation buttons, for which I want to mention that the suggested way according to the guide (workspace.backButton.removeFromParent()
) doesn't work, instead I have to use root.header.items -= backButton
in the init
block of my initial workspace class.
Exam editor:
I've tried a few things, but currently I'm using the concept of a single scope and two workspaces. Double clicking an item in the list sets an exam in an injected ExamModel (a singleton of ItemViewModel in the default scope) and replaces the default workspace with the exam editor workspace (using replaceWith
). Though this works, I basically have to have all my views in the exam editor workspace listen to changes to the exam item in the ExamModel instance so that if it changes, they completely rebuild their UI. Additionally, I set the default workspace to the exam editor one after loading its view, but this particular part feels like I'm forcing workspaces to work in a particular way that they actually aren't intended for.
Alternatively, I tried creating a new scope each time and setting the exam model in that scope. The upside is not insignificant, because this approach allows me to drastically simplify my view code as it is safe to assume that the exam instance does not change in the scope of the exam editor after it's been set in its creation. The only thing holding me back from committing to an approach with scopes is that workspaces are not deallocated when the user goes back to the initial view, as they extend View, and thus each time the user re-opens the exam editor, memory is leaked (I've confirmed with a profiler like VisualVM that in fact even multiple workspace instances for the same exam stay alive in memory - which is completely logical from the way it works, but this is obviously not what I want). Sounds like what I'm after is something like a Workspace which extends Fragment, or am I getting it wrong? Or am I missing a really obvious delete
/unregister
method on workspaces here?
To be completely honest, me and some colleagues already use the app in my day job and as such I've shifted my focus from optimal code structure to functionality and so stopped investigating scopes once I got a working flow - but I'm not super satisfied with it.
I'm trying to build an app to manage different tabular data sets using tornadofx. I've read most of the guide (the fundamentals and some of the advanced features) and parts of rxjava/kotlinfx by @thomasnield and I am very impressed, though I'm facing a design issue which I'm not certain how to best address.
So far, I have the following:
The problem I'm facing is that, when the user goes back and selects a different data set, i want the table view to be rebuilt. More precisely, I want to change the columns (every data set has different columns). Given how the underlying JavaFX TableView is engineered, it's easy to bind the rows, but there seems to be no pretty way to bind the structure of the table to the model.
My current solution is to add an init block in the DataSetView, in which I add a change listener to the model's itemproperty to manually rebuild the tableview and use replaceChildren on the root, but I was wondering if there's a better way to do this.