Skip to main content

Writing a UI controller in JavaFX

Posted 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--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:

package it.tidalwave.bluebillmfx.taxon.controller;

import it.tidalwave.bluebillmfx.taxon.model.Taxon;

public class TaxonSearchController
  {
    public var selectedTaxon = bind if (selectedTaxonIndex < 0) then null else filteredTaxons[selectedTaxonIndex];

    public var selectedTaxonIndex : Integer = -1;
     
    public var taxons: Taxon[];

    public var filter = "" on replace
      {
        filteredTaxons = taxons[taxon | matches(taxon, filter)];
        update();
      }

    public-read var autoCompleted = "";

    public var filteredTaxons: Taxon[];

    protected function matches (taxon : Taxon, string: String) : Boolean
      {
        if (string == "")
          {
            return true;
          }

        for (propertyGetter in Taxon.namePropertyGetters)
          {
            if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase()))
              {
                return true;
              }
          }

        return false;
      }

    protected function update(): Void
      {
        def autoCompletedTry = commonLeadingSubstring(filteredTaxons, findMatchingPropertyGetter());
        //
        // Sometimes it can't find a better auto-completion than the current filter, since it searches the displayName
        // and the scientificName at the same time. In this case, we just ignore the new value.
        //
        if (autoCompletedTry.length() > filter.length())
          {
            autoCompleted = autoCompletedTry;
          }

        selectedTaxonIndex = if (sizeof filteredTaxons == 1) then 0 else -1;
        println("selectedTaxonIndex: {selectedTaxonIndex}")
      }

    protected function findMatchingPropertyGetter(): function (:Taxon): String
      {
        for (taxon in filteredTaxons)
          {
            for (propertyGetter in Taxon.namePropertyGetters)
              {
                if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase()))
                  {
                    return propertyGetter;
                  }
              }
          }

        return null;
      }

    // some stuff later
 }

This class exposes the following properties:

  • taxons: you have to populate it with the full list of available bird species;
  • filter: the string containing the text you're entering in the search box;
  • filteredTaxons: the species filtered by the filter string;
  • autoCompleted: the auto-completion string guessed by the controller ("" if no guess is available);
  • selectedTaxon: if the filter drilled down to a single species, it is assigned to this variable;
  • selectedTaxonIndex: the index of selectedTaxon, -1 if not available.

The latest four properties are intended to be bound by client code, that will receive a notification of changes.

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): String
      {
        if (sizeof taxons == 0)
          {
            return "";
          }

        if (sizeof taxons == 1)
          {
            return propertyGetter(taxons[0]);
          }

        var common = propertyGetter(taxons[0]);

        for (other in taxons[1..])
          {
            common = commonLeadingSubstring(common, propertyGetter(other));

            if (common == "")
              {
                break; // don't waste time in further iterations, "" it's for sure the final result
              }
          }

        return root;
      }

    function commonLeadingSubstring (string1 : String, string2 : String): String
      {
        return if (string1.length() > string2.length())
          {
            commonLeadingSubstring(string2, string1);
          }
        else if (string1 == "")
          {
            "";
          }
        else if (string2.startsWith(string1))
          {
            string1;
          }
        else
          {
            commonLeadingSubstring(string1.substring(0, string1.length() - 1), string2);
          }
      }
The 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):

package it.tidalwave.bluebillmfx.taxon.view;

public class TaxonSearchScreen
  {
    public var taxons : Taxon[];

    var filter = "";

    public-read def controller = TaxonSearchController
      {
        taxons: bind taxons
        filter: bind filter
      }

    def autoCompleted = bind controller.autoCompleted on replace
      {
        if (autoCompleted != "")
          {
            filter = autoCompleted;
          }
      }

    def list = ListBox
      {
        items:                 bind controller.filteredTaxons
      };

    def searchBox = TextBox
      {
        text:       bind filter with inverse
      };
  }
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

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.

"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.