วันนี้ลองเล่น JGoodies Binding จริงๆจังๆดูบ้าง
หน้าจอที่จะทดสอบ มีหน้าตาแบบนี้
ด้านซ้ายเป็น JList และด้านขวาเป็น edit panel
เริ่มด้วย Domain Model ก่อน
ก็คือ Person
public class Person extends Model {
private String name;
private double salary;
}
สังเกตว่า extends จาก com.jgoodies.binding.beans.Model
Note: ถ้า binding แบบง่ายๆ เราไม่ต้อง extend Model ก็ได้
แต่ถ้า feature ที่ใช้ซับซ้อนขึ้น มันบังคับให้เราต้อง extend Model.
แนวคิดหลัก ของ JGoodies (ซึ่งลอกมาจาก VisualSmalltalk อีกที)
ก็คือ ValueModel
GUI Widget จะไม่ access ข้อมูลผ่าน Domain Object ตรงๆ
แต่จะ set หรือ get ข้อมูล ผ่านทาง ValueModel
ตรงนี้อาจจะสับสนกับ Swing Model พวก TableModel, ListModel นิดหน่อย
ตรงที่ Swing model ก็มีวัตถุประสงค์ทำนองเดียวกันกับ Value Model
แต่ว่า Swing model ไม่ได้มีลักษณะเป็น universal model
ที่สามารถใช้ร่วมกันทั้งหมด ไม่ว่าจะเป็น widget ชนิดไหน แบบเดียวกับ ValueModel
ข้อดีของ Universal Model แบบ ValueModel
นอกจากจะทำให้การเขียน โปรแกรม เป็น pattern เดียวกันหมดแล้ว
ยังมีข้อดีในแง่ที่เราสามารถ นำ ValueModel มาต่อกันเป็น chain ได้
ทำให้เราสามารถทำ feature พวก Buffered, Convertor, Indirection ได้
กลับมาที่โปรแกรมเรา
เราจะออกแบบโปรแกรมของเรา โดยใช้ pattern
Presentation Model (ใน VisualWork Smalltalk เรียก Aplication Model)
เริ่มด้วยการสร้าง PresentationModel ของเรา
public class ApplicationModel {
}
Person Object ของเรา จะเก็บอยู่ใน ArrayList แบบนี้
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();
}
step ถัดไปคือ เตรียม ValueModel ที่จะใช้ binding กับ JList,
ตัว ValueModel ที่เราจะนำมาใช้ ห่อ persons ของเราก็คือ
SelectionInListซึ่งออกแบบมาเพื่อใช้กับพวก container widget เช่น JList, JTable.
feature ของมันก็คือ มันจะช่วย provide ValueModel ของ Current selection,selection index
ทำให้เรานำไป binding กับ widget อื่นๆได้อีก
(ซึ่งในกรณีของเรา ก็จะนำ current selection ไป binding กับ edit widget ที่อยู่ด้านขวา)
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();
private SelectionInList<Person> selectionInList;
public ApplicationModel() {
selectionInList = new SelectionInList<Person>(persons);
}
public SelectionInList getPersons() {
return selectionInList;
}
}
ในฝั่งของ GUI เวลาจะนำไปใช้ ก็จะทำประมาณนี้
public class MainFrame extends javax.swing.JFrame {
private ApplicationModel model = new ApplicationModel();
/** Creates new form MainFrame */
public MainFrame() {
initComponents();
Bindings.bind(jList1, model.getPersons());
}
...
}
step ถัดไป ก็คือการ bind current selection ใน JList เข้ากับ Editor Panel ที่อยู่ด้านขวา
ซึ่งในกรณีนี้ประกอบด้วย JTextField 2 ตัว สำหรับ name และ salary
เริ่มด้วยในฝั่ง ApplicationModel ก็เตรียม ValueModel ที่จะนำไป bind กับ JTextField
ตรงนี้เราสามารถทำได้หลายวิธี แต่วิธีที่เลือกใช้ก็คือ ใช้ com.jgoodies.binding.PresentationModel เข้ามาช่วย
(เพื่อที่จะได้ใช้ feature BufferedValueModel)
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();
private SelectionInList<Person> selectionInList;
private PresentationModel<Person> person;
public ApplicationModel() {
selectionInList = new SelectionInList<Person>(persons);
person = new PresentationModel<Person>(selectionInList.getSelectionHolder());
}
public ValueModel getPersonName() {
return person.getBufferedModel("name");
}
public ValueModel getPersonSalary() {
return new DoubleToStringConvertor(person.getBufferedModel("salary"));
}
class DoubleToStringConvertor extends AbstractConverter {
DoubleToStringConvertor(ValueModel subject) {
super(subject);
}
public Object convertFromSubject(Object arg0) {
if (arg0 != null) {
return arg0.toString();
}
return null;
}
public void setValue(Object arg0) {
subject.setValue(new Double(arg0.toString()));
}
}
}
จาก code ข้างบน จะมีประเด็นที่น่าสนใจอยู่ 2 ประเด็นคือ
1. เราใช้ getBufferedModel ในการ return ค่า ValueModel ไปให้ GUI view
ซึ่งผลก็คือ การเปลี่ยนแปลงใดๆที่เกิดขึ้นที่ฝั่ง GUI View จะยังไม่มีผลต่อ Model
จนกว่าจะมีการสั่ง commit การเปลี่ยนแปลง
2. กรณีที่ Datatype ฝั่ง model ไม่ใช่ String ก็จะมีประเด็นเรื่องการ Convert Datatype
ซึ่งใช้เทคนิคการ chain ValueModel เข้าหากัน
ฝั่ง GUI View ก็สามารถนำไป binding ได้ดังนี้
Bindings.bind(jTextField1, model.getPersonName());
Bindings.bind(jTextField2, model.getPersonSalary());
ขั้นถัดไป ก็คือเตรียม Action ที่จะใช้เพื่อ commit การเปลี่ยนแปลงที่เกิดขึ้นใน BufferedValueModel
ฝั่ง ApplicationModel
public Action getApplyAction() {
return new AbstractAction("Apply") {
public void actionPerformed(ActionEvent arg0) {
person.triggerCommit();
selectionInList.fireSelectedContentsChanged();
}
};
}
ฝั่ง GUI View
jButton1.setAction(model.getApplyAction());
ปัญหาของ BufferdValueModel ก็คือ กรณีที่เกิดการแก้ขึ้นแล้ว
ยังไม่มีการ commit, ถ้า Selection เกิดเปลี่ยนไป ข้อมูลที่ bound ไว้
จะไม่ยอมเปลี่ยนตาม ดังนั้นเราต้องสั่ง cancel การแก้ไข ด้วยการเรียกใช้ method triggerFlush
selectionInList.addPropertyChangeListener(
SelectionInList.PROPERTYNAME_SELECTION,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent arg0) {
person.triggerFlush();
}
});
พอแค่นี้ก่อน ที่เหลือไม่ค่อยมีประเด็นแล้ว
Note.
1. แนวคิด + วิธีในการใช้งาน มีตัวเลือกให้ใช้มากเกินไป (many way to do one thing) ถ้า assign ให้เด็กใหม่เอาไปใช้ ไม่น่าจะรอด
2. กรณีที่มีการ refactor เปลี่ยนชื่อ property ของ Domain Model จะส่งผลกระทบเยอะ
เพราะ binding ใช้วิธีกำหนดชื่อของ property ในรุป String
3. รู้สึกว่า Groovy ก็มีใช้ pattern ValueModel ใน GUI ด้วยเหมือนกัน
(เจอใน
gapi แต่ยังไม่เจอเอกสารที่พูดถึง)