@Retention(value=SOURCE) @Target(value=TYPE) public @interface Model
Model.className()
. The generated class contains
getters and setters for properties defined via Model.properties()
and
getters for other, derived properties defined by annotating methods
of this class by ComputedProperty
. Each property
can be of primitive type, an enum type
or (in order to create
nested JSON structure)
of another class generated by @Model
annotation. Each property
can either represent a single value or be an array of its values.
The generated class
's toString
method
converts the state of the object into
JSON format. One can
use Models.parse(net.java.html.BrwsrCtx, java.lang.Class, java.io.InputStream)
method to read the JSON text stored in a file or other stream back into the Java model.
One can also use @OnReceive
annotation to read the model
asynchronously from a URL
.
An example where one defines class Person
with four
properties (firstName
, lastName
, array of addresses
and
fullName
) follows:
The generated model class has a default constructor, and also quick instantiation constructor that accepts all non-array properties (in the order used in the@Model
(className="Person", properties={@Property
(name = "firstName", type=String.class),@Property
(name = "lastName", type=String.class)@Property
(name = "addresses", type=Address.class, array = true) }) static class PersonModel {@ComputedProperty
static String fullName(String firstName, String lastName) { return firstName + " " + lastName; }@ComputedProperty
static String mainAddress(List<Address>
addresses) { for (Address a : addresses) { return a.getStreet() + " " + a.getTown(); } return "No address"; }@Model
(className="Address", properties={@Property
(name = "street", type=String.class),@Property
(name = "town", type=String.class) }) static class AddressModel { } }
Model.properties()
attribute) and vararg list
for the first array property (if any). One can thus use following code
to create an instance of the Person and Address classes:
Person p = new Person("Jaroslav", "Tulach", new Address("Markoušovice", "Úpice"), new Address("V Parku", "Praha") ); // p.toString() then returns equivalent of following JSON object { "firstName" : "Jaroslav", "lastName" : "Tulach", "addresses" : [ { "street" : "Markoušovice", "town" : "Úpice" }, { "street" : "V Parku", "town" : "Praha" }, ] }
In case you are using Knockout technology
for Java then you can associate such model object with surrounding HTML page by
calling: p.applyBindings();
(in case you specify Model.targetId()
.
The page can then use regular
Knockout bindings to reference your
model and create dynamic connection between your model in Java and
live DOM structure in the browser:
Name: <span data-bind="text: fullName"> <div data-bind="foreach: addresses"> Lives in <span data-bind="text: town"/> </div>
model class
(with appropriate
Knockout observable properties)
by calling Models.toRaw(p)
. For
example here is a way to obtain the value of fullName
property
(inefficient as it switches between Java and JavaScript back and forth,
but functional and instructive) via a JavaScript call:
The above shows how to read a value from Knockout observable. There is a way to change the value too: One can pass a parameter to the property-function and then it acts like a setter (of course not in the case of read only@JavaScriptBody
(args = "raw", javacall = true, body = "return raw.fullName();" // yes, the Knockout property is a function ) static native String jsFullName(Object raw); // and later Person p = ...; String fullName = jsFullName(Models.toRaw(p)
);
fullName
property,
but for firstName
or lastName
the setter is
available). Everything mentioned in this paragraph applies only when
Knockout technology is active
other technologies may behave differently.
model class
instance
by copy and convert
the the whole object
into plain
JSON. Just
print it as a string and parse it in JavaScript:
@JavaScriptBody
(args = { "txt" }, body =
"return JSON.parse(txt);"
)
private static native Object parseJSON(String txt);
public static Object toPlainJSON(Object model) {
return parseJSON(model.toString());
}
The newly returned instance is a one time copy of the original model and is no longer
connected to it. The copy based behavior is independent on any
particular technology and should work
in Knockout as well as other
technology implementations.
Model
annotation or try
a little math test.Modifier and Type | Required Element and Description |
---|---|
String |
className
Name of the model class.
|
Property[] |
properties
List of properties in the model.
|
public abstract String className
public abstract Property[] properties
public abstract String targetId
applyBindings
method
in the model class is going to be generated which later calls
Models.applyBindings(java.lang.Object, java.lang.String)
with appropriate targetId
. If the targetId
is specified as empty string, null
value is passed
to Models.applyBindings(java.lang.Object, java.lang.String)
method.
If the targetId
is not specified at all, no public
applyBindings
method is generated at all (a change compared
to previous versions of this API).applyBindings()
method topublic abstract String builder
Model.properties()
will get a builder like
setter (takes value of the property and returns this
so invocations can be chained). When this attribute is specified,
the non-default constructor isn't generated at all.
Specifying builder="assign"
and having properties
name
and
age
will generate method:
public MyModel assignName(String name) { ... } public MyModel assignAge(int age) { ... }These methods can then be chained as
MyModel m = new MyModel().assignName("name").assignAge(3);The
builder
attribute can be set to empty string ""
-
then it is possible that some property names clash with Java keywords.
It's responsibility of the user to specify valid builder prefix,
so the generated methods are compilable.property
names when
generating their builder methodspublic abstract boolean instance
@Model annotation
needs to
keep non-public, or non-JSON like state. One can achieve that by
specifying instance=true
when using the annotation. Then
the generated class gets a pointer to the instance of the annotated
class (there needs to be default constructor) and all the @ModelOperation
,
@Function
, @OnPropertyChange
and @OnReceive
methods may be non-static. The
instance of the implementation class isn't accessible directly, just
through calls to above defined (non-static) methods. Example:
@Model
(className="Data", instance=true, properties={@Property
(name="message", type=String.class) }) final class DataPrivate { private int count;@ModelOperation
void increment(Data model) { count++; }@ModelOperation
void hello(Data model) { model.setMessage("Hello " + count + " times!"); } } Data data = new Data(); data.increment(); data.increment(); data.increment(); data.hello(); assert data.getMessage().equals("Hello 3 times!");
The methods annotated by ComputedProperty
need to remain static, as
they are supposed to be pure functions (e.g. depend only on their parameters)
and shouldn't use any internal state.
How do I initialize private values?
The implementation class (the one annotated by @Model
annotation)
needs to have accessible default constructor. That constructor is used to
create the instance. Obviously such constructor does not have
any parameters, so no initialization is possible.
Later one can, however, call any @ModelOperation
method and pass in additional configuration parameters. In the above
example it should be possible add
@ModelOperation
void init(Data model, int count) {
this.count = count;
}
and then one can initialize the model using the init
as:
Data data = new Data(); data.init(2); data.increment(); data.hello(); assert data.getMessage().equals("Hello 3 times!");
Why there has to be default constructor? Because instances of
classes generated by @Model annotation
may be constructed
by the system as
wrappers around existing JavaScript objects
- then
there is nobody to provide additional parameters at construction time.
How do I read private values?
The methods annotated by ModelOperation
must return void
(as they can run asynchronously) and as such they aren't suitable for
returning values back to the caller. In case something like that is
needed, one can use the approach of the hello
method - e.g.
set value of some property
that has a getter:
data.hello(); assert data.getMessage().equals("Hello 3 times!");
Or one can use actor-like callbacks. Define callback interface and
use it in a @ModelOperation
method:
public interface ReadCount {
public void notifyCount(int count);
}
@ModelOperation
void readCount(Data model, ReadCount callback) {
callback.readCount(this.count);
}
Data data = new Data();
data.init(2);
data.increment();
data.readCount(count -> System.out.println("count should be 3: " + count));
The provided lambda-function callback may be invoked immediately
or asynchronously as documentation for ModelOperation
annotation describes.
true
if the model class should keep pointer to
instance of the implementation classCopyright © 2018 NetBeans. All rights reserved.