With the last installment of this series on integrating ExtJS with Seam, Client-side Form Handling, we looked at a way to pass the form as an instance of Seam.Remoting.Map. With this solution the SeamSubmitAction bound the form fields to the map, using the form field name the key to hold the form field value. The map instance passed over the Seam Remoting framework to the action method that accepts the Map as its sole argument. While our map will only represent the form data, this solution was somewhat inspired by the Struts2 map representation of the HttpServletRequest. What we have now is a very flexible way of processing the form submission.
Recall that one of the design goals was to support the ability to merge the form data into a graph of related objects. This is accomplished by using a naming convention for the form fields and further by using the very handy BeanUtils framework from Apache Commons. The naming convention is actually the BeanUtils naming scheme for describing JavaBean properties. For those that aren't familiar with BeanUtils it is best known (by me at least) to be form -> FormBean binding solution in Struts1.x and Seam is using it somewhere within its framework too. Using BeanUtils to merge the Map into a set of JavaBeans is a snap. As an aside I prefer to use BeanUtils over OGNL in the web tiers that I build for clients. OGNL is nice but it all happens at the framework level so there isn't a way to unit test form -> JavaBean bindings. Its a bit anal and adds a line or 2 more code but I like to see that what I expect to happen actual does. I digress.
Let's take a look at an example that will help you better understand what I'm trying to describe. Let's start with the client-side code where I have defined an instance of my Ext.form.ux.SeamFormPanel as follows:
McDConsultingLLC.CreateUserPanel = function(config) {
// add the seam component reference
var userAction = Seam.Component.getInstance('userAction');
Ext.apply(config, {
seamComponent: userAction,
remoteMethod: userAction.create,
useMap: true
});
McDConsultingLLC.CreateUserPanel.superclass.constructor.call(this, config);
};
Ext.extend(McDConsultingLLC.CreateUserPanel, Ext.ux.form.SeamFormPanel, {
initComponent: function() {
buttons = [{
text: 'Save',
iconCls: 'x-icon-save',
handler: this.doSave,
scope: this
}, ' ', {
text: 'Cancel',
iconCls: 'x-icon-cancel',
handler: this.doCancel,
scope: this
}];
Ext.apply(this, {
closable: true,
iconCls: 'x-icon-user-edit',
tbar: buttons,
validationEvent: false,
bodyStyle: {
padding: '10px'
},
defaultType: 'textfield',
defaults: {
allowBlank: false,
width: 200
},
items: [{
fieldLabel: 'First Name',
name: 'firstName',
maxLength: 45
},{
fieldLabel: 'Middle Initial',
name: 'middleInitial',
width: 15,
maxLength: 1,
allowBlank: true
},{
fieldLabel: 'Last Name',
name: 'lastName',
maxLength: 45
},{
xtype: 'emailTextField',
maxLength: 45
},{
fieldLabel: 'Password',
name: 'password',
inputType: 'password',
maxLength: 45
},{
fieldLabel: 'Confirm Password',
name: 'confirmPassword',
inputType: 'password'
},{
xtype: 'clientComboBox',
name: 'client.id'
}]
});
McDConsultingLLC.CreateUserPanel.superclass.initComponent.call(this);
}
...
So this form represents a User class with an associated Client. The Client is selected from the clientComboBox where the combo's value is the Id of the Client instance. The name for the clientComboBox field is client.id. Here's the User class that is represented as an EJB3 Entity Bean:
@Entity
@Table(name = "user")
@NamedQueries({
@NamedQuery(
name="findByEmailAndPassword",
query="select u from User u where u.email=:email and u.password=:password"
)
})
public class User
implements Serializable
{
private static final long serialVersionUID = 7230925371262886872L;
private Long id;
private String firstName;
private String lastName;
private char middleInitial;
private String email;
private String password;
private Client client;
@Id
@GeneratedValue
@Column(name = "id", unique = true)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "firstName", nullable = false, length = 45)
@NotNull
@Length(max = 45)
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@Column(name = "lastName", nullable = false, length = 45)
@NotNull
@Length(max = 45)
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Column(name = "middleInitial", length = 45)
public char getMiddleInitial() {
return middleInitial;
}
public void setMiddleInitial(char middleInitial) {
this.middleInitial = middleInitial;
}
@Column(name = "email", nullable = false, length = 45)
@NotNull
@Length(max = 45)
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
@OneToOne
@JoinColumn(name = "client_id", nullable = true)
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
}
I use a JavaBean Action layer on top of the Stateless Session Bean or transactional layer. I do this architecturally for a couple of reasons. I still believe that MVC is a good model for building web tiers, in particular with Ext based front-ends where Json rendering takes place. I see Json as just another view representation of the domain model like HTML or XML, i.e. the V in MVC, which simply means we can have different views of the underlying Model. Another reason that I feel a web tier Action layer is good in that there are certain things like input validation and type conversions that should not be the responsibility of the business tier. Again with Ext we are bypassing JSF by tunneling to the Seam components via Remoting. In any case enough on this here is the Action method that shows the form -> object binding:
@WebRemote
@JsonResult
public String create(Map<String, Object> form)
throws Exception
{
// construct the new instance and
// associated client
User user = new User();
user.setClient(new Client());
// merge the form state onto the user
BeanUtils.populate(user, form);
// persist the user
userService.create(user);
// all is well
return JsonUtil.SUCCESS;
}
Viola we have a new User. There is a little trickery with the persistence that needs to be explained. I'm creating the User and setting an instance of Client onto it. We know the Client is already persisted since it was selected from a combobox generated by data fetch from the DataSource. Since the relationship between the User and Client isn't cascading, the Client instance here is just to manage the relationship between the two objects, i.e. the Client's state will not be updated during the transaction. This solution will also work with detached instances. In any case you can see that it only takes a single line of code to merge the form into the User, BeanUtils.populate(user, form). While this example doesn't show it, BeanUtils also uses ConverterUtils to do type conversions when populating the instances with the values from the Map.
This solution is the foundation for some others ideas that I have to make the server-side form handling more transparent and general purpose. Unfortunately I haven't had enough time lately to explore these ideas.
Next up will be some simple techniques that I've used to pragmatically deal with rendering Json and using Json as a protocol between the client and web tiers. Stay tuned.