Now we will develop the user interface of our application. The file menu.properties configures the application menu (placed in src/java):
We have 2 custom components (src/rjava):
- net.sf.jzeno.tutorial.components.ProductNameEditor.java
- net.sf.jzeno.tutorial.components.SaveCancelProductToolbar.java
... and 4 screens (src/rjava):
net.sf.jzeno.tutorial.screen.LoginScreen.java
net.sf.jzeno.tutorial.screen.OrderListScreen.java
net.sf.jzeno.tutorial.screen.OrderProductScreen.java
net.sf.jzeno.tutorial.screen.ProductAdminScreen.java
The details of the menu.properties file can also be edited using the Menu Editor which is available if you log in as admin.
Screen and Layout
A screen in jZeno is the specific content you wish to display. In most applications however, you have besides the variable screen content also a part you always want to display, such as a header, menu, footer, .... . This is implemented in jZeno through the concept of a Layout. Think of a layout as a screen which contains different screens, but also a title, menu, footer,... or anything else that always needs to stay visible). You can create a layout by extending the AbstractLayout. jZeno calls the method setContent(Component component) when it wants to place a new screen inside the layout. In this tutorial we will use the built-in default layout net.sf.jzeno.echo.ZenoLayout.We encourage you to take a look at the implementation of this screen. Remember that jZeno's .jar file comes with both bytecode as well as source code, so you can at any time drill into the sources of any jZeno class (use CTRL+left click in eclipse, or use CTRL-T to call up the source of any class)
The menu is specified in the file menu.properties, where menu entries are entered in the form:
menu.menuItem=Menu Title,fully.qualified.screenName,requiredPermissionToViewMenuItem
The default layout and screen are specified in configuration.properties
initial.screen.classname=net.sf.jzeno.tutorial.screen.LoginScreen
default.layout.classname=net.sf.jzeno.echo.ZenoLayout
LoginScreen
Let's look at our login screen. How the layout is composed is pretty self-explanatory: It basically boils down to creating hierarchies of grids for positioning. Look at how there are 2 equivalent ways to initialize components.
DynaButton loginButton = new DynaButton(null, "", "");
loginButton.setText("login");
loginButton.setActionCommand("login");
and
mainGrid.add(new DynaButton(null, "", "text=InitializeB,actionCommand=initDb"));
The shorthand method will use reflection to set the properties on the component. The actionCommand is a property on our loginScreen which is called once the button is pressed (in this case we have specified a method public void login(ActionEvent event) and public void initDb() . You can specify either zero parameters, an ActionEvent or a PropertyComponent as a parameter in your event handler and jZeno will find it.
Look at how we receive input from the user.
usernameField = new DynaTextField(getClass(), "username", "");
This process is known as binding. With the above statement we tell jZeno to make the property "username" of the LoginScreen accessible to the textfield. From now on the visual text field will allways show and modify the string that was exposed as property username.
package net.sf.jzeno.tutorial.screen;
import net.sf.jzeno.aop.SecuritySupport;
import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.KeyActionCommand;
import net.sf.jzeno.echo.components.NavigationHistory;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.TaskBar;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaButton;
import net.sf.jzeno.echo.databinding.DynaLabel;
import net.sf.jzeno.echo.databinding.DynaPasswordField;
import net.sf.jzeno.echo.databinding.DynaTextField;
import net.sf.jzeno.tutorial.business.TestDbSetupFacade;
import net.sf.jzeno.tutorial.business.UserManagerFacade;
import net.sf.jzeno.tutorial.model.User;
import net.sf.jzeno.util.FastFactory;
import nextapp.echo.Component;
import nextapp.echo.EchoConstants;
import nextapp.echo.event.ActionEvent;
import echopoint.Label;
public class LoginScreen extends Screen implements Precreation {
private static final long serialVersionUID = 1L;
private static final String TITLE = "Welcome to the jZeno tutorial application.";
private String username;
private String password;
private DynaPasswordField passwordField;
private DynaTextField usernameField;
public LoginScreen() {
DynaGrid mainGrid = new DynaGrid();
addCentered(mainGrid);
mainGrid.setColumns(1);
mainGrid.setCellMargin(2);
mainGrid.setWidth(100);
mainGrid.setWidthUnits(DynaGrid.PERCENT_UNITS);
mainGrid.add(new Title(TITLE));
mainGrid.add(new DynaLabel(
"Login as administrator with username/password = 'admin'"));
mainGrid.add(new DynaLabel(
"Login as customer with username/password = 'customer'"));
DynaGrid loginGrid = new DynaGrid(getClass(), "",
"columns=2,width=0,cellMargin=2");
mainGrid.addCentered(loginGrid);
loginGrid.add(new Label("username:"));
usernameField = new DynaTextField(getClass(), "username", "");
loginGrid.add(usernameField);
loginGrid.add(new Label("password:"));
passwordField = new DynaPasswordField(getClass(), "password", "");
loginGrid.add(passwordField);
DynaGrid.Cell cell = new DynaGrid.Cell();
cell.setRowSpan(2);
DynaButton loginButton = new DynaButton(null, "", "");
loginButton.setText("login");
loginButton.setActionCommand("login");
cell.add(loginButton);
cell.setColumnSpan(2);
cell.setHorizontalAlignment(EchoConstants.CENTER);
loginGrid.add(cell);
KeyActionCommand kac = new KeyActionCommand();
kac.setActionCommand("login");
add(kac);
applyContext();
}
public String getHistoryTitle() {
return null;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public void login(ActionEvent event) {
String username = usernameField.getText();
String password = passwordField.getText();
User user = (User) BusinessFactory.getFacade(UserManagerFacade.class)
.authenticate(username, password);
if (user != null) {
SecuritySupport.login(user);
if (SecuritySupport.currentUserHasPermission("productAdminScreen")) {
EchoSupport.setScreen(new ProductAdminScreen());
} else {
EchoSupport.setScreen(new OrderProductScreen());
}
} else {
EchoSupport.addError("loginscreen.error.invalid.logon");
}
setTaskbarVisible(true);
}
public void initDb() {
BusinessFactory.getFacade(TestDbSetupFacade.class).setupDb();
}
private boolean init = false;
public void preRender() {
setTaskbarVisible(false);
if (!init) {
initDb();
init = true;
}
}
private void setTaskbarVisible(boolean b) {
TaskBar menuBar = (TaskBar) EchoSupport.findComponentByType(
(Component) EchoSupport.getLayout(), TaskBar.class);
menuBar.setVisible(b);
menuBar.rebuild();
NavigationHistory navigationHistory = (NavigationHistory) EchoSupport
.findComponentByType((Component) EchoSupport.getLayout(),
NavigationHistory.class);
navigationHistory.setVisible(false);
}
public void applyContext() {
if (FastFactory.isInsideContext()) {
usernameField.setFocused(true);
}
}
}
ProductAdministrationScreen
This screen will be responsible for creating and updating our products. This is a good opportunity to demonstrate the DynaTable functionality. Look at the code used to display our product table :
ConstructionList cl = new ConstructionList();
cl.add("", "Name", "", StringViewer.class,"","required=true",ProductNameEditor.class);
cl.add("description","Description","",StringViewer.class,"description","",StringEditor.class);
cl.add("price","Price","",DoubleViewer.class,"price","",BigDecimalEditor.class);
cl.add("","","text=Edit,actionCommand=editProduct",DynaButton.class,"",
"saveCommand=saveProduct,cancelCommand=cancelEditProduct",
SaveCancelProductToolbar.class);
allProductsTable = new DynaTable(getClass(), "allProducts", "", cl);
add(allProductsTable);
The products table is bound to the list "allProducts" of our ProductAdminScreen and is created based upon a ConstructionList, that basically contains specifications for the columns of the table we are creating. When using a ConstructionList every add statement corresponds to adding a column in the table. For such a column you can specify a component and the property of the corresponding product to which this component is bound (by default it is bound to the entire Product). But notice how we seem to declare our binding twice for each column, this is because a DynaTable can have 2 modes for rendering a row: display and edit. For the "Name" column we use a StringViewer to display the name of a product, and we use our own component ProductNameEditor to edit the name. I will explain the code of the ProductNameEditor below, but know for now it verifies no duplicate product name is used. Also note that even though we only specify the class name of the ProductNameEditor, we can still alter the properties in the construction-hints specified ("required=true").
For the last column an edit-button will be rendered in display-mode with an event handler public void editProduct(). This function will get the product of the row where the button was clicked and will set that row to edit-mode:
public void editProduct(ActionEvent event) {
Product p = (Product) ((CustomComponent) event.getSource()).getValue();
allProductsTable.edit(p);
}
For edit-mode another custom component is used which renders a save and cancel button and will fire callback events as specified in the constructionhints field.
@SuppressWarnings("unchecked")
public void saveProduct(ActionEvent event) {
EchoSupport.doValidationRecursively(this);
if (EchoSupport.isValidRecursively(this)) {
Product p = (Product) ((PropertyComponent) event.getSource()).getValue();
p = BusinessFactory.getProductFacade().save(p);
allProductsTable.apply();
refreshAllProducts();
allProductsTable.update();
}
}
public void cancelEditProduct(ActionEvent event) {
allProductsTable.cancel();
}
Notice the use of ((PropertyComponent)event.getSource()).getValue() what this does is use the action event's source, which is a PropertyComponent to retrieve the value that component is bound to. All jZeno dynamic components implement this interface and can be used in this way.
Finally, the process of adding a new product is also pretty simple:
@SuppressWarnings("unchecked")
public void createNewProduct() {
Product product = new Product();
allProductsTable.addNew(product);
}
}
ProductAdminScreen
package net.sf.jzeno.tutorial.screen;
import java.util.List;
import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.business.GenericFacade;
import net.sf.jzeno.echo.ConstructionList;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaButton;
import net.sf.jzeno.echo.databinding.DynaLink;
import net.sf.jzeno.echo.databinding.DynaTable;
import net.sf.jzeno.echo.databinding.PropertyComponent;
import net.sf.jzeno.echo.editor.BigDecimalEditor;
import net.sf.jzeno.echo.editor.StringEditor;
import net.sf.jzeno.echo.viewer.BigDecimalViewer;
import net.sf.jzeno.echo.viewer.StringViewer;
import net.sf.jzeno.tutorial.business.ProductFacade;
import net.sf.jzeno.tutorial.components.ProductNameEditor;
import net.sf.jzeno.tutorial.components.SaveCancelProductToolbar;
import net.sf.jzeno.tutorial.model.Product;
import net.sf.jzeno.util.FastFactory;
import nextapp.echo.event.ActionEvent;
public class ProductAdminScreen extends Screen implements Precreation {
private static final long serialVersionUID = 1L;
private static final String TITLE = "Product Administration";
private DynaGrid mainGrid;
private String name;
private List allProducts;
private DynaTable allProductsTable;
public ProductAdminScreen() {
mainGrid = new DynaGrid(getClass(), "", "columns=1,cellMargin=2");
add(mainGrid);
mainGrid.add(new Title(TITLE));
ConstructionList cl = new ConstructionList();
cl.add("name", "Name", "", StringViewer.class, "", "required=true",
ProductNameEditor.class);
cl.add("description", "Description", "", StringViewer.class,
"description", "", StringEditor.class);
cl.add("price", "Price", "", BigDecimalViewer.class, "price",
"width=70", BigDecimalEditor.class);
cl
.add(
"",
"",
"toolTipText=Edit Product,actionCommand=editProduct,iconName=edit.png",
DynaLink.class,
"",
"saveCommand=saveProduct,cancelCommand=cancelEditProduct",
SaveCancelProductToolbar.class);
allProductsTable = new DynaTable(getClass(), "allProducts", "", cl);
allProductsTable.setColumnWidthUnits(DynaTable.PIXEL_UNITS);
allProductsTable.setColumnWidth(0, 200);
allProductsTable.setColumnWidth(1, 200);
allProductsTable.setColumnWidth(2, 80);
allProductsTable.setColumnWidth(3, 50);
allProductsTable.setWidth(-1);
mainGrid.add(allProductsTable);
mainGrid.add(new DynaButton(null, null,
"actionCommand=createNewProduct,text=Create new Product"));
applyContext();
}
private void refreshAllProducts() {
allProducts = BusinessFactory.getFacade(ProductFacade.class).find(null);
}
public void editProduct(ActionEvent event) {
Product p = (Product) ((PropertyComponent) event.getSource())
.getValue();
allProductsTable.edit(p);
}
public void saveProduct(ActionEvent event) {
EchoSupport.doValidationRecursively(this);
if (EchoSupport.isValidRecursively(this)) {
Product p = (Product) ((PropertyComponent) event.getSource())
.getValue();
p = (Product) BusinessFactory.getFacade(GenericFacade.class)
.saveOrUpdate(p);
allProductsTable.apply();
refreshAllProducts();
}
}
public void cancelEditProduct(ActionEvent event) {
allProductsTable.cancel();
}
public void createNewProduct() {
Product product = new Product();
allProductsTable.addNew(product);
}
public String getHistoryTitle() {
return TITLE;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List getAllProducts() {
return allProducts;
}
public void setAllProducts(List added) {
this.allProducts = added;
}
public void applyContext() {
if (FastFactory.isInsideContext()) {
refreshAllProducts();
}
}
}
SaveCancelProductToolbar
This is a very basic custom component. It provides 2 buttons (save and cancel) and allows the user of the component to specify the actionCommand when one of the buttons is pressed.
package net.sf.jzeno.tutorial.components;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.EventPropagator;
import net.sf.jzeno.echo.components.CustomComponent;
import net.sf.jzeno.echo.databinding.DynaLink;
import nextapp.echo.event.ActionEvent;
public class SaveCancelProductToolbar extends CustomComponent
implements EventPropagator {
private static final long serialVersionUID = 1L;
private DynaLink saveButton;
private DynaLink cancelButton;
private String saveCommand = "save";
private String cancelCommand = "cancel";
public SaveCancelProductToolbar() {
this(null, null, null);
}
public SaveCancelProductToolbar(Class beanClass, String property,
String constructionHints) {
super(beanClass, property, "");
initGui();
EchoSupport.executeHints(this, constructionHints);
}
public void initGui() {
saveButton = new DynaLink(getClass(), "",
"toolTipText=Save Product,iconName=save.gif,actionCommand=dummy");
cancelButton = new DynaLink(getClass(), "",
"toolTipText=Cancel,iconName=cancel.gif,actionCommand=dummy");
add(saveButton);
add(cancelButton);
}
public String getSaveCommand() {
return saveCommand;
}
public void setSaveCommand(String saveCommand) {
this.saveCommand = saveCommand;
}
public String getCancelCommand() {
return cancelCommand;
}
public void setCancelCommand(String cancelCommand) {
this.cancelCommand = cancelCommand;
}
public void propagateEvent(ActionEvent event) {
Object source = event.getSource();
if (source == saveButton) {
fireActionEvent(new ActionEvent(this, getSaveCommand()));
} else if (source == cancelButton) {
fireActionEvent(new ActionEvent(this, getCancelCommand()));
} else {
fireActionEvent(event);
}
}
}
ProductNameEditor
Our ProductNameEditor show how easy it is to add some basic validation. The ProductNameEditor was bound to the root object (a product) of the table in the ProductAdminScreen and binds a StringEditor to the "name" property of this product. You see that the bound object is always accessible through the inherited property "value" (accessed by get/setValue()). We also want to add validation to the ProductNameEditor so it checks the name for uniqueness, this is done by adding a Validator to the object. When validation is called (see ProductAdministrationScreen.saveProduct()), this Validator will be asked for a list of ValidationErrors. These errors contain a component which will be visually marked and an error message which will be shown to the user.
package net.sf.jzeno.tutorial.components;
import java.util.ArrayList;
import java.util.List;
import net.sf.jzeno.business.BusinessFactory;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.ValidationError;
import net.sf.jzeno.echo.Validator;
import net.sf.jzeno.echo.components.CustomComponent;
import net.sf.jzeno.echo.databinding.PropertyComponent;
import net.sf.jzeno.echo.editor.StringEditor;
import net.sf.jzeno.tutorial.business.ProductCriteria;
import net.sf.jzeno.tutorial.business.ProductFacade;
import net.sf.jzeno.tutorial.model.Product;
public class ProductNameEditor extends CustomComponent {
private static final long serialVersionUID = 1L;
private StringEditor stringEditor;
public ProductNameEditor() {
this(null, null, null);
}
public ProductNameEditor(Class> beanClass, String property,
String constructionHints) {
super(beanClass, property, null);
stringEditor = new StringEditor(getClass(), "product.name", "");
stringEditor.addValidator(new ProductNameValidator());
add(stringEditor);
EchoSupport.executeHints(this, constructionHints);
}
public Product getProduct() {
return (Product) getValue();
}
public void setProduct(Product product) {
setValue(product);
}
private class ProductNameValidator implements Validator {
private static final long serialVersionUID = 1L;
public List getValidationErrors(PropertyComponent pc) {
List ret = new ArrayList();
if (pc instanceof StringEditor) {
StringEditor stringEditor = (StringEditor) pc;
String name = stringEditor.getText();
Product p = ProductNameEditor.this.getProduct();
ProductCriteria productCriteria = new ProductCriteria();
productCriteria.setName(name);
List products = BusinessFactory.getFacade(
ProductFacade.class).find(productCriteria);
if (products.size() > 0) {
for (Product product : products) {
if (!p.equals(product)) {
System.out.println(p.getId() + " - "
+ product.getId());
ret.add(new ValidationError(pc,
"Product name already taken."));
}
}
}
}
return ret;
}
}
public boolean isRequired() {
return stringEditor.isRequired();
}
public void setRequired(boolean required) {
stringEditor.setRequired(required);
}
}
OrderProductScreen
This screen is used to -you guessed it- place orders. The only interesting thing in this screen is how (error) messages can be added manually.
if(order.getOrderItems().size() > 0){
BusinessFactory.getOrderFacade().saveOrder(order);
EchoSupport.addMessage("productorderscreen.messages.order.placed");
EchoSupport.setScreen(new OrderProductScreen());
}else{
EchoSupport.addError("productorderscreen.errors.no.items.ordered");
}
OrderListScreen
Also, a very simple screen. SecuritySupport is used to see if the user has the permission to view other user's orders. If not, he can only see his own. Add current user to orderCriteria if he can only view his own orders:
OrderCriteria crit = new OrderCriteria();
if(SecuritySupport.currentUserHasPermission("orderlistscreen.edit")) {
crit.setUser((User)
SecuritySupport.getCurrentUser());
}
orders = BusinessFactory.getOrderFacade().find(crit);
package net.sf.jzeno.tutorial.screen;
import java.util.List;
import net.sf.jzeno.aop.SecuritySupport;
import net.sf.jzeno.echo.ConstructionList;
import net.sf.jzeno.echo.EchoSupport;
import net.sf.jzeno.echo.Precreation;
import net.sf.jzeno.echo.components.DynaGrid;
import net.sf.jzeno.echo.components.Screen;
import net.sf.jzeno.echo.components.Title;
import net.sf.jzeno.echo.databinding.DynaTable;
import net.sf.jzeno.echo.viewer.DateViewer;
import net.sf.jzeno.echo.viewer.DoubleViewer;
import net.sf.jzeno.echo.viewer.StringViewer;
import net.sf.jzeno.tutorial.business.BusinessFactory;
import net.sf.jzeno.tutorial.business.OrderCriteria;
import net.sf.jzeno.tutorial.model.Order;
import net.sf.jzeno.tutorial.model.User;
import net.sf.jzeno.util.FastFactory;
public class OrderListScreen extends Screen implements Precreation {
private static final long serialVersionUID = 1L;
private static final String TITLE = "Orders";
private DynaTable ordersTable;
private List orders;
@Override
public String getHistoryTitle() {
return TITLE;
}
public OrderListScreen() {
this(null, null, null);
}
public OrderListScreen(Class beanClass, String property,
String constructionHints) {
DynaGrid mainGrid = new DynaGrid(getClass(), "", "columns=1");
add(mainGrid);
mainGrid.add(new Title(TITLE));
ConstructionList cl = new ConstructionList();
cl.add("id", "OrderNr", "", StringViewer.class);
cl.add("orderDate", "Date Ordered", "", DateViewer.class);
cl.add("user.userName", "Placed By", "", StringViewer.class);
cl.add("price", "Total Price", "", DoubleViewer.class);
ordersTable = new DynaTable(getClass(), "orders", "", cl);
ordersTable.setWidth(-1);
mainGrid.add(ordersTable);
EchoSupport.executeHints(this, constructionHints);
applyContext();
}
public void applyContext() {
if (FastFactory.isInsideContext()) {
OrderCriteria crit = new OrderCriteria();
if (!SecuritySupport
.currentUserHasPermission("orderlistscreen.edit")) {
crit.setUser((User) SecuritySupport.getCurrentUser());
}
orders = BusinessFactory.getOrderFacade().find(crit);
}
}
public List getOrders() {
return orders;
}
public void setOrders(List orders) {
this.orders = orders;
}
}
Final note
We hope this tutorial has been helpful. More information can be found in the Docs section of this site. The how-to's cover many of the aspects in this tutorial in more detail.