Lập trình Web JavaServer Faces
Center of Excellence
Bài
9
Doanh
thu toàn công ty
I. Bài toán
Hãy phát triển một
web application để tính tổng doanh thu toàn năm của một
công ty thương mại. Sau đây là ví dụ về doanh thu 4 quí
trong năm của 3 chi nhánh thuộc công ty
Quí 1
|
Quí 2
|
Quí 3
|
Quí 4
|
|
Chi nhánh 1
|
$35,698.77
|
$36,148.63
|
$31,258.95
|
$30,864.12
|
Chi nhánh 2
|
$41,289.64
|
$43,278.52
|
$40,928.18
|
$42,818.98
|
Chi nhánh 3
|
$28914.56
|
$27631.52
|
$30596.64
|
$29834.21
|
II. Giải pháp
Project: TotalSalesJSF
1. Thiết kế UI
Trước tiên
application cần biết công ty có bao nhiêu chi nhánh
(divisions), để từ đó xuất ra số lượng ô nhập thích
hợp.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Yearly Sales</title>
</h:head>
<h:body>
<h2>Yearly Sales</h2>
<h:form prependId="false">
<label>Number of divisions:</label>
<h:inputText id="divisions" required="true" value="#{index.divisions}">
<f:validateLongRange minimum="1"/>
</h:inputText>
<h:message for="divisions" errorStyle="color:red"/><br/>
<div>
<h:commandButton value="Input Sales"/>
<h:commandButton value="Clear"/>
</div>
</h:form>
<h:messages errorStyle="color:red"/>
</h:body>
</html>
package controller;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value = "index")
@RequestScoped
public class Index {
private Integer divisions;
public Index() {
}
public Integer getDivisions() {
return divisions;
}
public void setDivisions(Integer divisions) {
this.divisions = divisions;
}
}
Một khi đã biết
được số chi nhánh (divisions), application cần khởi
tạo tổng cộng (divisions x 4) ô nhập, do mỗi chi
nhánh có các doanh số (sales figures) cho 4 quí. Số ô nhập
này có thể được tổ chức trong một two-dimensional
array.
@Named(value = "index")
@RequestScoped
public class Index {
private Integer divisions;
private SalesFigure[][] salesFigures;
public Index() {
}
public void initSalesFigures() {
final int QUARTERS = 4;
salesFigures = new SalesFigure[divisions][QUARTERS];
for (int i = 0; i < divisions; i++)
for (int j = 0; j < QUARTERS; j++)
salesFigures[i][j] = new SalesFigure();
}
public Integer getDivisions() {
return divisions;
}
public void setDivisions(Integer divisions) {
this.divisions = divisions;
}
public SalesFigure[][] getSalesFigures() {
return salesFigures;
}
}
Về mặt UI, khi user
clicks Input Sales button,
initSalesFigures() cần
được thực hiện, ta bổ sung action
attribute cho button này.
<h:form prependId="false">
<label>Number of divisions:</label>
<h:inputText id="divisions" required="true" value="#{index.divisions}">
<f:validateLongRange minimum="1"/>
</h:inputText>
<h:message for="divisions" errorStyle="color:red"/><br/>
<div>
<h:commandButton value="Input Sales" action="#{index.initSalesFigures()}"/>
<h:commandButton value="Clear"/>
</div>
</h:form>
Sau đó, application cần hiển
thị các ô nhập để user có thể nhập doanh thu vào đó.
<h:form prependId="false">
<label>Number of divisions:</label>
<h:inputText id="divisions" required="true" value="#{index.divisions}">
<f:validateLongRange minimum="1"/>
</h:inputText>
<h:message for="divisions" errorStyle="color:red"/><br/>
<div>
<h:commandButton value="Input Sales" action="#{index.initSalesFigures()}"/>
<h:commandButton value="Clear"/>
</div>
<table>
<thead>
<tr>
<th></th>
<th>Quarter 1</th>
<th>Quarter 2</th>
<th>Quarter 3</th>
<th>Quarter 4</th>
</tr>
</thead>
<tbody>
<ui:repeat id="Row" var="division" value="#{index.salesFigures}">
<tr>
<td>Division ???</td>
<ui:repeat id="Column" var="salesFigure" value="#{division}">
<td>
<h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
<f:validateDoubleRange minimum="0.0"/>
</h:inputText>
</td>
</ui:repeat>
</tr>
</ui:repeat>
</tbody>
</table>
</h:form>
Để
hiển thị đúng số thứ tự của các divisions, ta lợi
dụng varStatus
attribute của <ui:repeat>,
trong đó index bắt
đầu từ 0.
<tbody>
<ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
<tr>
<td>Division #{status.index + 1}</td>
<ui:repeat id="Column" var="figure" value="#{division}">
<td>
<h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
<f:validateDoubleRange minimum="0.0"/>
</h:inputText>
</td>
</ui:repeat>
</tr>
</ui:repeat>
</tbody>
Điều
cần chú ý nữa là các ô nhập này chỉ xuất hiện sau
khi user đã nhập vào số divisions. Ta xử lý vấn đề này
bằng cách đặt <table>
vào một <h:panelGroup>
và qui định rằng <h:panelGroup>
này chỉ được hiển thị (rendered) nếu salesFigures
đã được khởi tạo, hay nói cách khác, salesFigures
là không rỗng (not empty).
<h:panelGroup rendered="#{not empty index.salesFigures}">
<table>
<thead>
<tr>
<th></th>
<th>Quarter 1</th>
<th>Quarter 2</th>
<th>Quarter 3</th>
<th>Quarter 4</th>
</tr>
</thead>
<tbody>
<ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
<tr>
<td>Division #{status.index + 1}</td>
<ui:repeat id="Column" var="salesFigure" value="#{division}">
<td>
<h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
<f:validateDoubleRange minimum="0.0"/>
</h:inputText>
</td>
</ui:repeat>
</tr>
</ui:repeat>
</tbody>
</table>
</h:panelGroup>
Sau
khi user đã nhập xong doanh thu, ta cần thêm một button để
tính tổng và một dòng thông báo kết quả tính toán.
<h:panelGroup rendered="#{not empty index.salesFigures}">
<table>
<thead>
<tr>
<th></th>
<th>Quarter 1</th>
<th>Quarter 2</th>
<th>Quarter 3</th>
<th>Quarter 4</th>
</tr>
</thead>
<tbody>
<ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
<tr>
<td>Division #{status.index + 1}</td>
<ui:repeat id="Column" var="salesFigure" value="#{division}">
<td>
<h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
<f:validateDoubleRange minimum="0.0"/>
</h:inputText>
</td>
</ui:repeat>
</tr>
</ui:repeat>
</tbody>
</table>
<h:commandButton value="Compute Total Sales" action="#{index.computeTotalSales()}"/><br/>
<h:panelGroup rendered="#{not empty index.totalSales}">
<label>Total Sales:</label>
<h:inputText value="#{index.totalSales}" disabled="true"/>
</h:panelGroup>
</h:panelGroup>
@Named(value = "index")
@RequestScoped
public class Index {
private Integer divisions;
private SalesFigure[][] salesFigures;
private Double totalSales;
public Index() {
}
public void initSalesFigures() {
final int QUARTERS = 4;
salesFigures = new SalesFigure[divisions][QUARTERS];
for (int i = 0; i < divisions; i++)
for (int j = 0; j < QUARTERS; j++)
salesFigures[i][j] = new SalesFigure();
}
public void computeTotalSales() {
// TODO
}
public Double getTotalSales() {
return totalSales;
}
...
}
Đến đây nếu thử thi hành
application, nhập vào số divisions, rồi click Input
Sales button, ta sẽ được
kết quả sau
Bây
giờ nếu click Compute Total Sales
thì các ô nhập sẽ biến mất (!). Lý do là vì ta đã
khai báo @RequestScoped
cho managed bean. Bản chất của request scope là không nhớ
lại data của lần tương tác trước để dùng lại cho
lần tương tác sau. Khi muốn dùng lại data của lần tương
tác trước, ta cần đặt managed bean vào session scope.
@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
private Integer divisions;
private SalesFigure[][] salesFigures;
private Double totalSales;
...
}
2. Phát triển model
Đến
đây ta tập trung phát triển computeTotalSales().
Như đã đề cập trong bài Doanh thu trong tuần,
ta dùng SalesFigure
wrapper class chỉ nhằm tương tác với UI. Khi thực hiện
việc tính toán, ta tháo bỏ lớp vỏ bọc này.
@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
private Integer divisions;
private SalesFigure[][] salesFigures;
private Double totalSales;
public Index() {
}
public void initSalesFigures() {
final int QUARTERS = 4;
salesFigures = new SalesFigure[divisions][QUARTERS];
for (int i = 0; i < divisions; i++)
for (int j = 0; j < QUARTERS; j++)
salesFigures[i][j] = new SalesFigure();
}
public void computeTotalSales() {
double[][] figures = salesFiguresToDoubles();
totalSales = ???;
}
private double[][] salesFiguresToDoubles() {
int rows = salesFigures.length;
int columns = salesFigures[0].length;
double[][] figures = new double[rows][columns];
for (int i = 0; i < rows; i++)
for (int j = 0; j < columns; j++)
figures[i][j] = salesFigures[i][j].getValue();
return figures;
}
...
}
2.1. Design
2.2. Test class
package model;
import org.junit.Test;
import static org.junit.Assert.*;
public class YearlySalesTest {
@Test public void sum() {
double[][] quarterlySales = {
{ 35_698.77, 36_148.63, 31_258.95, 30_864.12 },
{ 41_289.64, 43_278.52, 40_928.18, 42_818.98 },
{ 28_914.56, 27_631.52, 30_596.64, 29_834.21 } };
assertEquals(419_262.72, new YearlySales(quarterlySales).sum(), 6E-11);
}
}
2.3. Main class
YearlySales.java
package model;
import model.util.ArrayUtils;
public class YearlySales {
private final double[][] quarterlySales;
public YearlySales(double[][] quarterlySales) {
this.quarterlySales = quarterlySales;
}
public double sum() {
double acc = 0.0;
for (double[] divisionalSales : quarterlySales)
acc += ArrayUtils.sum(divisionalSales);
return acc;
}
}
YearlySales
đã lợi dụng class bổ trợ là ArrayUtils
để thực hiện việc tính tổng.
ArrayUtils.java
package model.util;
public class ArrayUtils {
private ArrayUtils() {
throw new RuntimeException(this.getClass() +
" is a noninstantiable utility class");
}
public static double sum(double[] values) {
double acc = 0.0;
for (double each : values)
acc += each;
return acc;
}
public static double sum(double[][] values) {
double acc = 0.0;
for (double[] row : values)
acc += sum(row);
return acc;
}
public static double average(double[] values) {
return sum(values) / values.length;
}
public static double min(double[] values) {
double currentMin = Double.POSITIVE_INFINITY;
for (double value : values) {
if (value < currentMin)
currentMin = value;
}
return currentMin;
}
public static double max(double[] values) {
double currentMax = Double.NEGATIVE_INFINITY;
for (double value : values) {
if (value > currentMax)
currentMax = value;
}
return currentMax;
}
}
3. Kết nối managed bean với model
@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
private Integer divisions;
private SalesFigure[][] salesFigures;
private Double totalSales;
...
public void computeTotalSales() {
double[][] figures = salesFiguresToDoubles();
totalSales = new YearlySales(figures).sum();
}
...
}
4. Định dạng số
Khi
thi hành application, ta nhận được kết quả sau
Để
tránh sai sót trong quá trình nhập liệu, ta cần cho phép
user sử dụng dấu cách hàng ngàn. Chẳng hạn, thay vì
phải nhập 35698.77, user có thể nhập 35,698.77. Thêm vào
đó, ta cần định dạng kết quả sao cho chỉ hiển thị
hai chữ số sau dấu chấm thập phân. Cách thức định
dạng số đã được đề cập trong bài Doanh thu
trong tuần. Ta có code bổ
sung cho phần UI như sau
<h:panelGroup rendered="#{not empty index.salesFigures}">
<table>
<thead>
<tr>
<th></th>
<th>Quarter 1</th>
<th>Quarter 2</th>
<th>Quarter 3</th>
<th>Quarter 4</th>
</tr>
</thead>
<tbody>
<ui:repeat id="Row" var="division" value="#{index.salesFigures}" varStatus="status">
<tr>
<td>Division #{status.index + 1}</td>
<ui:repeat id="Column" var="salesFigure" value="#{division}">
<td>
<h:inputText id="SalesFigure" value="#{salesFigure.value}" required="true">
<f:convertNumber pattern="#,##0.00"/>
<f:validateDoubleRange minimum="0.0"/>
</h:inputText>
</td>
</ui:repeat>
</tr>
</ui:repeat>
</tbody>
</table>
<h:commandButton value="Compute Total Sales" action="#{index.computeTotalSales()}"/><br/>
<h:panelGroup rendered="#{not empty index.totalSales}">
<label>Total Sales:</label>
<h:inputText value="#{index.totalSales}" disabled="true">
<f:convertNumber pattern="#,##0.00"/>
</h:inputText>
</h:panelGroup>
</h:panelGroup>
Khi
đó, UI screen sẽ trông như sau
5. Phát triển clear()
index.xhtml
<h:head>
<title>Yearly Sales</title>
<script src="resources/clearInputTexts.js"/>
</h:head>
<h:body>
<h2>Yearly Sales</h2>
<h:form prependId="false">
<label>Number of divisions:</label>
<h:inputText id="divisions" required="true" value="#{index.divisions}">
<f:validateLongRange minimum="1"/>
</h:inputText>
<h:message for="divisions" errorStyle="color:red"/><br/>
<div>
<h:commandButton value="Input Sales" action="#{index.initSalesFigures}"/>
<h:commandButton value="Clear" action="#{index.clear()}" immediate="true"
onclick="clearInputTexts(this.form)"/>
</div>
...
</h:body>
Index.java
@Named(value = "index")
@SessionScoped
public class Index implements Serializable {
private Integer divisions;
private SalesFigure[][] salesFigures;
private Double totalSales;
public Index() {
}
public void clear() {
divisions = null;
salesFigures = null;
totalSales = null;
}
...
}
III. Tổng kết
Tiếp nối bài Doanh thu trong tuần, bài này tiếp tục
dùng session scope để duy trì dữ liệu nhập, phục vụ
cho việc tính tổng doanh thu toàn năm của một công ty.
Thêm vào đó, bài đã trình bày kỹ thuật hiển thị và
che dấu một phần UI thông qua việc sử dụng
<h:panelGroup>
tag cùng với rendered
attribute của nó.
IV. Tài liệu tham khảo
- Tony Gaddis (2010) Starting Out with Java From Control Structures Through Objects (4th edition), Pearson Education, Boston. Chương 8.