Monday Aug 03, 2009

Reflection in JavaFX

Last week I was trying to learn how to use JavaFX's reflection APIs - I was working on a test framework and had to use reflection to run a test. My requirement was quite simple: I had to instantiate a JavaFX class using reflection and had to call one of its functions. I didn't realize its difficult until I went through JavaFX's reflection api. I was expecting an api set similar to Java's reflection api, but I was wrong. I couldn't find much documentation on using it either. The sole blog I could find on reflection, didn't talk about what I was looking for. So, I finally had to experiment with reflection API to figure out how it works. I'm just jotting down what I learned, may be it could help someone taking the same route.

Here is a sample JavaFX class.


    public class ClassA {
        public var count: Integer;
        init {
            println("ClassA init");
        }
        public function methodA(): Void {
            println("methodA called");
        }
        public function methodB(str: String, num: Integer): Integer {
            println("methodB called {str} {num}");
            return 20;
        }
        public function methodC(): Number[] {
            println("methodC");
            return [10.0, 100.6, 1000.8];
        }
        public function methodD(num: Integer, seq: String[]): Void {
            println("methodD called {num} {sizeof seq}");
        }
    }
  

The classes for JavaFX reflection api is located in javafx.reflect package. For reflective operations you need an instance of 'FXContext' to operate on. In order to access objects and types in the current JVM, you have 'FXLocal.Context', which is an implementation of 'FXContext' using Java reflection (as per the API doc). I believe FXLocal.Context is what we need to use.


    var context = FXLocal.Context.getInstance();
  

Below code is self explanatory. I've given some comments on what they do, or rather my understanding of what they do :-)


    //This returns an instance of FXLocal.ClassType, a subclass of FXClassType.
    var cls = context.findClass("ClassA");

    //Creates an instance of 'ClassA'
    var objValue = cls.newInstance();
  

The above 'newInstance' call creates an instance of 'ClassA'. But the same can be done in two steps as below


    //A raw uninitialized object is returned by 'allocate()' function.
    var objValue = cls.allocate();

    //'initialize()' function of FXObjectValue makes the 'init' of the class to be called.
    objValue = objValue.initialize();
  

This is beneficial because you may need to initialize any variables of the class before it's 'init' is called, as follows:


    var objValue = cls.allocate();
    var num: Integer = 10;
    objValue.initVar("count", context.mirrorOf(num));
    objValue = objValue.initialize();
  

Note that the above 'objValue' is not a JavaFX 'object', but an instance of 'FXLocal.ObjectValue'. You can get an object from 'objValue', cast it to a class you want. That is covered in Jim's blog.

Now, we can look at how to call functions of this class⁞. Let us see how to call 'methodA()' which neither has any arguments nor any return value. 'getFunction' method of FXClassType accepts method name and a sequence of argument types of the function. Since 'methodA' is a no args function, an empty sequence is passed.


    var fnA = cls.getFunction("methodA", []);
    fnA.invoke(objValue, []);
  
Below is how the function 'methodB' is called. The function accepts two arguments, a String and an Integer. FXContext has methods which return the 'FXType' of different data types.


    var argString = "argument";
    var number = 20;
    var fnB = cl.getFunction("methodB", [context.getStringType(), context.getIntegerType()]);
    var retVal = fnB.invoke(ni, [context.mirrorOf(argString), context.mirrorOf(number)]);
  

It can be seen that methodB returns an Integer. It is fairly simple to retrieve the return value from FXValue.


    var retInt = Integer.parseInt(retVal.getValueString());
  

We also have a function 'methodC' which returns a Number sequence. This is how the return value is obtained from the FXValue


    var fnC = cl.getFunction("methodC", []);
    var fnCVal = fnC.invoke(ni, []);
    if (fnCVal.getItemCount() > 0) {
        for (i in [0..fnCVal.getItemCount() - 1) {
            var f = Float.parseFloat(fnCVal.getItem(i).getValueString());
            println("Value at index {i} is {f}");
        }
    }
  

I couldn't get to call a function which accepts a sequence argument. Somehow, 'getFunction' throws a ClassCastException for me. I tried both these ways, but both threw CCE for me.


    var inputSeq = ["ABC", "DEF", "GHI"];
    var fnD = cl.getFunction("methodD", [context.getIntegerType(), context.getStringType().getSequenceType()]);
    
    //or
    
    var valSeq = context.makeSequenceValue(for (i in inputSeq) context.mirrorOf(i), sizeof inputSeq, context.getStringType());
    var fnD = cl.getFunction("methodD", [context.getIntegerType(), valSeq.getType()]);
  

I'm still trying to figure out the right way to do it - or it could be a bug!

Update: This in fact turned out to be a JavaFX bug: see JFXC-3381. For now, you can loop through all the functions returned by 'getFunctions' method to find the method you are looking for, though I'm not sure if calling that method will work without issues.

Comments:

Have you looked at the JFXtras project?

If not, that might be helpful. They are also very helpful in the dev/user groups.

http://code.google.com/p/jfxtras/
http://steveonjava.com/2009/08/02/jfxtras-test-and-fest-unite/

/Pär

Posted by Pär Dahlberg on August 04, 2009 at 06:42 PM IST #

Post a Comment:
  • HTML Syntax: NOT allowed