As we get closer to a beta release of Seam Remoting 3.0, I'd like to talk a bit about what I think is one of the more exciting new features - the Model API. While chatting with Max in Antwerp last year, we were discussing the challenges associated with manipulating persistent objects remotely via AJAX. Dealing with lazy-loaded associations, detached entities, how to apply updates, etc are all issues faced when developing an AJAX-based user interface backed by an RPC-style API.
So it got me thinking. A view framework like JSF has an advantage in this area because it is component-centric, as opposed to RPC-centric. Each JSF control is typically bound to a component property somewhere in your server-side business model. When you invoke an action from JSF (e.g. submitting a form with a command button) any values that the user has entered into the form are (after validation) applied to the backing model (i.e. the update model phase), and then the action is invoked. Couple this with the extended managed persistence context provided by Seam and you have a powerful combination.
The Model API aims to provide a similar component-centric way of working with your server-side model, but using AJAX. To understand how it works, let's get right into the fun stuff and look at some code.
First of all we start by creating a model object (by the way this is client-side JavaScript):
var model = new Seam.Model();
So far so good. The model object is our gateway to the Model API. It provides methods for defining bean properties, fetching a model, expanding a model (I'll explain what this means later) and applying updates to a model. So the first step is to define the bean properties that we wish to work with. Let's say that we have a PersonAction bean that allows us to create new people, or edit existing people (this is server-side Java now):
public @ConversationScoped class PersonAction implements Serializable { @PersistenceContext EntityManager entityManager; @Inject Conversation conversation; private Person person; @WebRemote public void createPerson() { conversation.begin(); person = new Person(); person.setAddresses(new ArrayList<Address>()); } @WebRemote public void editPerson(Integer personId) { conversation.begin(); person = entityManager.find(Person.class, personId); } @WebRemote public void savePerson() throws Exception { if (person.getPersonId() == null) { entityManager.persist(person); } else { person = entityManager.merge(person); } conversation.end(); } public Person getPerson() { return person; } }
As you can see, besides the @WebRemote annotations there's nothing special about this class. It is a @ConversationScoped bean because we need to manipulate its state across multiple requests. The Person entity is quite simple too - it contains a few basic properties and a lazy-loaded collection of Addresses:
@Entity public class Person implements Serializable { private Integer personId; private String firstName; private String lastName; private Date dateOfBirth; private Collection<Address> addresses; @Id @GeneratedValue public Integer getPersonId() { return personId; } public void setPersonId(Integer personId) { this.personId = personId; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Date getDateOfBirth() { return dateOfBirth; } public void setDateOfBirth(Date dateOfBirth) { this.dateOfBirth = dateOfBirth; } @OneToMany(fetch = FetchType.LAZY, mappedBy = "person", cascade = CascadeType.ALL) public Collection<Address> getAddresses() { return addresses; } public void setAddresses(Collection<Address> addresses) { this.addresses = addresses; } }
Configuring the model properties
Getting back to our client code, let's configure a bean property for our model. The
addBeanProperty() method takes three parameters - an alias, a bean class, and a property name.
In this example, the value contained in the person property of the PersonAction bean (i.e.
the value returned by PersonAction.getPerson()) will be made accessible under the local alias
of person
.
model.addBeanProperty("person", "org.jboss.seam.remoting.examples.model.PersonAction", "person");
After the model is fetched, the bean property value can be accessed via its alias, by using the getValue() method:
model.getValue("person")
Multiple bean properties may be configured any time before fetching the model - after the model has been fetched it is not possible to configure additional bean properties.
Fetching the model
To fetch the model we use the fetch() operation. When fetching a model, we may also specify an optional action. In this example let's pretend that we want to edit an existing person object, with a person ID of 42. We will need to invoke the PersonAction.editPerson() method to load the Person object from persistent storage. To do this we define an action like so:
var action = new Seam.Action() .setBeanType("org.jboss.seam.remoting.examples.model.PersonAction") .setMethod("editPerson") .addParam(42);
Once the action is defined we can then fetch our model:
model.fetch(action);
When the server processes the fetch operation, it will first invoke the action method (if specified) and then marshal the model property values to send back to the client. In this example, the action method that we invoke has a call to conversation.begin() which begins a long-running conversation. The conversation ID is returned in the client response and the model automatically maintains a reference to it for subsequent requests.
After the model has been fetched we are free to modify the model properties however we like:
model.getValue("person").setFirstName("John"); model.getValue("person").setLastName("Smith");
Expanding the model
Remember how our Person entity has a lazy-loaded collection of Address objects? When the Person object is marshalled and sent to the client, the addresses property has a value of undefined. This is a very specific object state in JavaScript and is distinct from null.
What if we wish to work with the person's addresses though - for example to add a new address or modify an existing address? This is where model expansion can help us. The expand() method can be used to expand the model by loading uninitialized associations. In this case we wish to load the person's addresses and tack them onto our existing model. This is a piece of cake with the Model API - the expand() method accepts two parameters, the model property that contains the uninitialized value, and the name of the uninitialized property:
model.expand(model.getValue("person"), "addresses");
This operation will send a request that on the server will load the person's addresses, return the address objects back to the client and replace the previously uninitialized (i.e. undefined) addresses property with an initialized collection of addresses, which we can then modify however we want. Pretty cool huh?
Applying our changes
Once we have finished making changes to the model, we can then apply these changes by using the applyUpdates() operation. Like the fetch() operation we can also specify an optional action to invoke, but in contrast the action is invoked at the end of the request after first applying the model updates.
In this example, we wish to save the changes that we have made to the person (and its addresses) by invoking the PersonAction.savePerson() method. Once again, we create an action object defining the action we wish to invoke, and then pass it to the applyUpdates() method:
var action = new Seam.Action() .setBeanType("org.jboss.seam.remoting.examples.model.PersonAction") .setMethod("savePerson"); model.applyUpdates(action);
When the applyUpdates() operation is invoked, the client calculates a delta containing any differences between the original model values that were fetched, and the current values after being modified by the user. This is where most of the Model API magic happens - to summarize briefly though the client sends this delta to the server where it is applied to the appropriate managed entity instances before being flushed to the database.
Dynamic type loading
One other thing I should mention is another new feature of Seam Remoting - dynamic type loading. Previously, if you wished to work with certain object types in the client you were forced to import the JavaScript stubs for those objects, or else the Remoting API wouldn't recognize them. With dynamic type loading, when the Remoting API encounters one or more bean types that it doesn't recognize, it now makes a separate request to the server to fetch the metadata for those bean types. So in our examples above, when the client receives a model response containing references to Person and Address beans, the Remoting API transparently fetches the definitions for those types and then continues processing the original response without missing a beat.
Summary
To wind up, I hope that the Model API opens up some new possibilities for the interesting use of AJAX in dynamic web applications. Personally I've got some (at least I think so) pretty cool ideas for some new view layer related technology which will be based on this stuff, and it will be great to see what other people come up with also. You can play around with the Model API right now by checking out the Remoting module from SVN:
http://anonsvn.jboss.org/repos/seam/modules/remoting/trunk
Just be warned it's still in development, so please let me know if you find any issues. One thing that Seam doesn't have yet is transaction support for non-JSF requests (so the example that we looked at doesn't actually commit the transaction at the end) but we should have this shortly. Enjoy!