Online Books:
java.net on MarkMail:
Search |
||
Writing a UI controller in JavaFXPosted by fabriziogiudici on June 6, 2009 at 4:28 PM PDT
While JavaFX is great for the UI (binding, declarative stuff, etc...) it's also a good candidate for writing controllers (in a MVC).
Of course I'm not saying I'd write a complete application (I mean, a back-end) in JavaFX - JavaFX is a DSL and it's specific for the presentation part. You'd have problems in forcing it to a broader scope than the one it has been designed for. But the specific models and controllers of a front-end are fine - and you can take advantage of binding there too. For this post, I'm focusing on a couple of classes of blueBill Mobile, in particular to the controller that manages all the logic behind the "Search species" screen; thus this post could help you in getting more confident with the language features of JavaFX, beyond the UI stuff. I'm also using a few real-case points to illustrate some tips and pitfalls of some typical JavaFX constructs. I'm embedding an updated screencast of the application (in the meantime I've updated it to be resolution independent as I hope to be able to run it on a HTC Diamond Touch, just ordered). The video requires QuickTime. The concept is that, while you type in the search box, the list gets filtered in function of the english or scientific name. Furthermore (this is a new feature just implemented), blueBill Mobile is able to perform some autocompletion when it makes sense. For instance, if you carefully look at the video when I type in, I'm only entering "a-r-d-a-c" to select "Ardea Cinerea"; or "p-i-e-<space>-a" for "Pied Avocet". blueBill Mobile autocompletes the rest becase in some cases there are no multiple options. It's an important feature for improving the user's experience with a mobile gear: you get the same stuff by typing less. As per the MVC pattern, it's important to encapsulate this logic in a separate controller, that could be reused by different views; furthermore, it could be easily unit tested (I've posted to DZone a tip on how to run JUnit tests for JavaFX with NetBeans, waiting for it to be approved and published). First, let's have a look to the model class representing a Taxon (in short, it models the information about a bird species):
package it.tidalwave.bluebillmfx.taxon.model;
import java.lang.Comparable;
public class Taxon extends Comparable
{
public-read protected var displayName : String;
public-read protected var scientificName : String;
public-read protected var id : String;
override function compareTo (other : Object)
{
return displayName.compareTo((other as Taxon).displayName);
}
override function toString()
{
return "{displayName} ({scientificName}) ({id})"
}
}
public function displayNameGetter (taxon : Taxon): String
{
return taxon.displayName;
}
public function scientificNameGetter (taxon : Taxon): String
{
return taxon.scientificName;
}
public def namePropertyGetters = [displayNameGetter, scientificNameGetter];
Functions and variables defined out of the class braces are equivalent to Java static stuff.I've omitted stuff from the real project that is not related to what we're discussing about. Basically, the model exposes three properties, two of which are the interesting ones: displayName and scientificName. We also define two functions to adress them, and we put those functions in the sequence namePropertyGetters (whoa, these are closures indeed!). Now let's have a look at TaxonSearchController:
This class exposes the following properties:
filter has got a trigger (on replace), that is code executed when the variable changes value. The trigger performs the filtering with the | operator of JavaFX: we can read the first line of the trigger as "filteredTaxons is assigned to the sequence of taxons for which the matches() function returns true". The second line calls the update() function that I'm describing below. For a number of reasons, this is not necessarily efficient, as filteredTaxons is always entirely scanned. There are a lot of ways to make the selection faster, but I'm not going to do a premature optimization until I see how it performs on a real phone. On a laptop, it's pretty fast with about 1.000 items.The matches() function performs an iteration over all the property get functions and checks whether the related property starts with the value of filter (case-insensitive). I'd say it's pretty straightforward. One of the advantages of creating a sequence of property getter functions is that we could easily add new matching criteria by just defining new functions: for instance, other localized names in different languages. The controller would use them in the search without needing any modification.The update() function computes the auto-completion hint. It takes the sequence of filteredTaxons, the getter of the property that was used for the current selection (more about this later) and calls commonLeadingSubstring(), which has just to find the common substring out of a sequence of string properties. It's not always possible to find a good autocompletion guess, so sometimes the suggestion is even shorter than the current filter, in which case we ignore it. Please don't under-estimate the importance of that assignment to a temporary variable (autoCompletedTry): since autoCompleted might be bound (that is, client code could automatically receive change notifications) we don't want to assign it a value that could be quickly invalidated (if the length() test fails). Just to understand the importance of this point - it's not just a matter of avoiding useless updates, it's about not having the application broken. In the real application, a TextBox is updated when autoCompleted changes and in turn it makes a further update to filter (see the code at the end of this post). If you directly assign autoCompleted to a shorter string than filter, you get stuck in the TextBox, as further key strokes are thrown away. Consider this sequence (from a real case): you've typed "cal", you type another "i", the TextBox temporarily shows "cali", then the autocompletion guess fails and returns "cal", and the string in the TextBox goes back to "cal": you're stuck! Binding indeed is powerful, but has a dark side and must be learned with care.As the last operation, the code checks whether we've got a single selected bird species. Probably you're still curious about the reason why the computation of autoCompleted can fail. After all we're progressively shrinking a list of items, thus if you have typed "cali", then all the filtered species should start with "cali", right? Well, it would be true if we were filtering on a single set of names; but we're performing the search at the same time on the two sets of names (english and scientific) and thus there could be conflicts. Consider the following pairs selected by the "cali" filter (english, scientific): ("Calandra Lark", "Melanocorypha calandra"), ("Dunlin", "Calidris alpina"), ("California Quail", "Callipepla californica").Another interesting point is findMatchingPropertyGetter(). It must guess whether the current filter is working on the english or the scientific name, and return the related property getter. Basically, the controller has already got this information, inside the matches() method (look at the repeated code), but we threw it away. One could think about having matches() returning more than a single boolean, but it's not possible because it's used by the | operator while filtering the sequence: that operator wants a boolean. Probably we could just assign a member variable for recalling the information later (this would make the class not thread-safe, but after all this is only executed in the EDT) - but at the moment I think the code is more readable as it is now. To complete this post, here are the two last missing functions: protected function commonLeadingSubstring (taxons: Taxon[], propertyGetter: function (:Taxon): String): StringThe logic is pretty straightforward. The common leading string search is decomposed to pairs of adjacent strings; and the search on a single pair is implemented by recursion. To conclude the post, this is how the view class binds to the controller (I'm omitting all the UI specific parts): You have to load taxons with all the available species; then the thing just works, with ListBox being automatically updated with the filtered species, and the TextBox bi-directionally bound (bind ... with inverse) with filter. The bi-directionality is needed because one direction is when you type in the search box, thus commanding to the controller a new selection, the other direction is when the auto-completion is updated. »
Comments
Comments are listed in date ascending order (oldest first)
Submitted by mikeazzi on Mon, 2009-06-08 06:57.
"Of course I'm not saying I'd write a complete application (I mean, a back-end) in JavaFX - JavaFX is a DSL and it's specific for the presentation part. You'd have problems in forcing it to a broader scope than the one it has been designed for."
I have been thinking about exactly this particular point lately. Is JavaFX really just a DSL for the presentation? Is there anything inherent about JavaFX that would make it unsuitable for the back-end? Wouldn't you think that back-end business logic could also benefit from the highly productive, and expressive aspects of the language such as, its declarative syntax, and binding? And so, what would it take for JavaFX Script to make it suitable for modeling back-end business processes? In other words why can't we have a JavaFX version of the Rails framework? FXRails, or something.
Submitted by fabriziogiudici on Mon, 2009-06-08 08:50.
There are a number of features that, as you say, might be useful in the back end. The problem is about details: e.g. AFAIK binding is bound (pardon the pun) to the EDT thread, thus killing scalability on a server. Furthermore, sequences are powerful for simple uses, but they don't give you the control of the Collection frameworks (and thinkg e.g. of the concurrent features of collections). There are no structures such as Map, Set. You can't use annotation-based stuff for persistence, transactions, security. There are no generics (I left this at last since I know that many people don't like them, but I do).
Of course you can use TreeSet, CopyOnWriteList and HashMap, but they would miss all the good things we're demanding for (binding), thus with a poor integration with the rest.
For all these things, Java is still the best option. But this discussion makes clear that there's a lot of cross-contamination ideas ahead.
|
||
|
|