Membuat User Control Untuk Windows Form Di C#


Happy New Year! Selamat memulai lembaran baru di tahun 2014. Saya memulai awal tahun ini dengan menyentuh Windows Forms Designer milik C#. Hal ini membuat saya langsung teringat kenangan sepuluh tahun silam dimana saya masih memakai Visual Basic 6. Saat itu saya baru pertama kali mencoba membuat program yang memiliki tampilan visual. Saya sebelumnya sudah terbiasa dengan Turbo Basic dan QBasic sehingga tidak sulit untuk beralih ke Visual Basic 6. Kenangan lama pun muncul kembali di benak saya setelah melihat Windows Forms Designer C#. Selain nostalgia, juga ada sedikit rasa trauma: kode program Visual Basic 6 yang dulu saya buat cenderung monolithic dan bercampur aduk menjadi satu tanpa arsitektur yang jelas. Maklum saja, saat itu saya masih pemula! Untuk membuat kode program yang bisa jalan tanpa pesan kesalahan saja sudah boleh dibilang perjuangan berat, mana sempat lagi memikirkan kerapian seperti layered atau MVC pattern. Dampaknya baru terasa di kemudian hari: program menjadi rumit dan sulit dimodifikasi.

Tapi diawal tahun 2014 ini, saya sudah bisa melihat Windows Forms Designer dari perspektif yang lebih dewasa dan lebih matang. Memang cukup sulit untuk menerapkan pola MVC disini, tapi selama ini saya terbiasa dengan sebuah view yang ditunjang oleh sebuah model baik itu view model ataupun domain model. Saya tidak ingin kembali membuat kode program monolithic yang bercampur aduk di satu tempat. Lalu apa yang harus saya lakukan? Salah satu alternatif yang terpikirkan oleh saya adalah membuat sebuah user control yang dipakai khusus untuk mengisi sebuah domain object. Tampilan pada aplikasi bisnis biasanya cukup kaku, sehingga tidak ada salahnya bila seluruh komponen di-generate secara otomatis. User control tersebut harus dapat menghasilkan Label dan TextBox berdasarkan setiap property yang ada di domain object melalui reflection. Saya akan menyebut user control tersebut sebagai SimpleObjectEditor.

Langkah pertama yang saya lakukan di Visual Studio 2010 adalah memilih menu File, New, Project…. Pada dialog yang muncul, saya memilih Visual C#, Windows Forms Control Library. Saya mengisi Name dengan SimpleObjectEditor dan men-klik tombol OK. Visual Studio 2010 akan menampilkan visual editor yang mirip seperti pada saat merancang Windows Forms, hanya saja wilayah kerjanya lebih terbatas. Saya kemudian mengubah nama UserControl1 menjadi SimpleObjectEditor. Kemudian, saya akan menampilkan kode program dengan men-klik kanan pada SimpleObjectEditor.cs dan memilih menu View Code seperti yang terlihat pada gambar berikut ini:

Menampilkan kode program untuk Form

Menampilkan kode program untuk Form

Secara kode program, class SimpleObjectEditor diturunkan dari class UserControl. Definisi kode programnya sebenarnya terletak pada dua file yang berbeda, yaitu SimpleObjectEditor.cs dan SimpleObjectEditor.Designer.cs. Bahasa C# membolehkan definisi class pada beberapa file terpisah asalkan terdapat keyword partial di class tersebut.

Saya akan menambahkan sebuah property bernama DomainObject yang akan mewakili domain object yang hendak di-edit oleh pengguna dengan menambahkan kode program berikut ini:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace SimpleObjectEditor
{
    public partial class SimpleObjectEditor : UserControl
    {
        private bool designMode = false;

        private INotifyPropertyChanged domainObject;

    [Description("Domain object yang akan terhubung dengan view editor ini")]
        public INotifyPropertyChanged DomainObject
        {
            get { return domainObject; }

            set
            {
                domainObject = value;
                UpdateComponents();
            }
        }        

        public SimpleObjectEditor()
        {
        if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
            {
                designMode = true;
            }
            InitializeComponent();
            UpdateComponents();
        }
    }
}

Pada kode program di atas, saya membuat property DomainObject dengan tipe INotifyPropertyChanged. Ini berarti setiap domain object yang dipakainya harus mengimplementasikan interface INotifyPropertyChanged. Tujuannya agar saya dapat melakukan binding dua arah: saat pengguna mengubah isi TextBox, maka nilai property di domain object akan diperbaharui. Begitu juga sebaliknya, saat pengguna mengubah nilai di domain object, maka isi TextBox juga harus diperbaharui.

Saya juga mendeklarasikan sebuah variabel designMode untuk menampung flag apakah UserControl ditampilkan saat perancangan atau saat runtime (program telah dijalankan). User control yang user friendly biasanya memiliki tampilan di perancangan yang hampir menyerupai runtime sehingga programmer tidak bingung. Karena ini hanya bahan pembelajaran, saya tidak akan menghabiskan banyak waktu untuk membuat kenyamanan perancangan.

Berikutnya, saya perlu men-implementasi-kan method UpdateComponents(), yang isinya seperti berikut ini:

...
public void UpdateComponents()
{
   Controls.Clear();

   if (designMode)
   {
      DummyForDesignMode();
   }
   else
   {
      if (DomainObject == null) return;

      CreateComponentsForDomainObject();

      DomainObject.PropertyChanged += (o, p) =>
      {
         foreach (TextBox txt in Controls.Find("input" + p.PropertyName, true))
         {
             txt.Text = DomainObject.GetType().GetProperty(p.PropertyName).GetValue(DomainObject, null).ToString();
         }

      };
   }                        
}
...

Pada kode program di atas, saya menggunakan sesuatu yang mirip seperti PropertyChangeListener di Java. Karena domain object harus mengimplementasikan interface INotifyPropertyChanged, maka saya mengharapkan bahwa setiap perubahan nilai pada property di domain object akan menghasilkan event PropertyChanged. Pada kode program di atas, setiap kali nilai property di domain object berubah, maka isi dari TextBox juga akan ikut berubah.

Method UpdateComponents() ini akan dipanggil setiap kali saya memberikan domain object baru pada SimpleObjectEditor. Tugasnya adalah menghasilkan rangkaian Label dan TextBox sesuai dengan setiap property yang ada di domain object. Bila sedang di-preview pada design mode (melalui visual editor), maka ia hanya menampilkan dummy seperti yang terlihat pada kode program berikut ini:

...
private void DummyForDesignMode()
{
    SuspendLayout();

    Label lbl = new Label();
    lbl.Top = 0;
    lbl.Text = "NamaLabel";
    lbl.Anchor = AnchorStyles.Left | AnchorStyles.Top;
    Controls.Add(lbl);

    TextBox txt = new TextBox();
    txt.Top = 0;
    txt.Left = 100;
    txt.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
    Controls.Add(txt);

    Label lbl2 = new Label();
    lbl2.Top = lbl.Height + 5;
    lbl2.Text = "NamaLabel";
    lbl2.Anchor = AnchorStyles.Left | AnchorStyles.Top;
    Controls.Add(lbl2);

    TextBox txt2 = new TextBox();
    txt2.Top = txt.Height + 5;
    txt2.Left = 100;
    txt2.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
    Controls.Add(txt2);

    ResumeLayout();
}
...

Pada saat program dijalankan, components untuk mengisi nilai property domain object akan dibuat oleh kode program yang memakai reflection seperti berikut ini:

...
private void CreateComponentsForDomainObject()
{
    int posisiBaris = 0;

    SuspendLayout();
    foreach (PropertyInfo propInfo in DomainObject.GetType().GetProperties())
    {
        Label lbl = new Label();
        lbl.Name = "label" + propInfo.Name;
        lbl.Text = propInfo.Name;
        lbl.Top = posisiBaris;
        lbl.Anchor = AnchorStyles.Left | AnchorStyles.Top;
        Controls.Add(lbl);

        TextBox txt = new TextBox();
        txt.Name = "input" + propInfo.Name;
        txt.Top = posisiBaris;
        txt.Left = 100;
        txt.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
        txt.Width = 200;
        txt.Tag = propInfo.Name;
        if (Type.GetTypeCode(propInfo.PropertyType) == TypeCode.Int32)
        {
            txt.KeyPress += (s, e) =>
            {
                if (!char.IsDigit(e.KeyChar) && !char.IsControl(e.KeyChar)) e.Handled = true;
            };

            txt.TextChanged += (s, e) =>
            {
                DomainObject.GetType().GetProperty(txt.Tag as string).SetValue(DomainObject,
                   int.Parse(txt.Text), null);                        

            };

        }
        else
        {
            txt.TextChanged += (s, e) =>
            {
                DomainObject.GetType().GetProperty(txt.Tag as string).SetValue(DomainObject, txt.Text, null);
            };
        }
        Controls.Add(txt);

        posisiBaris += lbl.Height + 5;
    }
    ResumeLayout(true);

}
...

Pada kode program di atas, bila property bertipe int (TypeCode.Int32) maka hanya digit angka yang boleh di-isi di TextBox yang dihasilkan (penjagaan dilakukan melalui event KeyPress). Tentu saja pada kenyataannya, saya perlu membuat kode program yang lebih fleksibel untuk mendukung tipe data lainnya seperti tanggal, list, dan sebagainya.

Setiap kali nilai TextBox berubah, maka nilai property bersangkutan di domain object akan diperbaharui. Hal ini terlihat dari kode program yang memakai di event TextChanged.

Sampai disini, saya sudah selesai membuat sebuah user control. Saya dapat menggunakan user control ini di berbagai proyek lainnya. Tapi tentu saja saya perlu mengujinya terlebih dahulu.

Untuk menguji user control yang telah dibuat, saya harus membuat sebuah proyek baru. Karena proyek baru ini nantinya hanya untuk keperluan pengujian, saya akan menambahkannya dalam solution yang sama. Saya mulai dengan memilih menu File, Add, New Project…. Pada kotak dialog yang muncul, saya memilih menu Visual C#, Windows Forms Application. Saya mengisi bagian Name dengan SimpleObjectEditorTest dan men-klik tombol OK.

Saya perlu terlebih dahulu menyiapkan domain class yang mewakili permasalahan bisnis yang sedang ditangani. Karena seluruh domain class memiliki bagian kode program yang sama yang berhubungan dengan INotifyPropertyChanged, maka saya akan membuat sebuah superclass yang dapat diturunkan oleh seluruh domain class nantinya. Saya men-klik kanan nama proyek SimpleObjectEditorTest dan memilih menu Add, Class…. Pada dialog yang muncul, saya mengisi Name dengan DomainModel. Lalu, saya menambahkan kode program berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace SimpleObjectEditorTest
{
    public class DomainModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Berikutnya, saya melakukan langkah yang sama untuk membuat class Mahasiswa yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace SimpleObjectEditorTest
{
    public class Mahasiswa : DomainModel
    {

        private string nim;

        public string Nim 
        {
            get { return nim; }
            set
            {
                if (value != nim)
                {
                    nim = value;
                    NotifyPropertyChanged("Nim");
                }
            }
        }

        private string nama;

        public string Nama 
        {
            get { return nama; }
            set
            {
                if (value != nama)
                {
                    nama = value;
                    NotifyPropertyChanged("Nama");
                }
            }
        }

        private int usia;

        public int Usia 
        {
            get { return usia; }
            set
            {
                if (value != usia)
                {
                    usia = value;
                    NotifyPropertyChanged("Usia");
                }
            }

        }

        public override string ToString()
        {
            return string.Format("Nim: {0}, Nama: {1}, Usia: {2}", Nim, Nama, Usia);
        }

    }
}

Saya juga mengulangi langkah yang sama untuk membuat class ItemPenjualan yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace SimpleObjectEditorTest
{
    class ItemPenjualan : DomainModel
    {

        private string namaBarang;

        public String NamaBarang
        {
            get { return namaBarang; }
            set
            {
                if (namaBarang != value)
                {
                    namaBarang = value;
                    NotifyPropertyChanged("NamaBarang");
                }
            }
        }

        public int jumlah;

        public int Jumlah
        {
            get { return jumlah; }
            set
            {
                if (jumlah != value)
                {
                    jumlah = value;
                    NotifyPropertyChanged("Jumlah");
                }
            }
        }

        public int harga;

        public int Harga
        {
            get { return harga; }
            set
            {
                if (harga != value)
                {
                    harga = value;
                    NotifyPropertyChanged("Harga");
                }
            }
        }

        public int diskonNominal;

        public int DiskonNominal
        {
            get { return diskonNominal; }
            set
            {
                if (diskonNominal != value)
                {
                    diskonNominal = value;
                    NotifyPropertyChanged("DiskonNominal");
                }
            }
        }

        public void PeriksaDiskon()
        {
            if (jumlah > 40)
            {
                diskonNominal += (int)(0.03 * harga);
                NotifyPropertyChanged("DiskonNominal");
            }
            if (harga > 1000000)
            {
                diskonNominal += 10000;
                NotifyPropertyChanged("DiskonNominal");
            }
        }

        public int Total()
        {
            return jumlah * harga - diskonNominal;
        }

    }
}

Bila saya men-klik kanan nama proyek SimpleObjectEditorTest dan memilih menu View Class Diagram, saya akan memperoleh hasil seperti pada gambar berikut ini:

Class diagram yang dihasilkan Visual Studio

Class diagram yang dihasilkan Visual Studio

Setelah memastikan bahwa domain object sudah dibuat dengan benar, saya memilih menu Build, Build Solution untuk men-compile seluruh proyek yang ada. Setelah itu, saya akan menemukan sebuah user control bernama SimpleObjectEditor di kiri atas Toolbox seperti yang terlihat pada gambar berikut ini:

Memilih user control saat di proyek pengujian

Memilih user control saat di proyek pengujian

Saya kemudian men-drag user control tersebut ke tampilan designer dan juga menambahkan beberapa Button sehingga terlihat seperti pada gambar berikut ini:

Merancang form pengujian

Merancang form pengujian

Pada saat perancangan, SimpleObjectEditor hanya menampilkan dua baris dummy saja (hasil dari method DummyForDesignMode()). Pada saat dijalankan, tampilannya akan disesuaikan dengan isi property DomainObject. Pada saat ini, saya hanya dapat mengisi property DomainObject melalui kode program saja.

Property DomainObject  di Properties window

Property DomainObject di Properties window

Selanjutnya, saya menambahkan event handler untuk Button dan mengubah kode program untuk Form1 menjadi seperti berikut ini:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace SimpleObjectEditorTest
{
    public partial class Form1 : Form
    {
        private Mahasiswa mahasiswa;

        private ItemPenjualan itemPenjualan;

        public Form1()
        {
            InitializeComponent();            
        }

        private void btnNewMahasiswa_Click(object sender, EventArgs e)
        {
            mahasiswa = new Mahasiswa();
            simpleObjectEditor1.DomainObject = mahasiswa;
        }

        private void btnLihatMahasiswa_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Output: " + mahasiswa);
        }

        private void btnSetNamaMahasiswa_Click(object sender, EventArgs e)
        {
            mahasiswa.Nama = "The Solid Snake";
        }

        private void btnNewItemPenjualan_Click(object sender, EventArgs e)
        {
            itemPenjualan = new ItemPenjualan();
            simpleObjectEditor1.DomainObject = itemPenjualan;
        }

        private void btnLihatTotalItemPenjualan_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Output: " + itemPenjualan.Total());
        }

        private void btnPanggilMethodItemPenjualan_Click(object sender, EventArgs e)
        {
            itemPenjualan.PeriksaDiskon();
        }
    }
}

Perhatikan bahwa pada kode program di atas, saya tidak mengakses komponen di formsecara langsung melainkan melalui domain object. Saya juga tidak menuliskan kode program business logic di dalam event handler karena business logic sudah ada di domain object. Yang saya lakukan hanya memanggil method yang saya butuhkan dari domain object. Hal ini jauh lebih rapi dan lebih mudah dikelola bila dibandingkan dengan seluruh kode program bercampur aduk di event handler (seperti yang saya lakukan saat memakai Visual Basic 6 sepuluh tahun silam).

Agar dapat menjalankan proyek SimpleObjectEditorTest, saya perlu men-klik kanan nama proyek tersebut dan memilih Set as Startup Project.

Pada saat saya men-klik tombol Edit new Mahasiswa(), SimpleObjectEditor akan diasosiasikan dengan sebuah object Mahasiswa baru yang masih belum ada isinya. Ia akan menampilkan Label dan TextBox yang mewakili seluruh property yang ada di class tersebut, seperti yang terlihat pada gambar berikut ini:

Mengasosiasikan SimpleObjectEditor dengan sebuah objek

Mengasosiasikan SimpleObjectEditor dengan sebuah objek

Pada saat saya mengisi nilai TextBox yang ada, property milik object mahasiswa secara otomatis akan diperbaharui sesuai dengan apa yang saya ketik, seperti yang terlihat pada gambar berikut ini:

Binding dari view ke model

Binding dari view ke model

Sebaliknya, bila saya mengubah salah satu property milik object mahasiswa, maka TextBox yang berhubungan dengan property tersebut secara otomatis akan diperbaharui nilainya, seperti yang terlihat pada gambar berikut ini:

Binding dari model ke view

Binding dari model ke view

Kode program business logic adalah kode program yang berhubungan dengan peraturan atau kebutuhan bisnis yang harus di-implementasi-kan. Kode program tersebut idealnya harus berada di domain object. Contoh business logic adalah kode program yang menghitung total penjualan, memeriksa persyaratan diskon, menghitung indeks prestasi mahasiswa, dan sebagainya. Aktifitas tersebut berbeda dari kode program dengan logika lainnya seperti memperbaharui layar, mencetak, mengirim email, melakukan koneksi database, dan sebagainya. Karena business logic berada di domain object, maka event handler setiap Button (saya melihatnya seperti controller) hanya perlu memanggil method yang ada di domain object, seperti yang terlihat pada gambar berikut ini:

Memanggil method di domain object

Memanggil method di domain object

Kode program business logic juga dapat mengubah nilai Property yang ada. Sebagai contoh, pada class ItemPenjualan, bila jumlah penjualan item yang dijual lebih dari 40, maka nilai dari property DiskonNominal akan ditambah sebesar 0.03 % dari harga barang. Perubahan nilai property akibat pemanggilan method di domain object juga akan langsung memperbaharui TextBox bersangkutan, seperti yang terlihat pada gambar berikut ini:

Memanggil method di domain object yang mengubah nilai property

Memanggil method di domain object yang mengubah nilai property

Perihal Solid Snake
I'm nothing...

3 Responses to Membuat User Control Untuk Windows Form Di C#

  1. Ping-balik: Memakai Type Converter Di User Control C# | The Solid Snake

  2. trash sampah mengatakan:

    Mastah… tutorialnya mangstab sekali…
    mastah ane ada dua pertanyaan
    [pertama]
    untuk text labelnya bisa ngga kita buat tersendiri didalam domain modelnya.
    jadi “NamaBarang” nya bisa di tulis “Nama Barang”.
    [kedua]
    domain modelnya bisa ngga kita generate dari/berdasarkan field2 tabel dari database?

    • Solid Snake mengatakan:

      Pertama, bisa. Caranya dengan mengubah method CreateComponentsForDomainObject() di bagian lbl.Text = propInfo.Name; dengan kode program yang memberikan spasi atau output lainnya yang diharapkan.

      Kedua, untuk men-generate domain model dari database, sebaiknya gunakan Entity Framework (EF).

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: