Operasi CRUD Database Pada Aplikasi Desktop Dengan Griffon


Pada aplikasi web, model (M pada MVC) biasanya adalah domain model yang dapat langsung disimpan ke database.   Pada aplikasi desktop yang mengikuti pola MVC murni, model (M pada MVC) bukan  domain model.   Model disini hanya berlaku pada tampilan (presentation layer) sebagai media komunikasi antara view dan controller.   Dengan demikian, model bisa berisi atribut seperti buttonDisabled, textReadOnly, atau total.   Atribut buttonDisabled, misalnya, dipakai untuk menentukan apakah sebuah tombol di view boleh di-klik atau tidak.   Sementara itu, atribut total dipakai untuk menampung nilai sementara yang merupakan hasil kalkulasi yang akan ditampilkan di view.   Nilai-nilai tersebut jarang atau hampir tidak pernah ditemui pada model di MVC aplikasi web.

Tidak seperti pada Grails, tidak ada persistence layer seperti GORM (Hibernate) di Griffon. Sebagai gantinya, Griffon menyediakan plugin Gsql yang lebih primitif untuk mempermudah memberikan query SQL.   Saya akan meng-install plugin Gsql dengan memberikan seperti perintah berikut ini:

griffon install-plugin gsql

Pada bagian Configuration di Griffon View, saya akan menemukan file BootstrapGsql.groovy dan DataSource.groovy, seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Setelah Men-install Plugin Gsql

Struktur Proyek Setelah Men-install Plugin Gsql

Saya akan berpindah ke Project view, lalu membuka folder griffon-app, men-klik kanan pada folder resources, memilih New, File untuk membuat file baru yang saya beri nama schema.ddl. File ini akan berisi perintah SQL untuk membuat tabel yang dibutuhkan. Isi dari file tersebut adalah:

DROP TABLE IF EXISTS mahasiswa;

CREATE TABLE mahasiswa (
  nim CHAR(10) PRIMARY KEY,
  nama VARCHAR(200) NOT NULL,
  angkatan INT NOT NULL
);

File ini akan dikerjakan secara otomatis oleh Gsql.  Dengan demikian, saya bisa memastikan bahwa pada database yang saya pakai, tabel yang saya butuhkan selalu sudah ada.

Selain itu, untuk mempermudah pengujian, saya ingin tabel ini berisi data awal  yang sama saat dijalankan.   Oleh sebab itu, saya akan mengubah file BootstrapGsql.groovy sehingga isinya akan terlihat seperti berikut ini:

import groovy.sql.Sql

class BootstrapGsql {
    def init = { String dataSourceName = 'default', Sql sql ->
        def mahasiswa = sql.dataSet("mahasiswa")
        mahasiswa.add(nim: '1122334455', nama: 'Solid', angkatan: 2000)
        mahasiswa.add(nim: '1122334466', nama: 'Snake', angkatan: 2001)
        mahasiswa.add(nim: '1122334477', nama: 'Steve', angkatan: 2002)
    }

    def destroy = { String dataSourceName = 'default', Sql sql ->
    }
}

Berikutnya, saya membuka file DataSource.groovy untuk memberikan informasi yang berhubungan dengan database yang dipakai. Sebagai contoh, jika saya memakai database MySQL, dengan user berupa latihan, password berupa password, dan nama database berupa mahasiswa, maka saya perlu memberikan nilai untuk node dataSource() seperti berikut ini:

dataSource {
    driverClassName = 'com.mysql.jdbc.Driver'
    username = 'latihan'
    password = 'password'
    tokenizeddl = true
    pool {
        maxWait = 60000
        maxIdle = 5
        maxActive = 8
    }
}
environments {
    development {
        dataSource {
            dbCreate = 'create' // one of ['create', 'skip']
            url = 'jdbc:mysql://127.0.0.1/mahasiswa'
        }
    }
    test {
        dataSource {
            dbCreate = 'create'
            url = 'jdbc:mysql://127.0.0.1/mahasiswa'
        }
    }
    production {
        dataSource {
            dbCreate = 'skip'
            url = 'jdbc:mysql://127.0.0.1/mahasiswa'
        }
    }
}

Langkah terakhir yang berhubungan dengan database adalah mendaftarkan JAR MySQL ke proyek.  Untuk itu, saya membuka file BuildConfig.groovy dan menambahkan baris pada node dependencies() menjadi seperti berikut ini:

dependencies {
        runtime 'mysql:mysql-connector-java:5.1.18'
}

Sekarang, proyek sudah siap dan saya bisa mulai membuat kode program.

Operasi Read (cRud): Menampilkan Data

Saya akan membuat model pada file MahasiswaModel.groovy yang isinya seperti berikut ini:

class MahasiswaModel {
    List listMahasiswa = []
}

Setelah itu, saya membuat view sederhana seperti berikut ini:

package mahasiswa

application(title: 'mahasiswa',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform: true,
  iconImage:   imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    scrollPane(constraints: PAGE_START) {
        table {
            tableModel(list: model.listMahasiswa) {
                propertyColumn(header: "NIM", propertyName: "nim")
                propertyColumn(header:" Nama", propertyName: "nama")
                propertyColumn(header: "Angkatan", propertyName: "angkatan")
            }
        }
    }
}

Satu lagi pebedaan antara “MVC web” dan “MVC murni”: pada aplikasi desktop, view langsung ditampilkan!   Pada MVC web, biasanya yang pertama kali diakses adalah controller yang diwakili URL, baru kemudian controller menampilkan view.

Selain itu, view pada “MVC murni” bisa langsung mengakses controller kapan-pun.   Pada “MVC  web”, setiap kali view mengakses controller, berarti telah terjadi transportasi data dari browser ke server sehingga merupakan sebuah operasi yang berat.

Pertanyaannya adalah yang ditampilkan pertama kali adalah MahasiswaView, lalu kapan saya harus mengisi data di model untuk ditampilkan oleh view tersebut?

Saya bisa memanfaatkan life cycle event di Griffon. Sebuah aplikasi Griffon memiliki siklus hidup seperti initialize, startup, ready, shutdown, dan stop. Bila saya mendefinisikan sebuah closure dengan nama onStartupStart() di controller, maka pada saat fase Startup dimulai, isi closure onStartupStart() akan dipanggil.   Setelah fase Startup selesai diproses, maka closure dengan nama onStartupEnd() akan dipanggil bila ada.  Hal yang sama juga berlaku untuk fase lainnya.

Dengan demikian, saya akan membuat MahasiswaController yang mengisi model dengan data dari database pada saat fase Startup, seperti yang terlihat pada kode program berikut ini:

class MahasiswaController {

    def model
    def view

    def onStartupEnd = { app ->
        withSql { dataSourceName, sql ->
            sql.eachRow("SELECT nim, nama, angkatan FROM mahasiswa") {
                model.listMahasiswa << [nim: it.nim, nama: it.nama, angkatan: it.angkatan]
            }
        }
    }
}

Bila saya menjalankan aplikasi, saya akan menemukan tampilan seperti yang terlihat pada gambar berikut ini:

Menampilkan Data Dari Database

Menampilkan Data Dari Database

Operasi Create (dari Crud): Menambah Data Baru

Agar pengguna dapat menambah data baru, saya perlu memodifikasi MahasiswaView supaya terdapat panel untuk memasukkan data.   Untuk mempermudah membuat kode program tampilan, saya akan menggunakan MigLayout.   Saya mengubah MahasiswaView sehingga menjadi seperti berikut ini:

import net.miginfocom.swing.MigLayout

application(title: 'mahasiswa',
  preferredSize: [320, 240],
  pack: true,
  locationByPlatform: true,
  iconImage:   imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    scrollPane(constraints: CENTER) {
        table (id: 'table') {
            tableModel(list: model.listMahasiswa, id: 'tableModel') {
                propertyColumn(header: "NIM", propertyName: "nim")
                propertyColumn(header:" Nama", propertyName: "nama")
                propertyColumn(header: "Angkatan", propertyName: "angkatan")
            }
        }
    }
    panel(layout: new MigLayout('', '[right][left,grow]',''), constraints: PAGE_END) {
        label('NIM:')
        textField(columns: 15, constraints: 'wrap')
        label('Nama:')
        textField(constraints: 'growx,wrap')
        label('Angkatan:')
        textField(columns: 6, constraints: 'wrap')
        panel(constraints: 'span, growx, wrap') {
            flowLayout()
            button("Simpan")
        }
    }
}

Apa yang akan terjadi bila tombol Simpan di-klik?   Saya ingin closure simpan() di controller dikerjakan.   Oleh sebab itu, saya mengubah kode program MahasiswaView untuk  tombol Simpan menjadi seperti berikut ini:

button("Simpan", actionPerformed: controller.simpan)

Yup!  Di atas adalah cara menghubungkan view ke controller.  Lalu bagaimana dengan data-nya (model)?

Pada MVC di-web, controller harus bekerja mengambil request parameter dari browser untuk diterjemahkan menjadi model.

Apakah pada MVC murni pada desktop juga begitu??  Tidak!!

Pada Griffon, saya melakukan binding dimana setiap kali nilai di JTextField berubah, maka nilai di model langsung berubah pada saat itu juga.   Jadi, controller tidak perlu mengambil nilai secara manual dari view dan bisa langsung memakai model.

Berikut ini adalah isi MahasiswaModel:

package mahasiswa

class MahasiswaModel {
    @Bindable String nim
    @Bindable String nama
    @Bindable Integer angkatan
    List listMahasiswa = []
}

Untuk melakukan binding dari view ke model, saya mengubah definisi JTextField yang ada di view menjadi seperti berikut ini:

label('NIM:')
textField(columns: 15, text: bind(target: model, targetProperty: 'nim'), constraints: 'wrap')
label('Nama:')
textField(text: bind(target: model, targetProperty: 'nama'), constraints: 'growx,wrap')
label('Angkatan:')
textField(text: bind(target: model, targetProperty: 'angkatan', converter: { v ->
    try { Integer.parseInt(v) } catch (NumberFormatException) { 0 }
}), columns: 6, constraints: 'wrap')

Dengan menggunakan node bind(), saya dapat menghubungkan view dan model.   Selain itu, saya juga dapat menyertakan converter dan validator disini bila perlu.   Btw, bind() tidak harus diterapkan pada view ke model, tetapi juga berlaku untuk semua jenis bean yang mendukung.   Misalnya, bila nilai sebuah JTextField adalah hasil kalkulasi dari JTextField lainnya,  maka saya dapat men-bind() JTextField tersebut dengan menyertakan closure yang berisi rumus perhitungan dari JTextField lain.

Berikutnya, saya akan mengubah controller sehingga isinya akan terlihat seperti berikut ini:

import groovy.sql.Sql

class MahasiswaController {

    def model
    def view

    def onStartupEnd = { app ->
        refreshListMahasiswa()
    }

    def refreshListMahasiswa = {
        model.listMahasiswa.clear()
        withSql { dataSourceName, Sql sql ->
            sql.eachRow("SELECT nim, nama, angkatan FROM mahasiswa") {
                model.listMahasiswa << [nim: it.nim, nama: it.nama, angkatan: it.angkatan]
            }
        }  
    }

    def simpan = {
        withSql { dataSourceName, Sql sql ->
            def mahasiswa = sql.dataSet("mahasiswa")
            mahasiswa.add(nim: model.nim, nama: model.nama, angkatan: model.angkatan)
        }
        refreshListMahasiswa()
        edt {
            view.tableModel.fireTableDataChanged()
            view.table.revalidate()
        }
    }
}

Pada kode program di atas, terlihat bahwa kode program controller masih terlihat rumit akibat binding secara manual khusus untuk JTable.   Hal ini karena DefaultTableModel yang dipakai oleh JTable secara bawaan tidak mendukung binding dan event.   Untuk kode program yang lebih nyaman, saya seharusnya memakai GlazedLists.

Operasi penambahan baru sudah dapat bekerja seperti yang terlihat pada gambar berikut ini:

Operasi Insert Pada Database

Operasi Insert Pada Database

Operasi Update (dari crUd): Mengubah Data Yang Sudah Ada

Saya menginginkan pada saat pengguna men-klik sebuah baris yang ada di tabel, maka data tabel tersebut muncul di masing-masing JTextField sehingga pengguna bisa meng-editnya.

Langkah pertama yang saya lakukan adalah mengelompokkan setiap komponen JTextField yang ada ke dalam sebuah bindGroup, seperti yang diperlihatkan pada kode program berikut ini:

import net.miginfocom.swing.MigLayout

import javax.swing.event.ListSelectionEvent

application(title: 'mahasiswa',
  preferredSize: [320, 440],
  pack: true,
  locationByPlatform: true,
  iconImage:   imageIcon('/griffon-icon-48x48.png').image) {

    borderLayout()
    scrollPane(constraints: CENTER) {
        table (rowSelectionAllowed: true, id: 'table') {
            tableModel(list: model.listMahasiswa, id: 'tableModel') {
                propertyColumn(header: "NIM", propertyName: "nim")
                propertyColumn(header:" Nama", propertyName: "nama")
                propertyColumn(header: "Angkatan", propertyName: "angkatan")
            }
        }
        table.selectionModel.valueChanged = { ListSelectionEvent evt ->
            if (!evt.getValueIsAdjusting()) {
                model.barisTerpilih(table.getSelectedRow())
                entryForm.reverseUpdate()
            }
        }
    }
    panel(layout: new MigLayout('', '[right][left,grow]',''), constraints: PAGE_END) {
        bindGroup(id: 'entryForm', bound: true)
        label('NIM:')
        textField(columns: 15, text: bind(target: model, targetProperty: 'nim', group: entryForm), constraints: 'wrap')
        label('Nama:')
        textField(text: bind(target: model, targetProperty: 'nama', group: entryForm), constraints: 'growx,wrap')
        label('Angkatan:')
        textField(text: bind(target: model, targetProperty: 'angkatan', group: entryForm, converter: { v ->
            try { Integer.parseInt(v) } catch (NumberFormatException) { 0 }
        }), columns: 6, constraints: 'wrap')
        panel(constraints: 'span, growx, wrap') {
            flowLayout()
            button("Simpan", actionPerformed: controller.simpan)
        }
    }
}

Pada kode program di atas, masing-masing JTextField untuk NIM, nama dan angkatan masuk dalam dalam sebuah bindGroup dengan nama entryForm.   Tujuan pengelompokan ini adalah agar mudah melakukan pengaturan terhadap lebih dari satu binding sekaligus.

Kembali lagi karena keterbatasan JTable, saya harus membaca event secara manual tanpa binding untuk menentukan apakah pengguna memilih salah satu baris yang ada.   Bila pengguna memilih salah satu baris, maka method barisTerpilih() di model akan dikerjakan, lalu setiap binding yang termasuk dalam entryForm akan di-reverse update (karena arah binding normal mereka adalah view –> model [view memperbaharui model], maka reverse update akan menyebabkan view <– model [model memperbaharui view]).

P.S:  Nanti saya akan mengubah kode program di bawah ini  ke cara yang lebih rapi dengan memakai PropertyListener.

Kode program MahasiswaModel sekarang akan terlihat seperti berikut ini:

class MahasiswaModel {
    @Bindable String nim
    @Bindable String nama
    @Bindable Integer angkatan
    @Bindable int selectedRow = -1
    List listMahasiswa = []

    public void  barisTerpilih(int selectedRow) {
        if (!listMahasiswa.isEmpty()) {
            if (selectedRow in (0..listMahasiswa.size()-1)) {
                nim = listMahasiswa[selectedRow].nim
                nama = listMahasiswa[selectedRow].nama
                angkatan = listMahasiswa[selectedRow].angkatan
            }
        }
    }

}

Langkah berikutnya, saya akan mengubah closure simpan() di MahasiswaController untuk mendukung proses update, sehingga terlihat seperti berikut ini:

def simpan = {
    withSql { dataSourceName, Sql sql ->
            DataSet mahasiswa = sql.dataSet("mahasiswa")
            if (sql.firstRow("SELECT nim FROM mahasiswa WHERE nim = ${model.nim}") != null) {
                // sudah ada NIM yang sama, maka lakukan update.
                mahasiswa.executeUpdate("UPDATE mahasiswa SET nama = ${model.nama}, angkatan = ${model.angkatan} WHERE nim = ${model.nim}")
            } else {
                // belum ada NIM, maka tambahkan.
                mahasiswa.add(nim: model.nim, nama: model.nama, angkatan: model.angkatan)
            }

        }
        refreshListMahasiswa()
        edt {
            view.tableModel.fireTableDataChanged()
            view.table.revalidate()
        }
    }

Pada kode program controller di atas, saya memberikan query dengan langsung menyertakan nilai ke dalam GString.   Walaupun terlihat hampir sama seperti string di  PHP, memakai GString dalam query di Gsql menyebabkan beberapa hal yang terjadi di balik layar.  Pertama, saya tidak memberikan tanda kutip untuk kolom varchar karena Gsql akan secara otomatis mengolah nilai parameter tersebut.  Kedua, Gsql akan secara otomatis melakukan pengamanan untuk menghindari SQL Injection.

Sekarang, saya bisa meng-edit data seperti yang diperlihatkan oleh gambar berikut ini:

Operasi Edit Pada Database

Operasi Edit Pada Database

Operasi Delete (dari cruD): Menghapus Data Yang Sudah Ada

Operasi yang terakhir adalah operasi untuk menghapus.   Untuk itu, saya perlu menambah sebuah JButton di view yang berfungsi untuk melakukan penghapusan, seperti berikut ini:

        panel(constraints: 'span, growx, wrap') {
            flowLayout()
            button("Simpan", actionPerformed: controller.simpan)            
            button("Hapus", enabled: bind {model.selectedRow!=-1}, actionPerformed: controller.hapus)
        }

Disini saya melakukan binding property enabled dari JButton tersebut terhadap sebuah ekspresi di dalam closure.  Hal ini karena tombol Hapus hanya boleh di-click bila ada sebuah baris yang terpilih. Agar binding ini bekerja dengan baik, saya akan merombak MahasiswaModel sehingga isinya akan terlihat seperti berikut ini:

import griffon.transform.PropertyListener

class MahasiswaModel {
    @Bindable String nim
    @Bindable String nama
    @Bindable Integer angkatan
    @PropertyListener(updateSelectedRow) @Bindable int selectedRow = -1
    @Bindable boolean bolehHapus = false
    List listMahasiswa = []

    def updateSelectedRow = {
        if (!listMahasiswa.isEmpty() ) {
            if (selectedRow in (0..listMahasiswa.size()-1)) {
                nim = listMahasiswa[selectedRow].nim
                nama = listMahasiswa[selectedRow].nama
                angkatan = listMahasiswa[selectedRow].angkatan
            }
        }
    }
}

Perbedaan dengan versi sebelumnya adalah saya memakai @PropertyListener(updateSelectedRow) di selectedRow. Makna dari annotation tersebut adalah apabila nilai selectedRow berubah, maka closure updateSelectedRow akan dikerjakan. Sementara cara saya yang sebelumnya saya pakai dinilai agak ‘kasar’ karena saya menimpa setter yang ada!

Langkah terakhir, saya perlu menambahkan closure di MahasiswaController yang akan melakukan proses penghapusan, seperti pada kode program berikut ini:

def hapus = {
   withSql { dataSourceName, Sql sql ->
      sql.executeUpdate("DELETE FROM mahasiswa WHERE nim = ${model.nim}")
   }
   refreshListMahasiswa()
   edt {
      view.tableModel.fireTableDataChanged()
      view.table.revalidate()
   }
}

Lagi-lagi saya harus memperbaharui JTable secara manual tanpa dapat memakai fasilitas binding. Saya membutuhkan sebuah table yang lebih peka terhadap event bila ingin menghasilkan kode program yang sederhana dan rapi!

Pada saat program di atas dijalankan, tombol Hapus tidak akan bisa di-klik sebelum sebuah baris di tabel terpilih, seperti yang terlihat pada gambar berikut ini:

Operasi Hapus Data Yang Sudah Ada

Operasi Hapus Data Yang Sudah Ada

Tentang Solid Snake
I'm nothing...

20 Responses to Operasi CRUD Database Pada Aplikasi Desktop Dengan Griffon

  1. Ping-balik: Memakai Glazed Lists Di Griffon « The Solid Snake

  2. Mas, mau tanya nih..
    ketika saya jalanin programnya saya dapat error seperti ini

    2013-03-18 13:03:40,550 [main] DEBUG griffon.plugins.datasource.DataSourceEnhancer – Enhancing groovy.lang.ExpandoMetaClass@19f1aa5[class mahasiswa.MahasiswaController] with griffon.plugins.datasource.DefaultDataSourceProvider@8e4ea6
    2013-03-18 13:03:40,573 [main] DEBUG griffon.plugins.datasource.DataSourceEnhancer – Enhancing groovy.lang.ExpandoMetaClass@21840d[class mahasiswa.TestController] with griffon.plugins.datasource.DefaultDataSourceProvider@8e4ea6
    2013-03-18 13:03:40,672 [main] WARN griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but default-schema-dev.ddl was not found in classpath.
    2013-03-18 13:03:40,672 [main] WARN griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but default-schema.ddl was not found in classpath.
    2013-03-18 13:03:40,673 [main] WARN griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but schema-dev.ddl was not found in classpath.
    2013-03-18 13:03:40,673 [main] WARN griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but schema.ddl was not found in classpath.
    2013-03-18 13:03:40,673 [main] ERROR griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but no suitable schema was found in classpath.
    2013-03-18 13:03:40,695 [main] DEBUG griffon.plugins.datasource.AbstractDataSourceProvider – Executing statement on dataSource ‘default’
    2013-03-18 13:03:41,047 [main] INFO griffon.swing.SwingApplication – Initializing all startup groups: [mahasiswa]
    2013-03-18 13:03:42,262 [main] DEBUG griffon.plugins.datasource.AbstractDataSourceProvider – Executing statement on dataSource ‘default’
    2013-03-18 13:03:58,351 [AWT-EventQueue-0] ERROR griffon.util.GriffonExceptionHandler – Uncaught Exception
    groovy.lang.MissingMethodException: No signature of method: mahasiswa.MahasiswaModel.barisTerpilih() is applicable for argument types: (java.lang.Integer) values: [0]
    at mahasiswa.MahasiswaView$_run_closure1_closure2_closure5.doCall(MahasiswaView.groovy:22)

    dan ketika launching lagi, muncul pesan error, katanya table mahasiswa sudah ada..
    kalau di schema.dll kan sudah dikatakan kalau ada table mahasiswa akan didrop dan diganti dgn yg baru tapi itu kok ga jalan ya?

    di datasource jg saya ga ngerti dengan
    dbCreate = ‘create’ // one of [‘create’, ‘skip’]
    url = ‘jdbc:mysql://127.0.0.1/mahasiswa’

    url disini maksudnya apa ya?
    saya disini pake xampp kira2 gmn ya settingannya?

    tombol hapus juga ga jalan ketika saya klik masing2 row, muncul error
    2013-03-18 13:03:58,351 [AWT-EventQueue-0] ERROR griffon.util.GriffonExceptionHandler – Uncaught Exception
    groovy.lang.MissingMethodException: No signature of method: mahasiswa.MahasiswaModel.barisTerpilih() is applicable for argument types: (java.lang.Integer) values: [0]
    at mahasiswa.MahasiswaView$_run_closure1_closure2_closure5.doCall(MahasiswaView.groovy:22)

    itu gmn ya?

    mohon pencerahannya..

    • Solid Snake says:

      Baris berikut ini:

      2013-03-18 13:03:40,673 [main] WARN griffon.plugins.datasource.DataSourceConnector – DataSource[default].dbCreate was set to ‘create’ but schema.ddl was not found in classpath.

      Menunjukkan bahwa file schema.ddl tidak ditemukan. Pastikan kamu meletakkannya pada lokasi yang benar. Pastikan juga tidak ada yang salah di nama file.

      URL seperti jdbc:mysql://127.0.0.1/mahasiswa adalah JDBC URL; di ADO.NET, ini disebut connection string. 127.0.0.1 adalah lokasi komputer lokal yang menjalankan server database (dalam hal ini localhost). mahasiswa adalah nama database yang ada di database. Pastikan nama database ini sudah dibuat sebelumnya. Gunakan perintah CREATE DATABASE dari MySQL atau phpMyAdmin bawaan XAMPP untuk membuat database baru.

      Untuk kesalahan:

      groovy.lang.MissingMethodException: No signature of method: mahasiswa.MahasiswaModel.barisTerpilih()

      Ini adalah kesalahan saya. Di model, method setSelectedRow() seharusnya adalah barisTerpilih(). Saya akan memperbaharui kode program di atas.

  3. untuk schema.dll sudah saya buat di folder resource, tp itu tetap tidak dikenal apa perlu config sesuatu mas?
    untuk url database, databasenya sudah saya buat apa sintaksnya harus 127.0.0.1/phpmyadmin/mahasiswa ya?

    saya tunggu updateannya mas..

    Mas, tahu cara buat window baru di dalam main window ga mas?kaya buka view ke2 di main window..mohon bantuannya..

    • Solid Snake says:

      Bukan ‘schema.dll’ tapi ‘schema.ddl’ (data definition language). JDBC URL tidak perlu menyertakan phpmyadmin, tetapi langsung pada database yang kamu buat, misalnya “jdbc:mysql://127.0.0.1/mahasiswa”.

      Kamu bisa mendefinisikan sebuah view baru yang berisi JPanel; untuk menampilkannya buat sebuah JFrame yang isinya adalah panel dari view tersebut. Kamu perlu menambahkan view baru ini di Application.groovy. Untuk informasi lebih lanjut, baca dokumentasi Swing.

      Cara lain adalah dengan membuat sebuah MVCGroup baru. Bila membuat MVCGroup baru, kamu bisa membuat view, controller & model baru yang khusus untuk group tersebut. Sebuah MVCGroup dapat memanggil MVCGroup lainnya. Untuk informasi lebih lanjut, baca dokumentasi Griffon.

  4. Steven Way says:

    Untuk membuat window di dalam window, gunakan JInternalFrame. Penjelasan, screenshot dan contoh kode program bisa dilihat di http://docs.oracle.com/javase/tutorial/uiswing/components/internalframe.html.

  5. @Solid Snake: ah, maaf itu kesalahan saya yg ga liat extensionnya, hehe..Thanks bgt mas, sekarang applikasinya udah jalan, fungsi tombol hapus juga sudah bisa dipakai setelah mengganti def setSelectedRow di model. Mas, itu berarti kan hanya 1 window saja yg bisa dimodif viewnya, yaitu kita memodif view dari master viewnya. Seperti contoh pada GroovyEdit aalmiray memang ada 2 MVCGroup, tp yg dimodif adalah view utama. Nah, saya sih pengen buat aplikasi penjualan pakai griffon, apakah bisa ya?

    @Steven Way: Thanks untuk infonya. Apakah bisa diimplementasikan di Griffon mas?

    • Solid Snake says:

      Layout yang sering saya pakai adalah sebuah toolbar di atas dan isinya berupa panel. Masing-masing view didaftarkan ke panel ini dengan CardLayout. Sebagai informasi, CardLayout seperti tumpukan view, boleh mendaftarkan banyak view, tapi hanya 1 view yang terlihat. Pada saat pengguna memilih icon di toolbar, maka panel akan menampilkan view yang sesuai dengan action di toolbar tersebut.

      Saya cenderung menghindari penggunaan style MDI (lebih dari satu window yang muncul secara bersamaan) bisa membingungkan pengguna dan rumit di sinkronisasi antar-window yang ditampilkan secara bersamaan oleh pengguna.

      Tapi kamu tetap bisa memakai style MDI di Griffon.. Kamu bisa memakai node internalFrame(). Contoh kode program MDI untuk Griffon bisa dilihat di http://markmail.org/message/2qm75esbcwxlml25.

  6. Mas, ketika saya run muncul error seperti ini:

    2013-03-23 12:13:01,877 [main] DEBUG griffon.plugins.i18n.I18nEnhancer – Enhancing org.codehaus.groovy.runtime.HandleMetaClass@b7ea5c[groovy.lang.ExpandoMetaClass@b7ea5c[class griffon.swing.SwingApplication]] with griffon.plugins.i18n.MessageSourceHolder@7b7bee
    2013-03-23 12:13:01,909 [main] ERROR griffon.util.GriffonExceptionHandler – Uncaught Exception
    groovy.lang.GroovyRuntimeException: Cannot add new method [getMessage] for arguments [[class java.lang.String, class java.util.Locale]]. It already exists!
    at griffon.plugins.i18n.I18nEnhancer.enhance(I18nEnhancer.groovy:34)
    at griffon.plugins.i18n.I18nEnhancer.enhance(I18nEnhancer.groovy)
    at griffon.plugins.i18n.I18nEnhancer$enhance.call(Unknown Source)
    at I18nSupportGriffonAddon.addonPostInit(I18nSupportGriffonAddon.groovy:40)
    at griffon.core.GriffonAddon$addonPostInit.call(Unknown Source)
    at griffon.core.GriffonAddon$addonPostInit.call(Unknown Source)
    at org.codehaus.griffon.runtime.util.AddonHelper$_handleAddonsAtStartup_closure3.doCall(AddonHelper.groovy:110)
    at org.codehaus.griffon.runtime.util.AddonHelper.handleAddonsAtStartup(AddonHelper.groovy:108)
    at org.codehaus.griffon.runtime.core.DefaultAddonManager.doInitialize(DefaultAddonManager.java:33)
    at org.codehaus.griffon.runtime.core.AbstractAddonManager.initialize(AbstractAddonManager.java:101)
    at org.codehaus.griffon.runtime.util.GriffonApplicationHelper.initializeAddonManager(GriffonApplicationHelper.java:394)
    at org.codehaus.griffon.runtime.util.GriffonApplicationHelper.prepare(GriffonApplicationHelper.java:149)
    at org.codehaus.griffon.runtime.core.AbstractGriffonApplication.initialize(AbstractGriffonApplication.java:231)
    at griffon.swing.AbstractSwingGriffonApplication.bootstrap(AbstractSwingGriffonApplication.java:75)
    at griffon.swing.AbstractSwingGriffonApplication.run(AbstractSwingGriffonApplication.java:132)
    at griffon.swing.SwingApplication.run(SwingApplication.java:45)
    at griffon.swing.SwingApplication.main(SwingApplication.java:37)
    [delete] Deleting directory G:\latihan\outer\staging\windows

    Apakah sintaksnya berbeda antara griffon 0.9.4 dengan 1.2.0?seperti di post di 0.9.4 bisa dipakai..

  7. Mas, ketika saya lakukan uninstall pada plugin i18n-support dan jalankan program lagi, dia tetap melakukan install ulang plugin tersebut, sementara plugin ini dibutuhkan oleh plugin actions. Bagaimana baiknya ya? Hmm..

    • Solid Snake says:

      Pada link terakhir yang saya berikan, disebutkan bahwa selain plugin i18n, plugin actions juga telah menjadi bagian dari Griffon. Oleh sebab itu, plugin i18n, i18n-support dan actions boleh di-uninstall dengan aman.

  8. Akhirnya bisa juga..Ternyata selain uninstall plugin kita juga harus hapus pemakaiannya di application.properties, karena disana yang mentrigger pemakaian pluginnya 😀

  9. mas, untuk internal frame, apakah dia bisa load isi suatu tabel ketika diklik?
    mksdnya ketika saya klik tombol action1 muncul internal frame action1 yang didalamnya sudah diload isi tabel ‘test’ langsung data2 yg sudah ada..kalo yg disini kan karna 1 mvc jadi kelihatan langsung..kalau yg saya pakai 2 mvc dan mvc yg kedua ini tidak bisa diload isi tablenya..

    kalau fungsi def onStartupEnd tu fungsinya apa ya mas?

  10. Mas, gmn ya caranya nampilin konten database tabel di dalam JInternal Frame?
    klo saya gambarin seperti ini mas: http://i.stack.imgur.com/3TXnz.png

    gmn ya kira2?

  11. mas, mau tanya nih..
    gmn ya cranya encrypt md5 di griffon?
    saya lihat di griffon ada sintaks md5.encode() dan pakai plugin bcrypt jg tp setelah saya coba tidak bisa juga..
    saya kan lg buat app untuk table user yg mana disana harus menggunakan md5 untuk storing ke sql kan dan juga menampilkannya di app, yg misal

    user.add(userID: model.userID, username: model.username, password: model.password, level: model.level)

    edt { model.listUser << [userID: model.userID, username: model.username, password: model.password, level: model.level] }

    di bagian password itu di md5 dan ketika si user memasukkan data baru atau update data maka di appnya akan terlihat hasil md5 tersebut. kira2 bagaimana ya caranya?

  12. mas saya pake kode yg di https://gist.github.com/ikarius/299062 kok ga ada keluaran apa2 ya?

    code:
    def generateMD5(String s) {
    MessageDigest digest = MessageDigest.getInstance(“MD5”)
    digest.update(s.bytes);
    // saya ingin mereturn nilai string disini, kok malah kosong ya?
    return new BigInteger(1, digest.digest()).toString(16).padLeft(32, ‘0’)
    }

    apa saya yg salah buat kode?

  13. ah, ketemu..

    tinggal edit seperti ini saja 😀

    String generateMD5(String s) {
    MessageDigest digest = MessageDigest.getInstance(“MD5”)
    digest.update(s.bytes);
    return new BigInteger(1, digest.digest()).toString(16).padLeft(32, ‘0’)
    }

    maaf ngerepotin mas, hehe..

Tinggalkan Balasan ke Komang Hendra Santosa Batalkan balasan