Apa itu Assembly Di .NET?

Unit deployment pada Java adalah sebuah file JAR (atau WAR/EAR untuk Java EE). Pada .NET, unit deployment disebut dengan assembly (ini sama sekali tidak ada hubungannya dengan bahasa mesin). Bentuk fisiknya dapat berupa file EXE atau DLL. Berbeda dengan file EXE/DLL pada umumnya, assembly untuk .NET tidak langsung berisi bahasa mesin melainkan IL (Intermediate Language) dan metadata. Bila dibandingkan dengan Java, file JAR juga tidak langsung berisi bahasa mesin melainkan bytecode.

Untuk melihat informasi sebuah assembly, saya dapat menggunakan tool ILDASM.EXE bawaan Visual Studio:

Mencari tool IL Disassembler

Mencari tool IL Disassembler

Gambar berikut ini memperlihatkan contoh tampilan ILDASM.EXE:

Memakai IL Disassembler

Memakai IL Disassembler

Pada gambar di atas terlihat bahwa hasil reverse engineering dari method main() bukanlah dalam bentuk bahasa mesin x86 melainkan dalam bahasa perantara Intermediate Language (IL). Selain itu, gambar di atas juga memperlihatkan bahwa sebuah assembly (dalam bentuk file EXE/DLL) memiliki informasi metadata yang disebut sebagai assembly manifest. Pada file JAR untuk Java, informasi metadata seperti ini diletakkan di folder META-INF di dalam file JAR tersebut.

Untuk mengatur nilai pada assembly manifest, saya dapat mengubah file AssemblyInfo.cs yang dihasilkan oleh Visual Studio secara otomatis pada saat saya membuat proyek baru, seperti yang terlihat pada gambar berikut ini:

AssemblyInfo.cs yang dihasilkan Visual Studio

AssemblyInfo.cs yang dihasilkan Visual Studio

Untuk mendapatkan informasi tentang metadata di assembly melalui kode program, saya dapat memakai class Assembly seperti pada contoh kode program berikut ini:

using System;
using System.Reflection;

namespace Latihan
{
    class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            AssemblyName assemblyName = assembly.GetName();
            Console.WriteLine("Nama lengkap assembly ini adalah {0}", assembly.FullName);
            Console.WriteLine("Nama: {0}", assemblyName.Name);
            Console.WriteLine("Versi: {0}", assemblyName.Version);
            Console.WriteLine("Codebase: {0}", assemblyName.CodeBase);
            Console.ReadLine();
        }
    }
}

Kode program di atas akan menghasilkan output seperti:

Nama lengkap assembly ini adalah Latihan, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Nama: Latihan
Versi: 1.0.0.0
Codebase: file:///C:/Latihan/bin/Debug/Latihan.EXE

Saya juga dapat membuat sebuah application manifest di lokasi yang sama dengan AssemblyInfo.cs. Salah satu kegunaannya adalah memunculkan dialog UAC setiap kali program dijalankan (sehingga program dijalankan sebagai administrator). Caranya adalah dengan men-klik kanan nama proyek dan memilih menu Add, New Item. Pada dialog yang muncul, saya memilih Application Manifest File dan men-klik tombol Add. Saya kemudian mengubah isi file app.manifest menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</asmv1:assembly>

Sekarang, bila saya menjalankan program dengan men-double click assembly (file EXE), saya akan memperoleh tampilan seperti pada gambar berikut ini:

Dialog UAC muncul setiap kali program dijalankan

Dialog UAC muncul setiap kali program dijalankan

Untuk melihat daftar assembly yang sedang aktif di memori, saya dapat membuat kode program seperti berikut ini:

using System;
using System.Reflection;

namespace Latihan
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Daftar Assembly Yang Sedang Dipakai:\n");
            foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
            {
                AssemblyName asmName = asm.GetName();
                Console.WriteLine("{0} {1}", asmName.Name, asmName.Version);
            }
            Console.ReadLine();
        }
    }
}

Contoh tampilan program di atas akan terlihat seperti berikut ini:

Daftar Assembly Yang Sedang Dipakai:

mscorlib 4.0.0.0
Microsoft.VisualStudio.HostingProcess.Utilities 10.0.0.0
System.Windows.Forms 4.0.0.0
System.Drawing 4.0.0.0
System 4.0.0.0
Microsoft.VisualStudio.HostingProcess.Utilities.Sync 10.0.0.0
Microsoft.VisualStudio.Debugger.Runtime 10.0.0.0
vshost32 10.0.0.0
System.Core 4.0.0.0
System.Xml.Linq 4.0.0.0
System.Data.DataSetExtensions 4.0.0.0
Microsoft.CSharp 4.0.0.0
System.Data 4.0.0.0
System.Xml 4.0.0.0
Latihan 1.0.0.0

Untuk membaca ‘bytecode’ yang berisi IL untuk sebuah method, saya dapat menggunakan kode program seperti berikut ini:

using System;
using System.Reflection;

namespace Latihan
{
    class Program
    {
        static void Main(string[] args)
        {
            MethodInfo mi = typeof(Console).GetMethod("ReadLine");
            byte[] il = mi.GetMethodBody().GetILAsByteArray();
            foreach (byte b in il) 
            {
                Console.Write("{0} ", b.ToString);
            }
            Console.ReadLine();
        }
    }
}
Iklan

Memahami Correlation Di Windows Workflow Foundation (WF)

Pada saat memakai WF untuk menyediakan layanan web service, correlation adalah sesuatu yang kerap dipakai. Correlation berkaitan dengan activity Receive dimana sebuah activity Receive akan diterjemahkan menjadi sebuah web service yang dapat dipanggil oleh client. Sebagai contoh, saya akan membuat sebuah proyek baru di Visual Studio 2010 dengan template Visual C# WCF Workflow Service Application.

Saya mulai dengan menghapus template workflow yang sudah ada dan membuat workflow sederhana baru untuk proses registrasi yang berisi activity ReceiveAndSendReply, seperti yang terlihat pada gambar berikut ini:

Membuat sebuah activity ReceiveAndSendReply

Membuat sebuah activity ReceiveAndSendReply

Saya membutuhkan beberapa variabel untuk menampung informasi registrasi, seperti yang terlihat pada gambar berikut ini:

Daftar variabel dalam workflow

Daftar variabel dalam workflow

Untuk menentukan parameter yang dibutuhkan pada saat service Daftar() dipanggil oleh client, saya men-klik tulisan View message… pada bagian Content dan mengisi dialog yang muncul agar terlihat seperti berikut ini:

Parameter untuk activity Receive Daftar()

Parameter untuk activity Receive Daftar()

Saya juga melakukan langkah yang sama untuk menentukan hasil kembalian dari service ini, dengan mengisi dialog yang muncul seperti berikut ini:

Nilai yang dikembalikan oleh service Daftar()

Nilai yang dikembalikan oleh service Daftar()

Selanjutnya, saya menambahkan sebuah activity ReceiveAndSendReply lagi dengan nama operasi Aktivasi. Saya mengisi parameternya seperti pada gambar berikut ini:

Parameter untuk activity Receive Aktivasi()

Parameter untuk activity Receive Aktivasi()

Saya mengisi content definition untuk hasil kembalian dari servce ini seperti yang terlihat pada gambar berikut ini:

Nilai yang dikembalikan oleh service Aktivasi()

Nilai yang dikembalikan oleh service Aktivasi()

Pada workflow sederhana ini, pengguna pertama kali memanggil service Daftar() untuk melakukan proses registrasi. Anggap saja ia kemudian menerima email beserta kode aktivasi yang akan memanggil service Aktivasi(). Workflow-nya akan terlihat seperti pada gambar berikut ini:

Hasil rancangan workflow

Hasil rancangan workflow

Bila saya menjalankan aplikasi ini, WcfClientTester.exe otomatis akan dijalankan dengan tampilan seperti yang terlihat pada gambar berikut ini:

Workflow services yang tersedia

Workflow services yang tersedia

Bila saya mencoba memanggil salah satu dari service Daftar() atau Aktivasi(), saya akan memperoleh pesan kesalahan seperti berikut ini:

There is no context attached to the incoming message for the service and the current operation is not marked with “CanCreateInstance = true”. In order to communicate with this service check whether the incoming binding supports the context protocol and has a valid context initialized.

Mengapa demikian? Apa itu workflow instance? Sama seperti satu atau lebih object adalah instance dari sebuah class, bisa terdapat satu atau lebih instance dari sebuah workflow. Sebagai contoh, proses registrasi untuk email solidsnake@gmail.com adalah instance yang berbeda dari proses registrasi untuk email liquidsnake@gmail.com, walaupun mereka berdasarkan workflow yang sama.

Bila instance dari object akan dibuat dengan keyword new, lalu kapan instance dari workflow akan dibuat? Jawabannya adalah pada saat sebuah activity dengan nilai property CanCreateInstance=true dipanggil. Biasanya ini adalah activity Receive yang didefinisikan pertama kali. Oleh sebab itu, saya mengatur nilai property CanCreateInstance menjadi true untuk activity tersebut seperti yang terlihat pada gambar berikut ini:

Mengatur nilai property CanCreateInstance

Mengatur nilai property CanCreateInstance

Sekarang, saya dapat memanggil operasi Daftar() dengan baik seperti yang terlihat pada gambar berikut ini:

Memanggil service Daftar()

Memanggil service Daftar()

Lalu bagaimana dengan operasi Aktivasi()? Pesan kesalahan tetap akan muncul. Tapi permasalahannya adalah bila saya memberi nilai CanCreateInstance=true pada activity tersebut, maka ia akan membuat instance workflow baru sama halnya seperti Daftar(). Akibatnya, Aktivasi() dapat dipanggil sebelum Daftar(). Padahal, harusnya Aktivasi() adalah bagian dari instance yang diciptakan oleh Daftar(). Permasalahan ini mirip seperti permasalahan session di aplikasi web. Apa yang harus saya lakukan agar operasi dapat dipanggil secara berurut?

Agar bisa mengenali setiap instance, WF memakai correlation untuk memetakan setiap request ke instance yang bersangkutan. Pada contoh ini, saya akan menggunakan content-based correlation dimana saya perlu mencari sebuah nilai unik yang menjadi pengenal instance workflow. Pada workflow proses registrasi yang saya buat, email adalah kandidat yang tepat untuk dipakai sebagai identitas sebuah instance workflow karena ia selalu unik untuk setiap instance.

Saya perlu melakukan inisialisasi nilai correlation dipakai sebagai pengenal workflow instance dengan mengisi nilai property CorrelationInitializers di activity Receive untuk operasi Daftar() dengan nilai seperti berikut ini:

Melakukan inisialisasi pengenal workflow instance

Melakukan inisialisasi pengenal workflow instance

Berikutnya, saya perlu mengatur correlation untuk activity Receive untuk service Aktivasi(). Caranya adalah dengan men-klik tombol di sisi kanan property CorrelatesOn dan mengisi dialog yang muncul dengan informasi seperti pada gambar berikut ini:

Mengatur nilai correlation

Mengatur nilai correlation

Sekarang, bila saya mengerjakan operasi Daftar() sebelum Aktivasi() dengan email yang sama, maka operasi Aktivasi() akan dikerjakan dengan baik, seperti yang terlihat pada gambar berikut ini:

Memanggil service Aktivasi()

Memanggil service Aktivasi()

Tapi bila saya memanggil Aktivasi() dengan sebuah email berbeda yang belum didaftarkan oleh Daftar(), maka saya akan memperoleh pesan kesalahan seperti berikut ini:

The execution of an InstancePersistenceCommand was interrupted because the instance key ‘6ec2e27e-9585-0253-0eb0-707d3d08dfbe’ was not associated to an instance. This can occur because the instance or key has been cleaned up, or because the key is invalid. The key may be invalid if the message it was generated from was sent at the wrong time or contained incorrect correlation data.

Memakai WF Bersama Dengan WCF: Workflow Services

Pada artikel sebelumnya, saya menggunakan class WorkflowApplication untuk menjalankan workflow. Sebagai alternatif lain, WF juga menyediakan class WorkflowServiceHost yang memakai WCF. Dengan menggunakan class tersebut, saya dapat mengimplementasikan message exchange pattern (MEP) seperti Datagram (komunikasi satu arah dari client ke server), Request-Response (client menghubungi server dan server menjawab client), dan Duplex (komunikasi dua arah). Caranya adalah dengan menggunakan activity Send, SendReply, Receive atau ReceiveReply saat merancang workflow.

Sebagai latihan, saya akan mengubah workflow restoran yang saya buat sebelumnya menjadi aplikasi n-tier (multi-tier) yang memakai WCF. Presentation tier terdiri atas waitress.exe, kasir.exe, dan koki.exe. Ketiganya dibuat dengan WPF. Pada kasus yang lebih realistis, mungkin saja mereka bisa berupa aplikasi web untuk pemesanan dan aplikasi yang dijalankan di perangkat khusus (embedded). Saya akan membuat 2 business logic tier yaitu sebuah services tier yang memakai workflow dan sebuah data access tier untuk operasi database. Data tier-nya sendiri adalah sebuah database Microsoft SQL Server 2008. Sesungguhnya konfigurasi seperti ini terlalu rumit (overkill) untuk sebuah permasalahan yang sederhana, tetapi setidaknya saya dapat memperoleh bayangan seperti apa yang disebut arsitektur n-tier (multi-tier).

Dengan demikian, hasil akhir yang saya peroleh adalah sebuah solution di Visual Studio yang terdiri atas 5 proyek dimana terdapat 3 proyek WPF (presentation tier), 1 proyek WCF (business logic tier untuk services), dan 1 proyek WF (business logic tier untuk data access).

Membuat Domain Model

Kode program pada proyek business logic tier dan services tier akan memakai domain model yang sama. Boleh dibilang domain model adalah ‘inti sari’ dari permasalahan bisnis sehingga ia bisa saja dibutuhkan diberbagai tempat di bussiness logic tier. Oleh sebab itu, saya akan membuat sebuah proyek baru yang khusus untuk menampung domain class dan persistence layer-nya sehingga mereka bisa dipakai ulang di proyek lainnya.

Saya men-klik kanan nama solution dan memilih menu Add, New Project…. Pada dialog yang muncul, saya memilih Class Library dan mengisi nama proyek dengan Common. Data akan disimpan ke dalam database SQL Server dengan menggunakan Entity Framework 6. Oleh sebab itu, saya memilih Project, Manage NuGet Packages… untuk men-install Entity Framework 6 pada proyek ini.

Berikutnya, saya akan membuat domain class yang isinya terlihat seperti berikut ini:

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

namespace Common
{
    public class Order
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        public DateTime Tanggal { get; set; }

        public String NamaPelanggan { get; set; }

        public decimal Harga { get; set; }

        public Guid WorkflowId { get; set; }

        public virtual List<RiwayatOrder> ListRiwayatOrder { get; set; }

        public String StatusTerakhir { get; set; }

        public void TambahRiwayat(RiwayatOrder riwayat)
        {
            ListRiwayatOrder.Add(riwayat);
            StatusTerakhir = riwayat.Status;
        }
    }

    public abstract class RiwayatOrder
    {

        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }

        public DateTime Tanggal { get; set; }

        public String Nama { get; set; }

        public abstract String Status { get; }        
    }

    public class RiwayatOrderMulaiDimasak : RiwayatOrder
    {
        public override string Status
        {
            get { return "MulaiDimasak"; }
        }
    }

    public class RiwayatOrderSelesaiDimasak : RiwayatOrder
    {
        public override string Status
        {
            get { return "SelesaiDimasak"; }
        }
    }

    public class RiwayatOrderPelangganMembayar : RiwayatOrder
    {
        public override string Status
        {
            get { return "PelangganMembayar"; }
        }

        public String JenisPembayaran { get; set; }
    }
}

Saya juga akan membuat sebuah persistence context yang isinya seperti berikut ini:

using System.Data.Entity;

namespace Common
{
    public class LatihanWorkflowContext : DbContext
    {
        public LatihanWorkflowContext() : base("LatihanWorkflow") { }

        public DbSet<Order> DaftarOrder { get; set; }

        public DbSet<RiwayatOrder> RiwayatOrder { get; set; }
    }    
}

Model di atas akan menghasilkan tabel di Microsoft SQL Server seperti yang terlihat pada gambar berikut ini:

Struktur tabel yang dihasilkan dari domain model

Struktur tabel yang dihasilkan dari domain model

Saya juga akan menambahkan class untuk data transfer object (DTO) pada proyek ini. Untuk itu, saya menambahkan referensi System.Runtime.Serialization pada proyek ini. Setelah itu, saya membuat sebuah class baru yang isinya terlihat seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace Common
{

    [DataContract]
    public class OrderSimpan
    {
        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String NamaPelanggan { get; set; }

        [DataMember]
        public decimal Harga { get; set; }

        [DataMember]
        public Guid WorkflowId { get; set; }
    }

    [DataContract]
    public class OrderRequest
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String NamaPelanggan { get; set; }

        [DataMember]
        public decimal Harga { get; set; }

        [DataMember]
        public Guid WorkflowId { get; set; }

        [DataMember]
        public List<RiwayatOrderRequest> ListRiwayatOrder { get; set; }

        [DataMember]
        public String StatusTerakhir { get; set; }
    }

    [DataContract]
    public class RiwayatOrderSimpan
    {
        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String Nama { get; set; }

        [DataMember]
        public String Status { get; set; }

        [DataMember]
        public String JenisPembayaran { get; set; }
    }

    [DataContract]
    public class RiwayatOrderRequest
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public DateTime Tanggal { get; set; }

        [DataMember]
        public String Nama { get; set; }

        [DataMember]
        public String Status { get; set; }

        [DataMember]
        public String JenisPembayaran { get; set; }
    }
}

Membuat Data Access Services

Saya akan membuat sebuah proyek baru yang mewakili data access services dengan template Visual C# Console Application yang saya beri nama DataServices. Karena saya akan memakai WCF, maka saya perlu menambahkan referensi System.ServiceModel pada proyek dengan men-klik kanan nama proyek dan memilih Add Reference…. Selain itu, saya juga perlu menambahkan referensi ke proyek Common melalui tab Projects di dialog Add Reference.

Saya perlu mempublikasikan layanan web services untuk melakukan operasi database dengan menggunakan WCF. Oleh sebab itu, saya membuat sebuah class baru dengan nama Services yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
using Common;

namespace DataServices
{
    [ServiceContract]
    public class Services
    {

        [OperationContract]
        public int BuatOrder(OrderSimpan orderSimpan)
        {
            Order order = new Order()
            {
                Tanggal = orderSimpan.Tanggal,
                NamaPelanggan = orderSimpan.NamaPelanggan,
                Harga = orderSimpan.Harga,
                WorkflowId = orderSimpan.WorkflowId
            };
            order.StatusTerakhir = "Dipesan";
            using (var db = new LatihanWorkflowContext())
            {
                db.DaftarOrder.Add(order);
                db.SaveChanges();
            }
            return order.Id;
        }

        [OperationContract]
        public Boolean TambahRiwayatOrder(int orderId, RiwayatOrderSimpan riwayatOrderSimpan)
        {
            using (var db = new LatihanWorkflowContext())
            {
                var hasilQuery = from o in db.DaftarOrder
                              where o.Id == orderId
                              select o;
                Order order = hasilQuery.FirstOrDefault();
                if (order != null)
                {
                    RiwayatOrder riwayatOrder = null;
                    if (riwayatOrderSimpan.Status.Equals("MulaiDimasak"))
                    {
                        riwayatOrder = new RiwayatOrderMulaiDimasak();
                    }
                    else if (riwayatOrderSimpan.Status.Equals("SelesaiDimasak"))
                    {
                        riwayatOrder = new RiwayatOrderSelesaiDimasak();
                    }
                    else if (riwayatOrderSimpan.Status.Equals("PelangganMembayar"))
                    {
                        riwayatOrder = new RiwayatOrderPelangganMembayar();
                        (riwayatOrder as RiwayatOrderPelangganMembayar).JenisPembayaran =
                            riwayatOrderSimpan.JenisPembayaran;
                    }
                    else
                    {
                        return false;
                    }

                    riwayatOrder.Nama = riwayatOrderSimpan.Nama;
                    riwayatOrder.Tanggal = riwayatOrderSimpan.Tanggal;

                    order.TambahRiwayat(riwayatOrder);
                    db.SaveChanges();

                    return true;
                }
                return false;
            }
        }

        [OperationContract]
        public List<OrderRequest> LihatSemuaOrder(String statusTerakhir)
        {
            var result = new List<OrderRequest>();
            using (var db = new LatihanWorkflowContext())
            {
                var hasilQuery = from o in db.DaftarOrder
                                 where o.StatusTerakhir == statusTerakhir
                                 select o;

                foreach (Order order in hasilQuery)
                {
                    OrderRequest orderRequest = new OrderRequest()
                    {
                        Id = order.Id,
                        Tanggal = order.Tanggal,
                        NamaPelanggan = order.NamaPelanggan,
                        Harga = order.Harga,
                        WorkflowId = order.WorkflowId,
                        StatusTerakhir = order.StatusTerakhir
                    };
                    orderRequest.ListRiwayatOrder = new List<RiwayatOrderRequest>();
                    foreach (RiwayatOrder riwayatOrder in order.ListRiwayatOrder)
                    {
                        RiwayatOrderRequest riwayatOrderRequest = new RiwayatOrderRequest()
                        {
                            Id = riwayatOrder.Id,
                            Tanggal = riwayatOrder.Tanggal,
                            Nama = riwayatOrder.Nama,
                            Status = riwayatOrder.Status
                        };
                        if (riwayatOrder is RiwayatOrderPelangganMembayar)
                        {
                            riwayatOrderRequest.JenisPembayaran = 
                                (riwayatOrder as RiwayatOrderPelangganMembayar).JenisPembayaran;
                        }
                        orderRequest.ListRiwayatOrder.Add(riwayatOrderRequest);
                    }
                    result.Add(orderRequest);
                }
            }
            return result;
        }

    }
}

Terlihat cukup kompleks, bukan? Selain karena penggunaan DTO, hal ini juga ditambah lagi dengan ketidaksesuaian antara object oriented dan service oriented. Pada service oriented, saya harus menyediakan layanan tanpa memakai konsep OOP seperti polymorphism dan inheritance. Sementara itu, saya merancang business logic dalam bentuk OOP di domain class.

Langkah terakhir adalah memakai ServiceHost dari WCF untuk menjadikan proyek ini server yang menyediakan layanan web services. Untuk itu, saya mengubah kode program Program.cs menjadi seperti berikut ini:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace DataServices
{
    class Program
    {
        static void Main(string[] args)
        {            
            ServiceHost selfHost = new ServiceHost(typeof(Services), new Uri("http://localhost:8080/LatihanMultiTier"));
            try
            {
                selfHost.AddServiceEndpoint(typeof(Services), new WSHttpBinding(), "DataServices");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;                
                selfHost.Description.Behaviors.Add(smb);
                selfHost.Open();
                Console.WriteLine("Data services sudah aktif dan siap melayani request...");
                Console.ReadLine();
                selfHost.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Telah terjadi kesalahan: {0}", ex.Message);
                selfHost.Abort();
            }
            Console.ReadLine();
        }
    }
}

Sebelum mencoba services yang disediakan oleh program ini, saya terlebih dahulu menjalankan Visual Studio sebagai administrator. Saya kemudian membuka command prompt untuk mengerjakan WcfTestClient.exe guna menguji setiap web services yang disediakan oleh proyek ini. Saya juga memastikan isi tabel diambil/diperbaharui dengan baik. Setelah yakin semuanya dalam keadaan baik, saya kemudian lanjut ke proyek berikutnya.

Membuat Workflow Services

Saya mulai dengan membuat sebuah proyek baru untuk workflow services dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Pada dialog yang muncul, saya memilih Workflow Console Application dan mengisi nama proyek dengan WorkflowServices. Saya juga perlu menambahkan referensi ke proyek Common dengan men-klik kanan nama proyek dan memilih menu Add Reference….

Proyek ini akan memanggil web services yang dibuat di proyek DataServices. Oleh sebab itu, saya perlu menambahkan referensi ke layanan yang dipublikasikan oleh proyek tersebut. Caranya adalah dengan men-klik kanan nama proyek WorkflowServices dan memilih menu Add Service Reference…. Pada dialog yang muncul, saya mengisi alamat dengan http://localhost:8080/LatihanMultiTier dan men-klik tombol Go (pastikan bahwa aplikasi pada proyek DataServices sudah dijalankan terlebih dahulu!). Setelah mengisi nama namespace dengan DataServices, saya kemudian men-klik tombol OK untuk mulai men-import web services. Setelah selesai, saya menekan tombol F6 untuk men-build ulang proyek ini.

Setelah ini, saya akan mempersiapkan custom activity untuk dipakai dalam workflow designer. Saya akan mulai dengan membuat sebuah class baru dengan nama GetWorkflowInstanceId yang isinya adalah seperti berikut ini (diambil dari WF Sample dari Microsoft):

//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------

using System;
using System.Activities;

namespace Microsoft.Samples.Activities.Statements
{

    /// <summary>
    /// This activity returns the instance id of the workflow it is executing in
    /// 
    /// We choose CodeActivity as the base type because we need access to the CodeActivityContext for getting access to the workflow instance ID.
    /// 
    /// This activity returns a System.Guid for the instance ID
    /// </summary>
    /// 
    public sealed class GetWorkflowInstanceId : CodeActivity<Guid>
    {
        protected override Guid Execute(CodeActivityContext context)
        {
            return context.WorkflowInstanceId;
        }
    }
}

Saya membutuhkan custom activity di atas karena WorkflowInstanceId harus diperoleh dari sebuah CodeActivityContext. Semoga saja suatu saat nanti WF akan menyediakan cara lain yang lebih user-friendly tanpa harus membuat custom activity.

Saya kemudian mengubah nama Workflow1.xaml menjadi RestoranServices.xaml. Saya juga turut mengubah nama class untuk workflow ini dengan mengubah nilai atribut x:Class menjadi WorkflowServices.RestoranServices. Pada saat membuka workflow designer, saya akan menemukan custom activity yang saya tambahkan beserta dengan activity untuk memanggil layanan web services yang sudah saya buat di proyek DataServices sebelumnya, seperti yang terlihat pada gambar berikut ini:

Custom activity yang dapat dipakai di workflow designer

Custom activity yang dapat dipakai di workflow designer

Garis besar rancangan workflow yang saya buat terlihat seperti pada gambar berikut ini:

Rancangan workflow secara garis besar

Rancangan workflow secara garis besar

Sequence Kasir Membuat Order Baru terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Kasir membuat order baru*

Rancangan untuk sequence *Kasir Membuat Order Baru*

Activity Receive akan menunggu client memanggil web service BuatOrder(). Activity ini memiliki property CanCreateInstance dengan nilai true sehingga ia adalah activity yang harus dipanggil pertama kali untuk membuat instance workflow baru. Activity SendReplyToReceive akan mengembalikan hasil ke client yang sebelumnya telah memanggil web service BuatOrder(). Saya memakai activity InitializeCorrelation untuk memakai id dari order yang dibuat sebagai nilai pengenal pada pemanggil web service berikutnya (anggap saja sepeti session di web).

Sequence Koki Mulai Memasak terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Koki Mulai Memasak*

Rancangan untuk sequence *Koki Mulai Memasak*

Sequence ini juga memiliki activity Receive dengan nama MulaiDimasak. Saya mengatur nilai correlation pada activity ini sehingga ia harus dipanggil setelah activity Receive sebelumnya dipanggil.

Sequence Koki Selesai Memasak tidak jauh berbeda dengan sebelumnya, terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Koki Selesai Memasak*

Rancangan untuk sequence *Koki Selesai Memasak*

Terakhir, sequence Pelanggan Membayar terlihat seperti pada gambar berikut ini:

Rancangan untuk sequence *Pelanggan Membayar*

Rancangan untuk sequence *Pelanggan Membayar*

Seluruh activity Receive setelah yang pertama (yang memiliki property CanCreateInstance dengan nilai true) harus memiliki nilai correlation sehingga mereka bisa berpartisipasi dalam sebuah workflow instance (bayangkan session!) yang sama. Tanpa nilai correlation, WF tidak dapat menentukan urutan eksekusi karena web service bisa dipanggil dari berbagai client yang berbeda dan urutan yang berbeda.

Saya kemudian membuat kode program yang memakai WorkflowServiceHost untuk membuat sebuah server web services bagi workflow ini dengan mengubah kode program Program.cs menjadi seperti berikut ini:

using System;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

namespace WorkflowServices
{

    class Program
    {
        static void Main(string[] args)
        {
            RestoranServices restoranServices = new RestoranServices();
            WorkflowServiceHost host = new WorkflowServiceHost(restoranServices, 
                new Uri("http://localhost:8181/LatihanMultiTier/WorkflowServices"));                                    
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);
            host.AddDefaultEndpoints();
            host.Open();
            Console.WriteLine("Workflow services sudah aktif dan siap melayani request...");            
            Console.ReadLine();
            host.Close();
        }
    }
}

Saya perlu memastikan bahwa port yang dipakai oleh server ini berbeda dengan port yang dipakai oleh server data services bila ingin menjalankan kedua server tersebut pada komputer yang sama.

Berikutnya, sebelum melakukan pengujian, saya perlu menjalankan proyek DataServices dan WorkflowServices (karena proyek WorkflowServices bergantung pada layanan yang disediakan proyek DataServices). Setelah itu, saya menguji web services yang disediakan oleh proyek WorkflowServices dengan menggunakan WcfTestClient.exe seperti yang terlihat pada gambar berikut ini:

Layanan web services yang disediakan oleh proyek *WorkflowServices*

Layanan web services yang disediakan oleh proyek *WorkflowServices*

Membuat User Interface Untuk Waitress

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Waitress. Proyek ini akan memanggil web services yang disediakan oleh proyek WorkflowServices. Oleh sebab itu, saya menambahkan referensi ke layanan yang disediakan oleh proyek tersebut (setelah sebelumnya menjalankan proyek WorkflowServices) dengan men-klik kanan nama proyek dan memilih menu Add Service Reference…. Saya kemudian mengisi kotak dialog yang muncul agar terlihat seperti pada gambar berikut ini dan men-klik tombol OK:

Menambahkan service reference

Menambahkan service reference

Saya kemudian membuat sebuah class baru untuk mewakili view model yang isinya seperti berikut ini:

using System;
using System.Windows;
using System.Windows.Input;
using waitress.WorkflowServices;

namespace waitress
{
    public class ViewModel
    {
        public ViewModel()
        {
            BuatOrderCommand = new BuatOrderCommandImpl(this);
        }

        public DateTime Tanggal { get; set; }

        public string NamaPelanggan { get; set; }

        public decimal Harga { get; set; }

        public ICommand BuatOrderCommand { get; private set; }
    }

    public class BuatOrderCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public BuatOrderCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            ServiceClient service = new ServiceClient();

            OrderSimpan orderSimpan = new OrderSimpan();
            orderSimpan.Tanggal = viewModel.Tanggal;
            orderSimpan.NamaPelanggan = viewModel.NamaPelanggan;
            orderSimpan.Harga = viewModel.Harga;
            service.BuatOrder(ref orderSimpan);
            MessageBox.Show("Order berhasil disimpan");
        }
    }

}

Saya kemudian merancang view agar terlihat seperti berikut ini:

Contoh rancangan view untuk waitress

Contoh rancangan view untuk waitress

Saya juga tidak lupa mengisi nilai DataContext untuk view sehingga terjadi binding dengan view model yang telah saya buat. Saya memakai modus binding OneWayToSource karena form ini hanya dipakai untuk mengisi data saja.

Membuat User Interface Untuk Koki

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Koki. Proyek ini akan memanggil web services yang disediakan oleh proyek WorkflowServices dan proyek DataServices (untuk mencari seluruh order yang ada berdasarkan status). Saya perlu menambahkan referensi ke dua proyek tersebut dengan men-klik kanan nama proyek dan memilih menu Add Service Reference….

Saya kemudian membuat sebuah view model yang isinya seperti berikut ini:

using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Koki.DataServices;

namespace Koki
{
    public class ViewModel
    {
        public DataServices.ServicesClient dataServices;
        public WorkflowServices.ServiceClient workflowServices;

        public ViewModel()
        {
            this.dataServices = new DataServices.ServicesClient();
            this.workflowServices = new WorkflowServices.ServiceClient();

            this.ListUntukMulaiDimasak = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("Dipesan"));
            this.ListUntukSedangDimasak = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("MulaiDimasak"));

            MulaiDimasakCommand = new MulaiDimasakCommandImpl(this);
            SelesaiDimasakCommand = new SelesaiDimasakCommandImpl(this);
            RefreshCommand = new RefreshCommandImpl(this);
        }

        public ObservableCollection<OrderRequest> ListUntukMulaiDimasak { get; private set; }

        public ObservableCollection<OrderRequest> ListUntukSedangDimasak { get; private set; }

        public ICommand MulaiDimasakCommand { get; private set; }

        public ICommand SelesaiDimasakCommand { get; private set; }

        public ICommand RefreshCommand { get; private set; }

        public OrderRequest SelectedMulaiDimasak { get; set; }

        public OrderRequest SelectedSelesaiDimasak { get; set; }

        public String NamaKoki { get; set; }
    }

    public class RefreshCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public RefreshCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.ListUntukMulaiDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("Dipesan"))
            {
                viewModel.ListUntukMulaiDimasak.Add(o);
            }

            viewModel.ListUntukSedangDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("MulaiDimasak"))
            {
                viewModel.ListUntukSedangDimasak.Add(o);
            }
        }

    }

    public class MulaiDimasakCommandImpl : ICommand 
    {
        private ViewModel viewModel;

        public MulaiDimasakCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedMulaiDimasak != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.workflowServices.MulaiDimasak(new WorkflowServices.MulaiDimasak()
            {
                orderId = viewModel.SelectedMulaiDimasak.Id,
                nama = viewModel.NamaKoki,
                tanggal = DateTime.Now,                
            });

            viewModel.ListUntukMulaiDimasak.Remove(viewModel.SelectedMulaiDimasak);

            viewModel.ListUntukSedangDimasak.Clear();
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("MulaiDimasak"))
            {
                viewModel.ListUntukSedangDimasak.Add(o);
            }
        }
    }

    public class SelesaiDimasakCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public SelesaiDimasakCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedSelesaiDimasak != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.workflowServices.SelesaiDimasak(new WorkflowServices.SelesaiDimasak()
            {
                orderId = viewModel.SelectedSelesaiDimasak.Id,
                nama = null,
                tanggal = DateTime.Now,                
            });
            viewModel.ListUntukSedangDimasak.Remove(viewModel.SelectedSelesaiDimasak);            
        }
    }
}

Seperti biasa, saya perlu mengisi DataContext untuk melakukan binding dengan view model. Setelah itu, saya membuat view dengan tampilan yang terlihat seperti pada gambar berikut ini:

Contoh rancangan view untuk koki

Contoh rancangan view untuk koki

Membuat User Interface Untuk Kasir

Saya mulai dengan men-klik kanan nama solution dan memilih menu Add, New Project…. Setelah itu, saya memilih template WPF Application dan mengisi nama proyek dengan Kasir. Saya juga menambahkan referensi ke web services yang disediakan oleh proyek WorkflowServices dan proyek DataServices seperti pada proyek sebelumnya.

Saya kemudian membuat sebuah view model yang isinya seperti berikut ini:

using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Kasir.DataServices;
using Kasir.WorkflowServices;

namespace Kasir
{
    public class ViewModel
    {
        public DataServices.ServicesClient dataServices;
        public WorkflowServices.ServiceClient workflowServices;

        public ViewModel()
        {
            this.dataServices = new DataServices.ServicesClient();
            this.workflowServices = new WorkflowServices.ServiceClient();

            this.ListUntukDibayar = new ObservableCollection<OrderRequest>(
                dataServices.LihatSemuaOrder("SelesaiDimasak"));

            this.BayarCommand = new BayarCommandImpl(this);
            this.RefreshCommand = new RefreshCommandImpl(this);
        }

        public ObservableCollection<OrderRequest> ListUntukDibayar { get; private set; }

        public OrderRequest SelectedDibayar { get; set; }

        public ICommand BayarCommand { get; private set; }

        public ICommand RefreshCommand { get; private set; }

        public String JenisPembayaran { get; set; }
    }

    public class RefreshCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public RefreshCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            viewModel.ListUntukDibayar.Clear();                        
            foreach (OrderRequest o in viewModel.dataServices.LihatSemuaOrder("SelesaiDimasak"))
            {
                viewModel.ListUntukDibayar.Add(o);
            }
        }
    }

    public class BayarCommandImpl : ICommand
    {
        private ViewModel viewModel;

        public BayarCommandImpl(ViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.SelectedDibayar != null && viewModel.JenisPembayaran != null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            PelangganMembayar pelangganMembayar = new PelangganMembayar()
            {
                orderId = viewModel.SelectedDibayar.Id,
                tanggal = viewModel.SelectedDibayar.Tanggal,
                jenisPembayaran = viewModel.JenisPembayaran
            };
            viewModel.workflowServices.PelangganMembayar(pelangganMembayar);
            viewModel.ListUntukDibayar.Remove(viewModel.SelectedDibayar);
        }
    }
}

Rancangan view yang saya buat terlihat seperti pada gambar berikut ini:

Contoh rancangan view untuk kasir

Contoh rancangan view untuk kasir

Menjalankan Aplikasi

Sampai disini, saya telah memiliki 6 proyek di solution Visual Studio, seperti yang terlihat pada gambar berikut ini:

Daftar proyek untuk solution di Visual Studio

Daftar proyek untuk solution di Visual Studio

Pada saat aplikasi dijalankan, komunikasi antar-tier akan terlihat seperti pada gambar berikut ini:

Arsitektur aplikasi n-tier (multi-tier)

Arsitektur aplikasi n-tier (multi-tier)

Contoh data yang tersimpan di database adalah:

Contoh isi tabel

Contoh isi tabel

Setiap tier dapat dijalankan pada sebuah komputer (server) tunggal. Mereka akan saling berkomunikasi dengan menggunakan web services. Contoh kasus yang lebih realistis adalah presentation tier untuk kasir dalam bentuk aplikasi mobile yang dijalankan di smartphone, presentation tier untuk koki dalam bentuk aplikasi WPF yang dijalankan di perangkat touch screen, presentation tier untuk kasir dalam bentuk aplikasi Windows Forms yang dijalankan di PC yang dilengkapi dengan printer.

Penggunaan WF pada contoh aplikasi sederhana ini memang terlalu berlebihan, namun ada beberapa keuntungan yang dapat diperoleh. Developer dapat memperoleh visualisasi workflow sehingga mereka dapat lebih memahami kasus yang dihadapi. Selain itu, WF menjaga urutan eksekusi setiap activity Receive. Dengan demikian, client akan memperoleh pesan kesalahan bila ia memanggil web service PelangganMembayar() sebelum ada yang memanggil SelesaiDimasak() pada workflow instance yang sama.

Memakai Bookmark Di Windows Workflow (WF)

Salah satu fitur bawaan sejak .NET Framework 3 adalah Windows Workflow (WF). Fitur yang satu ini cukup unik. Pertama, tidak seperti temannya yang disingkat WCF dan WPF, Windows Workflow cukup disingkat menjadi WF. Alasannya? Bila disingkat menjadi WWF, maka ia terlihat seperti ‘berhubungan’ dengan organisasi tinju (World Wrestling Federation) atau pencinta binatang (World Wide Fund for Nature). Kedua, teknologi kompetitor .NET seperti Java sama sekali tidak menyediakan workflow engine secara bawaan sehingga dibutuhkan tools dari pihak ketiga seperti yang disediakan Oracle (secara teknis, karena Java sudah dibeli oleh Oracle, maka score-nya seimbang ;). Beberapa orang lebih senang membuat workflow engine sendiri untuk mengurangi ketergantungan pada pihak ketiga. Sebagai contoh nyata, peralihan dari WF 3 ke WF 4 menimbulkan perubahan yang cukup drastis sehingga proses upgrade tidak gampang.

Apa itu workflow? Pada aplikasi sederhana untuk restoran, misalnya, pemesanan dimulai dari waitress, kemudian disajikan oleh koki, lalu dinikmati pelanggan, dan berakhir dengan dibayar oleh pelanggan melalui kasir. Proses yang butuh waktu beberapa menit hingga beberapa jam ini adalah contoh dari workflow. Pada implementasinya, kode program biasanya mencatat nilai statusTerakhir dari sebuah pemesanan, lalu mengubah state tersebut selangkah demi selangkah. Ini adalah kandidat untuk state machine workflow. Contoh ini sangat sederhana sekali sehingga penggunaan workflow engine akan terlalu berlebihan.

Lalu kapan memakai workflow engine? Tentu saja bila aplikasi memiliki workflow yang rumit 🙂 Sebagai contoh, pada aplikasi pengajuan kredit di perbankan, workflow dimulai dengan aplikasi kredit, kemudian dilanjutkan dengan verifikasi, setelah data untuk verifikasi terkumpul, dibutuhkan konfirmasi manual dari credit analyst. Verifikasi terdiri atas lebih dari satu jenis aktifitas, misalnya verifikasi lapangan (seperti mengambil foto bangunan yang dijaminkan), verifikasi riwayat kartu kredit, dan sebagainya. Jumlah langkah verifikasi akan berbeda tergantung pada jenis pelanggan (misalnya corporate atau personal). Sebelum seluruh data verifikasi terkumpul (beberapa verifikasi wajib dan beberapa lagi tidak wajib, tergantung pada jenis pelanggan), eksekusi workflow tidak bisa dilanjutkan. Proses seperti ini tentunya akan lebih mudah dipahami bila dirancang dan dibuat secara visual.

Seperti apa contoh workflow? Untuk menunjukkannya, saya akan mulai dengan membuat sebuah proyek baru di Visual Studio 2010. Saya memilih Visual C#, Workflow, Workflow Console Application seperti yang terlihat pada gambar berikut ini:

Membuat proyek yang memakai workflow

Membuat proyek yang memakai workflow

Saya kemudian membuat sebuah domain class baru bernama Order dengan men-klik kanan nama proyek, memilih Add, Class…. Isi dari class tersebut adalah:

using System;

namespace LatihanBookmark
{
    public class Order
    {
        public String Id { get; set; }

        public String namaPelanggan { get; set; }

        public String menuMakanan { get; set; }

        public String namaKoki { get; set; }

        public String namaKasir { get; set; }

        public DateTime mulaiDimasak { get; set; }

        public DateTime selesaiDimasak { get; set; }

        public override string ToString()
        {
            return "ID: " + Id + "\n" +
                "Nama Pelanggan: " + NamaPelanggan + "\n" +
                "Menu Makanan: " + MenuMakanan + "\n" +
                "Nama Koki: " + NamaKoki + "\n" +
                "Nama Kasir: " + NamaKasir + "\n" +
                "Mulai Dimasak: " + MulaiDimasak.ToString("dd-MM-yyyy hh:mm") + "\n" +
                "Selesai Dimasak: " + SelesaiDimasak.ToString("dd-MM-yyyy hh:mm");
        }
    }
}

Class di atas adalah contoh rancangan yang buruk dan tidak rapi, tapi fokus saya adalah pada workflow sehingga saya bisa mengabaikan masalah tersebut.

Berikutnya, saya men-klik kanan file Workflow1.xaml dan memilih Rename untuk mengubah nama file tersebut menjadi ProsesPemesanan.xaml. Sama seperti pada WPF, rancangan workflow ditulis dalam format XAML (Extensible Application Markup Language). XAML sebenarnya adalah sebuah teknologi untuk membuat object dan mengisi property-nya melalui XML. Walaupun ia lebih dikenal berkat WPF, XAML tidak hanya terbatas dipakai untuk merancang form. Untuk melihat XAML dari sebuah workflow, saya dapat men-klik kanan dan memilih menu View Code. Pada XAML ini, saya perlu mengubah nilai atribut x:Class pada <Activity> di baris paling awal menjadi seperti berikut ini:

<Activity mc:Ignorable="sads sap" x:Class="LatihanBookmark.ProsesPemesanan" 
...

Sekarang, saya kembali melihat visualisinya di workflow designer. Untuk itu, saya men-klik kanan file ProsesPemesanan.xaml dan memilih menu View Designer.

Sebuah workflow dapat memiliki parameter. Sebagai contoh, pada proses pemesanan, saya dapat memakai sebuah instance dari object Order sebagai argumen. Saya segera mendefinisikan parameter dengan men-klik Arguments di bagian bawah designer dan mengisinya seperti yang terlihat pada gambar berikut ini:

Menambah parameter untuk workflow

Menambah parameter untuk workflow

Sebelum memilih Argument Type, saya men-build proyek terlebih dahulu dengan men-klik menu Build, Build Solution. Setelah itu, pada pilihan di Argumen Type, saya memilih Browse For Types.. untuk memilih Order.

Saya kemudian merancang workflow untuk proses pemesanan yang terlihat seperti pada gambar berikut ini:

Contoh rancangan flowchart workflow

Contoh rancangan flowchart workflow

Untuk menjalankan workflow, saya mengubah kode program di Programs.cs menjadi seperti berikut ini:

using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;

namespace LatihanBookmark
{

    class Program
    {
        static void Main(string[] args)
        {
            Order contohOrder = new Order
            {
                Id = "ORDER1",
                menuMakanan = "Tasty Food",
                namaPelanggan = "A Snake"
            };

            var argumen = new Dictionary<string, object>()
            {
                { "order", contohOrder }
            };


            AutoResetEvent syncEvent = new AutoResetEvent(false);
            WorkflowApplication app = new WorkflowApplication(new ProsesPemesanan(), argumen);
            app.Completed = (e) => syncEvent.Set();
            app.Run();

            // Menunggu hingga workflow selesai dikerjakan
            syncEvent.WaitOne();

            Console.ReadLine();
        }
    }
}

Kode program bawaan memakai WorkflowInvoker.Invoke() untuk menjalankan workflow. Method tersebut akan menunggu hingga workflow selesai dikerjakan baru lanjut ke eksekusi baris berikutnya (bersifat synchronous). Pada kode program di atas, saya membuat instance WorkflowApplication dan memanggil method Run() miliknya. Method tersebut tidak akan menunggu hingga workflow selesai dikerjakan tetapi langsung lanjut ke eksekusi baris berikutnya. Oleh sebab itu, saya memakai AutoResetEvent untuk menunggu hingga eksekusi workflow selesai dikerjakan.

Bila saya menjalankan program di atas, maka workflow akan langsung berjalan hingga tahap terakhir yang men-cetak “Order ORDER1 selesai diproses!”. Ini bukan sesuatu yang diharapkan, bukan?

Saya men-double click sequence Periksa Ketersediaan Bahan dan menambahkan rancangan seperti yang terlihat pada gambar berikut ini:

Contoh rancangan workflow

Contoh rancangan workflow

Pada sequence di atas, agar sederhana, saya hanya memeriksa nama menu dan membatalkan workflow berdasarkan nama menu. Pada kasus nyatanya, saya perlu memeriksa ketersediaan bahan di database (dan sejenisnya).

Berikutnya, saya perlu membuat detail untuk sequence Mulai dimasak. Bagian ini sedikit membingungkan: saya harus menunggu konfirmasi dari koki bahwa order sedang diproses olehnya! Tapi saat ini, workflow saya hanyalah imitasi sebuah method. Workflow yang sesungguhnya adalah proses yang berjalan selama jangka waktu lama yang tidak dapat dicakup oleh sebuah method. Terkadang workflow perlu ditunda (di-suspend) untuk dilanjutkan (di-resume) oleh pihak terkait. Untuk keperluan tersebut, saya dapat memakai bookmark.

Sayang sekali workflow designer tidak menyediakan komponen untuk membuat bookmark sehingga saya perlu membuat custom activity baru dengan nama MulaiDimasak yang isinya seperti berikut ini:

using System;
using System.Activities;

namespace LatihanBookmark
{
    public class MulaiDimasak : NativeActivity
    {
        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark("MulaiDimasak", new BookmarkCallback(OnResume));
        }

        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }

        public void OnResume(NativeActivityContext context, Bookmark bookmark, object koki)
        {
            WorkflowDataContext dataContext = context.DataContext;
            Order order = (Order) dataContext.GetProperties()
                                  .Find("order", false).GetValue(dataContext);
            order.NamaKoki = (string) koki;
            order.MulaiDimasak = DateTime.Now;                      
            Console.WriteLine("{0} sudah mulai memasak...", koki);
        }
    }
}

Pada NativeActivity di atas, saya membuat bookmark baru dengan memakai context.CreateBookMark(). Pada bookmark tersebut, saya menyertakan sebuah callback yaitu OnResume. Dengan demikian, pada saat seseorang me-resume bookmark tersebut, maka kode program di method OnResume() akan dikerjakan. Method tersebut akan mengisi property NamaKoki dan MulaiDimasuk dari order yang diberikan sebagai argumen pada workflow ini.

Saya kemudian men-build proyek dengan memilih menu Build, Build Solution. Saat saya kembali workflow designer untuk ProsesPemesanan, saya akan menemukan item baru di Toolbox dengan nama MulaiDimasak. Saya akan memakainya untuk menggantikan sequence yang sudah ada sebelumnya seperti yang terlihat pada gambar berikut ini:

Memakai custom activity di workflow designer

Memakai custom activity di workflow designer

Bila saya menjalankan program, maka kali ini tidak akan ada output yang muncul di layar karena program sedang menunggu hingga seseorang me-resume bookmark dengan nama MulaiDimasak. Pada kenyataannya, aplikasi di koki yang perlu me-resume bookmark ini pada saat koki men-klik tombol tertentu. Pada contoh sederhana disini, saya akan me-resume bookmark secara manual pada saat itu juga, dengan mengubah kode program di Program.cs menjadi seperti berikut ini:

using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;

namespace LatihanBookmark
{

    class Program
    {
        static void Main(string[] args)
        {
            Order contohOrder = new Order
            {
                Id = "ORDER1",
                MenuMakanan = "Tasty Food",
                NamaPelanggan = "A Snake"
            };

            var argumen = new Dictionary<string, object>()
            {
                { "order", contohOrder }
            };


            AutoResetEvent syncEvent = new AutoResetEvent(false);
            WorkflowApplication app = new WorkflowApplication(new ProsesPemesanan(), argumen);
            app.Completed = (e) => syncEvent.Set();
            app.Run();

            app.ResumeBookmark("MulaiDimasak", "Master Chef");

            // Menunggu hingga workflow selesai dikerjakan
            syncEvent.WaitOne();

            Console.WriteLine("Workflow sudah ditutup.\n\nIsi Contoh Order adalah:");
            Console.WriteLine(contohOrder);
            Console.ReadLine();
        }
    }
}

Method ResumeBookmark() akan menunggu hingga ada bookmark dengan nama MulaiDimasak yang dapat di-resume.

Hasil dari kode program di atas akan terlihat seperti berikut ini:

Master Chef sudah mulai memasak...
Order ORDER1 selesai diproses!
Workflow sudah ditutup.

Isi Contoh Order adalah:
ID: ORDER1
Nama Pelanggan: A Snake
Menu Makanan: Tasty Food
Nama Koki: Master Chef
Nama Kasir:
Mulai Dimasak: 18-01-2014 11:00
Selesai Dimasak: 01-01-0001 12:00

Sama seperti pada langkah sebelumnya, saya juga perlu mengubah sequence Selesai Dimasak menjadi custom activity yang memakai bookmark. Hal ini karena ia perlu menunggu hingga koki mengisyaratkan bahwa makanan telah selesai dimasak. Oleh sebab itu, saya membuat class SelesaiDimasak yang isinya seperti berikut ini:

using System;
using System.Activities;

namespace LatihanBookmark
{
    public class SelesaiDimasak : NativeActivity
    {
        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark("SelesaiDimasak", new BookmarkCallback(OnResume));
        }

        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }

        public void OnResume(NativeActivityContext context, Bookmark bookmark, object koki)
        {
            WorkflowDataContext dataContext = context.DataContext;
            Order order = (Order)dataContext.GetProperties()
                                  .Find("order", false).GetValue(dataContext);
            order.SelesaiDimasak = DateTime.Now;            
            Console.WriteLine("Order sudah mulai dimasak...");
        }
    }
}

Sequence Pelanggan Membayar juga harus membuat bookmark karena ia perlu menunggu hingga pelanggan selesai makan dan membayar di kasir. Untuk itu, saya membuat class PelangganMembayar yang isinya seperti berikut ini:

using System;
using System.Activities;

namespace LatihanBookmark
{
    public class PelangganMembayar : NativeActivity
    {
        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark("PelangganMembayar", new BookmarkCallback(OnResume));
        }

        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }

        public void OnResume(NativeActivityContext context, Bookmark bookmark, object namaKasir)
        {
            WorkflowDataContext dataContext = context.DataContext;
            Order order = (Order)dataContext.GetProperties()
                                  .Find("order", false).GetValue(dataContext);
            order.NamaKasir = (string) namaKasir;
            Console.WriteLine("Pelanggan sudah melunasi tagihannya...");
        }
    }
}

Seperti biasa, saya men-build ulang proyek. Lalu pada workflow designer, saya memakai custom activity yang sudah saya buat sebelumnya sehingga terlihat seperti pada gambar berikut ini:

Memakai custom activity di workflow designer

Memakai custom activity di workflow designer

Sebagai langkah terakhir, saya perlu mensimulasikan proses resume dari bookmark yang sudah dibuat sebelumnya. Caranya adalah dengan menambahkan kode program berikut ini pada Programs.cs:

...
app.ResumeBookmark("MulaiDimasak", "Master Chef");
app.ResumeBookmark("SelesaiDimasak", "Master Chef");
app.ResumeBookmark("PelangganMembayar", "Cute Girl");
...

Bila saya menjalankan program, saya akan memperoleh hasil seperti berikut ini:

Master Chef sudah mulai memasak...
Order sudah mulai dimasak...
Pelanggan sudah melunasi tagihannya...
Order ORDER1 selesai diproses!
Workflow sudah ditutup.

Isi Contoh Order adalah:
ID: ORDER1
Nama Pelanggan: A Snake
Menu Makanan: Tasty Food
Nama Koki: Master Chef
Nama Kasir: Cute Girl
Mulai Dimasak: 17-01-2014 10:00
Selesai Dimasak: 17-01-2014 10:10

Bila saya menghilangkan salah satu dari ResumeBookmark() di Programs.cs, maka workflow proses pemesanan tidak akan pernah selesai dikerjakan karena ia akan terus menunggu hingga seseorang men-resume bookmark-nya.

Membuat Web Service Dengan Windows Communication Foundation (WCF)

Sebuah layer dalam software engineering adalah pemisahan secara logika dalam mengelompokkan kode program. Biasanya pengelompokkan ini dilakukan berdasarkan peran dan fungsi kode program. Berbeda dengan layer, tier adalah kode program yang berjalan di sebuah fisik yang berbeda, misalnya di komputer server. Tier dibedakan berdasarkan fisik (seperti komputer server) ini. Biasanya, sebuah tier diwakili oleh unit eksekusi (seperti file exe) yang berbeda. Sebagai contoh, bila terdapat A.EXE, B.EXE, dan C.EXE sebagai bagian dari sebuah aplikasi di komputer yang sama dimana mereka saling berkomunikasi melalui mekanisme Interprocess Communication (IPC) seperti shared memory, maka mereka boleh dianggap sebagai tier yang berbeda.

Windows Communication Foundation (WCF) adalah fasilitas dari .NET Framework untuk mempermudah komunikasi antar-tier. Sebagai latihan, saya akan mengubah kode program yang saya buat di artikel sebelumnya, Melakukan Binding Collection Di WPF, sehingga kode program tersebut murni hanya merupakan presentation tier dengan WPF. Nantinya, ia akan mengakses business service tier yang berada di aplikasi berbeda. Domain model yang sebelumnya dipakai langsung di aplikasi WPF tersebut akan saya pindahkan ke business service tier karena logika bisnis akan dikerjakan disini (pada latihan sederhana saya, logika bisnis-nya adalah method Total() yang menghitung total setelah diskon).

Saya akan mulai dengan membuat sebuah proyek console application Visual C# yang saya beri nama LatihanBusinessLogicTier. Untuk memakai WCF, saya perlu men-klik kanan nama proyek, kemudian memilih Add Reference…. Pada tab .NET, saya memilih System.ServiceModel dan men-klik tombol OK. Selain itu, saya perlu melakukan langkah yang sama untuk menambahkan referensi ke System.Runtime.Serialization agar nantinya saya bisa memakai atribut seperti [DataContract] dan [DataMember].

Saya kemudian memindahkan ItemPenjualan.cs dari proyek sebelumnya ke proyek baru ini. Setelah itu, saya mengubah namespace dari LatihanMVVM menjadi LatihanBusinessLogicTier. Mengapa domain model diletakkan disini? Bila seandainya logika bisnis (pada contoh ini menghitung Total()) diletakkan di presentation tier yang memakai WPF lalu suatu saat nanti ada presentation tier baru yang berbasis web, maka kode program Total() harus diduplikasi. Ini bukanlah sesuatu yang baik. Oleh sebab itu, saya membuat presentation tier menjadi sebuah aplikasi‘dummy’ yang tidak tahu apa-apa selain bertugas menampilkan.

Walaupun domain model dikerjakan oleh business service tier, object-nya tidak akan dikembalikan begitu saja ke presentation tier. Dengan kata lain, sebaiknya business service tier tidak mengembalikan instance dari ItemPenjualan secara langsung ke presentation tier. Mengapa demikian? Lazy loading tidak akan bekerja dengan baik karena presentation tier tidak lagi memiliki akses ke database. Karena lazy loading tidak bekerja, maka tidak semua method di domain model dapat dikerjakan. Lagi pula, presentation tier tidak perlu memanggil method yang mewakili business logic tersebut. Sebaiknya ia hanya memperoleh nilai dan menampilkannya (atau sebaliknya, mengirim nilai).

Oleh sebab itu, saya perlu membuat class yang akan dipakai oleh data transfer object (DTO). Sebuah class DTO hanya memiliki property tanpa method. Walaupun property-nya hampir sama seperti milik domain model, mereka biasanya terbatas untuk keperluan service tertentu. Sebagai contoh, navigation property (misalnya mengakses Penjualan dari ItemPenjualan) tidak disertakan bila tidak dibutuhkan oleh client. Beberapa programmer merasa bahwa penggunaan DTO terlalu berlebihan untuk kasus yang sederhana. Yup! Sebenarnya, saya dapat langsung men-share domain model ke client untuk contoh sederhana ini. Tapi pada aplikasi yang lebih besar dan lebih rumit (dimana domain model memiliki navigation property dan asosiasi seperti one-to-many), penggunaan DTO merupakan sesuatu yang disarankan.

Setelah menemukan rancangan arsitektur yang akan dipakai, saya siap untuk membuat kode program. Saya akan mulai dengan membuat sebuah interface baru yang mewakili service contract dengan nama ItemPenjualanService yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace LatihanBusinessLogicTier
{
    [ServiceContract]
    public class ItemPenjualanService
    {
        [OperationContract]
        public void Simpan(ItemPenjualanSaveDTO itemPenjualanDTO)
        {
            // Kode Program Disini!
        }

        [OperationContract]
        public DaftarItemPenjualanDTO LihatSemua()
        {
            // Kode Program Disini!
        }
    }

    [DataContract]
    public class ItemPenjualanDTO
    {
        [DataMember]
        public long Id;

        [DataMember]
        public string NamaBarang;

        [DataMember]
        public int Jumlah;

        [DataMember]
        public decimal Harga;

        [DataMember]
        public decimal DiskonPersen;

        [DataMember]
        public decimal Total;
    }

    [CollectionDataContract]
    public class DaftarItemPenjualanDTO : List<ItemPenjualanDTO> { }

    [DataContract]
    public class ItemPenjualanSaveDTO
    {
        [DataMember]
        public string NamaBarang;

        [DataMember]
        public int Jumlah;

        [DataMember]
        public decimal Harga;

        [DataMember]
        public decimal DiskonPersen;
    }
}

Pada kode program di atas, DTO untuk operasi Simpan() tidak memiliki property Id dan Total karena memang tidak dibutuhkan untuk operasi tersebut. Dengan demikian, sebuah DTO tidak selalu memiliki property yang sama dengan domain class yang diwakilinya.

Kode program di atas belum selesai. Saya perlu mengimplementasikan isi service untuk kode program di atas, tapi sebelumnya saya akan terlebih dahulu membuat persistence layer.

Langkah selanjutnya adalah menambahkan Entity Framework sebagai persistence layer yang akan mengakses database. Untuk itu, saya men-install Entity Framework terbaru di proyek dengan memilih menu Project, Manage NuGet Packages…. Setelah Entity Framework berhasil ditambahkan pada proyek, saya perlu mengubah App.config sama seperti pada proyek di artikel sebelumnya. Saya juga perlu men-klik kanan nama proyek dan memilih Add Reference…, .NET, MySql.Data.Entity for EF6 agar dapat memakai database MySQL dengan Entity Framework 6. Berikutnya, saya men-copy file LatihanContext.cs dan MyHistoryContext.cs dari proyek di artikel sebelumnya ke proyek baru ini. Saya perlu mengubah namespace yang dipakai oleh kedua file tersebut dari LatihanMVVM menjadi LatihanBusinessLogicTier. Setelah ini persistence layer sudah selesai dibuat dan siap dipakai.

Saya akan membuat implementasi untuk service Simpan() di ItemPenjualanService.cs yang isinya adalah seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  [OperationContract]
  public void Simpan(ItemPenjualanSaveDTO itemPenjualanDTO)
  {
    using (var db = new LatihanContext())
    {
      ItemPenjualan p = new ItemPenjualan()
      {
        NamaBarang = itemPenjualanDTO.NamaBarang,
        Jumlah = itemPenjualanDTO.Jumlah,
        Harga = itemPenjualanDTO.Harga,
        DiskonPersen = itemPenjualanDTO.DiskonPersen
      };
      db.DaftarItemPenjualan.Add(p);
      db.SaveChanges();
    }
  }

  ...
}
...

Saya juga mengimplementasikan service LihatSemua() yang isinya seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  ...
  [OperationContract]
  public DaftarItemPenjualanDTO LihatSemua()
  {
    DaftarItemPenjualanDTO hasil = new DaftarItemPenjualanDTO();
    using (var db = new LatihanContext())
    {
      foreach (var p in db.DaftarItemPenjualan)
      {
        hasil.Add(new ItemPenjualanDTO()
        {
          Id = p.Id.Value,
          NamaBarang = p.NamaBarang,
          Harga = p.Harga,
          DiskonPersen = p.DiskonPersen,
          Total = p.Total()
        });
     }
   }
   return hasil;
  }
  ...
}
...

Saya juga membuat sebuah service HitungTotal() yang akan mengembalikan sebuah decimal seperti berikut ini:

...
[ServiceContract]
public class ItemPenjualanService
{

  ...
  [OperationContract]
  public decimal HitungTotal(ItemPenjualanSaveDTO itemPenjualanDTO)
  {
    ItemPenjualan p = new ItemPenjualan()
    {
       NamaBarang = itemPenjualanDTO.NamaBarang,
       Jumlah = itemPenjualanDTO.Jumlah,
       Harga = itemPenjualanDTO.Harga,
       DiskonPersen = itemPenjualanDTO.DiskonPersen
    };
    return p.Total();
  }
  ...
}
...

Pada langkah berikutnya, saya perlu men-host web services yang sudah saya buat. Agar sederhana, saya akan memakai fasilitas self-hosting. Pada prakteknya, web services sebaiknya di-host ke web server seperti Internet Information Services (IIS). Untuk membuat aplikasi console yang dapat melayani request (self-hosting), saya akan membuat kode program berikut ini pada file Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace LatihanBusinessLogicTier
{
    class Program
    {
        static void Main(string[] args)
        {
            Uri baseAddress = new Uri("http://localhost:80/LatihanBusinessLogicTier/Service");
            ServiceHost selfHost = new ServiceHost(typeof(ItemPenjualanService), baseAddress);
            try
            {
                selfHost.AddServiceEndpoint(typeof(ItemPenjualanService), new WSHttpBinding(), "ItemPenjualanService");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;                
                selfHost.Description.Behaviors.Add(smb);
                selfHost.Open();
                Console.WriteLine("Business Logic Tier sudah aktif dan siap melayani request.");
                while (true)
                {
                    System.Threading.Thread.Sleep(50);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Telah terjadi kesalahan: {0}", ex.Message);
                selfHost.Abort();
            }
            Console.ReadLine();
        }
    }
}

Salah satu kelebihan WCF adalah ia bisa mendukung satu atau lebih binding. Pada kosa kata WCF, binding adalah sesuatu yang menentukan bagaimana proses komunikasi terjadi (protokol yang dipakai, format data, dan sebagainya). Contoh binding yang disediakan oleh .NET saat ini adalah WSHttpBinding untuk web services berbasis SOAP, WebHttpBinding yang dapat dipakai untuk membuat web services berbasis REST, NetTcpBinding untuk komunikasi jaringan melalui socket, dan sebagainya. Programmer juga dapat mengimplementasikan binding baru bila perlu. Menariknya, sebuah aplikasi boleh memiliki lebih dari satu binding. Salah contoh penerapannya, sebuah aplikasi yang sama dapat men-ekspos services yang dimilikinya dalam bentuk REST maupun SOAP. Pada contoh ini, saya hanya memakai WsHttpBinding sehingga services akan dipublikasi dalam bentuk layanan SOAP yang mendukung spesifikasi WS-* .

Sekarang saatnya menjalankan program yang berisi web services ini. Tapi, sebelumnya, saya perlu menutup Visual Studio dan menjalankan dengan memilih Run As Administrator. Hal ini karena bila program tersebut dijalankan oleh user yang tidak memiliki hak akses, akan muncul pesan kesalahan HTTP could not register URL ... Your process does not have access rights to this namespace.

Bila saya menjalankan program, saya akan memperoleh pesan seperti berikut ini:

Business Logic Tier sudah aktif dan siap melayani request.

Untuk menguji apakah services dapat diakses dengan baik, saya akan membuka Visual Studio Command Prompt (2010) dan memberikan perintah WcfTestClient. Pada aplikasi WcfTestClient yang muncul, saya memilih menu File, Add Service…. Lalu pada dialog pengisian yang muncul, saya mengisi dengan nilai http://localhost/LatihanBusinessLogicTier/Service dan men-klik tombol OK. Saya akan memperoleh hasil seperti pada gambar berikut ini:

Menguji web services yang telah dibuat

Menguji web services yang telah dibuat

Saya akan mencoba mengerjakan service LihatSemua() dengan men-double click nama service tersebut. Kemudian pada bagian kanan, saya men-klik tombol Invoke. Saya akan memperoleh hasil seperti pada gambar berikut ini:

Memanggil service `LihatSemua()`

Memanggil service `LihatSemua()`

Saya juga dapat mencoba mengerjakan service Simpan() seperti yang terlihat pada gambar berikut ini:

Memanggil service `Simpan()`

Memanggil service `Simpan()`

Service Simpan() tidak mengembalikan respon berupa sebuah nilai. Untuk memastikan apakah ia berhasil dikerjakan dengan baik, saya perlu memeriksa database secara manual untuk melihat apakah data pada tabel sudah bertambah.

Setelah memastikan bahwa business service tier sudah beres, saya akan memperbaharui presentation tier agar memanggil business service tier. Saya perlu menghapus file yang berkaitan dengan Entity Framework karena kini presentation tier tidak lagi mengakses database secara langsung. Saya juga perlu membuang domain model yang sudah saya buat sebelumnya (file ItemPenjualan.cs) karena presentation tier tidak lagi menampung logika bisnis. Yang tersisa hanya dua komponen untuk MVVM, yaitu view (file MainWindow.xaml) dan view model (file ItemPenjualanViewModel.cs). Lalu, mana model-nya? MVVM tidak akan pernah lengkap tanpa model, bukan?

Yang kini menjadi model dalam MVVM disini adalah DTO yang dikembalikan oleh business service tier. Muncul sebuah pertanyaan disini: Class DTO didefinisikan di business service tier, apakah saya perlu mereferensi class tersebut dengan menyertakan proyek business service tier sebagai dependency? Tidak harus! Lalu, class apa yang harus saya pakai? Karena class DTO hanya berisi data tanpa logika program, maka saya bisa menghasilkan class tersebut secara otomatis selama business service tier mempublikasikan metadata untuk services yang dimilikinya. Dengan demikian, saya tidak perlu mereferensikan proyek business service tier di proyek presentation tier sehingga dapat mengurangi ketergantungan.

Untuk menghasilkan DTO dan kode program pembantu lainnya secara otomatis, saya men-klik kanan padaService References (bukan References!) di Solution Explorer, lalu memilih Add Service Reference…. Saya memastikan terlebih dahulu bahwa program untuk business service tier sudah dijalankan, kemudian mengisi dialog yang muncul seperti pada gambar berikut ini:

Menghasilkan kode program dan DTO di client berdasarkan metadata

Menghasilkan kode program dan DTO di client berdasarkan metadata

Bila saya melihat di Object Browser, saya akan menemukan beberapa class baru yang dibuat oleh Visual Studio 2010 secara otomatis untuk saya, seperti yang terlihat di gambar berikut ini:

Class yang dihasilkan

Class yang dihasilkan

Class DTO yang dihasilkan hampir mirip seperti asli-nya di proyek business service tier, hanya saja sudah dilengkapi dengan implementasi INotifyPropertyChanged. Dengan demikian, sebenarnya DTO dapat langsung dilewatkan oleh view model ke view (binding langsung dari view ke model DTO). Selain itu, saya juga akan menemukan interface ItemPenjualanService dan ItemPenjualanServiceChannel serta sebuah class ItemPenjualanServiceClient. Saya dapat mengakses web services yang disediakan business service tier melalui class tersebut.

Langkah terakhir adalah mengubah view model (file ItemPenjualanViewModel.cs) agar memakai DTO yang dihasilkan sehingga kode programnya menjadi seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
using System.Windows;
using System.Collections.ObjectModel;
using LatihanMVVM.BusinessLogicService;


namespace LatihanMVVM
{
    public class ItemPenjualanViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private ItemPenjualanSaveDTO model;

        private ICommand simpanCommand;        

        private ItemPenjualanServiceClient itemPenjualanService;

        private ObservableCollection<ItemPenjualanDTO> listItemPenjualan;

        public ItemPenjualanViewModel(ItemPenjualanSaveDTO itemPenjualan = null)
        {
            this.model = itemPenjualan ?? new ItemPenjualanSaveDTO();            

            this.itemPenjualanService = new ItemPenjualanServiceClient();
            this.ListItemPenjualan = new ObservableCollection<ItemPenjualanDTO>(itemPenjualanService.LihatSemua());
        }


        public ObservableCollection<ItemPenjualanDTO> ListItemPenjualan 
        {
            get { return this.listItemPenjualan; }
            private set { this.listItemPenjualan = value; }
        }

        public string NamaBarang
        {
            get { return model.NamaBarang; }
            set
            {
                if (value != model.NamaBarang)
                {
                    model.NamaBarang= value;
                    PropertyChanged(this, new PropertyChangedEventArgs("NamaBarang"));
                }
            }
        }

        public int Jumlah
        {
            get { return model.Jumlah; }
            set
            {
                if (value != model.Jumlah)
                {
                    model.Jumlah= value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Jumlah"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public decimal Harga
        {
            get { return model.Harga; }
            set
            {
                if (value != model.Harga)
                {
                    model.Harga = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Harga"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));                     
                }
            }
        }

        public decimal DiskonPersen
        {
            get { return model.DiskonPersen; }
            set
            {
                if (value != model.DiskonPersen)
                {
                    model.DiskonPersen = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("DiskonPersen"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public string Total
        {
            get 
            {
                decimal? total = itemPenjualanService.HitungTotal(model);
                if (!total.HasValue)
                {
                    return "-";
                }
                else
                {
                    return total.Value.ToString("C");
                }
            }
        }

        public ItemPenjualanSaveDTO Model
        {
            get { return this.model; }
        }

        public ItemPenjualanService ItemPenjualanService
        {
            get { return this.itemPenjualanService; }
        }

        public ICommand SimpanCommand
        {
            get
            {
                if (this.simpanCommand == null)
                {
                    this.simpanCommand = new SimpanCommand(this);
                }
                return this.simpanCommand; 
            }
        }

    }


    public class SimpanCommand : ICommand
    {

        private ItemPenjualanViewModel viewModel;

        public SimpanCommand(ItemPenjualanViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return !viewModel.Total.Equals("Rp0");
        }

        public void Execute(object parameter)
        {            
            viewModel.ItemPenjualanService.Simpan(viewModel.Model);            
            MessageBox.Show("Data berhasil disimpan ke database");
        }

    }

}

Sebenarnya view model dapat meng-ekpos DTO secara langsung untuk dipakai oleh view. Akan tetapi, saya berusaha menjaga agar tidak perlu ada perubahan pada view sehingga saya hanya perlu mengubah view model saja. Bila aplikasi dijalankan, ia akan bekerja sama seperti sebelumnya, tapi kali ini business logic dan database bisa saja berada di komputer terpisah, seperti yang terlihat pada gambar berikut ini:

Arsitektur Program

Arsitektur Program

Menerapkan MVVM Di Windows Presentation Foundation (WPF)

Seorang teman sedang giat mempersiapkan materi pelatihan untuk pattern Model View Controller (MVC) bagi mahasiswa pemula. Ia beberapa kali berkonsultasi dengan saya mengenai implementasi yang tepat. Sebagai informasi, Model View Controller (MVC) adalah pola yang diterapkan pada perancangan user interface/UI (presentation layer). Setelah upaya saya menunjukkan beberapa pola UI yang efektif, pada akhirnya teman tersebut lebih tertarik menerapkan pattern Model View View Model (MVVM). Boleh dibilang MVVM adalah sebuah variasi dari MVC yang memisahkan view menjadi dua, yaitu view dan view model yang terhubung melalui data binding. Saya sudah biasa memakai pola sejenis MVVM di framework Griffon. Pada kesempatan ini, saya akan membahas MVVM dengan contoh berupa sebuah aplikasi yang memakai Windows Presentation Foundation (WPF). Yup, menurut sejarah, MVVM pertama kali terlahir untuk keperluan WPF dan Silverlight (.NET Framework 3). Beberapa programmer (termasuk saya) kerap memodifikasi pola ini dengan menambahkan sebuah controller agar tidak mencemari view model. Walaupun demikian, saya akan tetap menyebutnya sebagai pola MVVM (karena ketergantungan pada penggunaan binding dan view model). Atau, apa perlu membuat istilah baru: MVVMC ? 😉

Saya akan mulai dengan membuat sebuah proyek baru di Visual Studio 2010. Saya memilih jenis proyek Visual C#, Windows, WPF Application untuk membuat aplikasi desktop yang memakai WPF.

Yang dimaksud dengan model dalam MVVM adalah domain model atau entity yang nantinya disimpan ke database. Seluruh nilai dan operasi yang berkaitan dengan permasalahan yang dihadapi (business logic) harus diletakkan disini! Saya akan membuat sebuah model baru dengan men-klik kanan nama proyek dan memilih Add, Class…. Saya memberi nama model ini sebagai ItemPenjualan yang kode programnya terlihat seperti berikut ini:

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

namespace LatihanMVVM
{
    public class ItemPenjualan
    {
        public ItemPenjualan()
        {
            DiskonPersen = 0;
        }

        public long Id { get; set; }

        public string NamaBarang { get; set; }

        public int Jumlah { get; set; }

        public decimal Harga { get; set; }

        public decimal DiskonPersen { get; set; }

        public decimal Total()
        {
            decimal total = Jumlah * Harga;
            return total - (DiskonPersen / 100 * total);
        }
    }
}

Beberapa programmer menambahkan event pada model sehingga dapat di-ekspos langsung melalui view model. Walaupun membuat kode program view model nanti menjadi lebih sederhana, saya tidak terlalu suka dengan pendekatan seperti itu karena mengotori kode program model yang seharusnya hanya berisi business logic.

Berikutnya, saya perlu membuat view berdasarkan isi model. Itu sebabnya saya selalu mulai dengan membuat domain model karena ia adalah pemicu dari segala pengembangan berikutnya. View pada Windows Presentation Foundation (WPF) dibuat dalam bentuk XML yang disebut sebagai XAML. Sama seperti view pada Windows Forms, view XAML ini juga didukung oleh sebuah class C#.

Editor untuk WPF di Visual Studio 2010

Editor untuk WPF di Visual Studio 2010

Sebuah view harus dirancang agar sebisa mungkin hanya berisi tampilan UI (sesuai namanya). Hal ini tidak jadi masalah pada WPF karena view berupa XAML yang tidak mengizinkan adanya kode program. Tapi pada teknologi lain, seperti JSP atau PHP yang membolehkan adanya kode program, pemula yang tidak berhati-hati bisa saja melanggar ketentuan MVC dengan meletakkan kode program seperti data access logic atau business logic di view. Begitu juga dengan teman saya yang berkonsultasi. Ia memakai designer NetBeans untuk membuat aplikasi dengan UI Swing. Setelah mencoba penggunaan MVC, ia mengeluh kenapa tidak meletakkan beberapa logic yang seharusnya bisa langsung diletakkan di view misalnya di bagian actionPerformed di tombol. Kenapa harus memindahkan semua logic yang tidak berkaitan dengan view ke controller? Meletakkan segala sesuatunya di view adalah sesuatu yang tepat (dan dianjurkan!) untuk aplikasi sederhana. Tapi, tentu saja itu bukan lagi mengikuti pola MVC. Dan kita tidak sedang ingin menciptakan pola setengah MVC, bukan? Penggunaan pola yang standar akan mempermudah komunikasi tim dimana setiap anggota tim dapat memahami hasil kerjaan anggota tim lainnya dengan mudah. Cara sederhana untuk menguji apakah arsitektur kode program yang dibuat sudah ‘standar’ adalah dengan mempublikasikannya di forum diskusi dan melihat masukan dari programmer lain. Bila kode program tersebut sulit dimengerti atau dijauhi pengunjung, kemungkinan ada yang salah dengan arsitekturnya 🙂

Saya mengubah kode program di MainWindow.xaml menjadi seperti berikut ini:

<Window x:Class="LatihanMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="356" Width="528">

    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="20" />
            <Setter Property="FontFamily" Value="Myriad Pro" />
            <Setter Property="FontWeight" Value="SemiBold" />
            <Setter Property="Background">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FF508FC4" Offset="0" />
                        <GradientStop Color="#FF6F94AD" Offset="1" />
                        <GradientStop Color="#FFC7F3FF" Offset="0.302" />
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="Foreground">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FF5252CE" Offset="0" />
                        <GradientStop Color="#FF0000DB" Offset="0.953" />
                        <GradientStop Color="#FF6363CB" Offset="0.337" />
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="Label">
            <Setter Property="FontSize" Value="14" />            
        </Style>

        <Style TargetType="TextBox">
            <Setter Property="Language" Value="in-IN" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Border x:Name="customBorder" Background="{TemplateBinding Background}" CornerRadius="5" BorderThickness="2" BorderBrush="Gray">
                            <ScrollViewer x:Name="PART_ContentHost"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsKeyboardFocused" Value="True">                                
                                <Setter TargetName="customBorder" Property="Effect">
                                    <Setter.Value>
                                        <DropShadowEffect BlurRadius="10" ShadowDepth="0" Color="#578EC9"/>
                                    </Setter.Value>
                                </Setter>                                
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="False">
                                <Setter Property="Foreground" Value="Gray" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="Button">
            <Setter Property="Background" Value="#DEF2FC" />
            <Setter Property="Foreground" Value="Black" />
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="Effect">
                <Setter.Value>
                    <DropShadowEffect BlurRadius="10" ShadowDepth="0" Color="#578EC9"/>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Border x:Name="customBorder" Background="{TemplateBinding Background}" CornerRadius="4" BorderThickness="2" BorderBrush="Gray">
                            <ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" />
                        </Border>     
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="#2394CC" />
                                <Setter Property="Foreground" Value="White" />                                
                            </Trigger>
                            <Trigger Property="IsPressed" Value="True">                                
                                <Setter Property="Effect" Value="{x:Null}" />
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="Effect">
                                    <Setter.Value>
                                        <BlurEffect Radius="3"  />
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>                    
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>        
        <Label Content="Nama Barang:" Height="29" HorizontalAlignment="Left" Margin="0,49,0,0" Name="label2" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="107" />
        <TextBox Height="23" HorizontalAlignment="Stretch" Margin="112,55,12,0" Name="textBox1" VerticalAlignment="Top" />
        <Label Content="Jumlah:" Height="27" HorizontalAlignment="Left" Margin="1,86,0,0" Name="label3" VerticalAlignment="Top" Width="106" HorizontalContentAlignment="Right" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="113,90,0,0" Name="textBox2" VerticalAlignment="Top" Width="62" />
        <Label Content="Harga:" Height="28" HorizontalAlignment="Left" Margin="12,122,0,0" Name="label4" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="95" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="113,127,0,0" Name="textBox3" VerticalAlignment="Top" Width="124" />
        <Button Content="Simpan" Height="27" HorizontalAlignment="Left" Margin="207,228,0,0" Name="button1" VerticalAlignment="Top" Width="82" />
        <Label Content="Diskon (%):" Height="33" HorizontalAlignment="Left" Margin="12,161,0,0" Name="label5" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="95" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="113,165,0,0" Name="textBox4" VerticalAlignment="Top" Width="62" />
        <Label Content="Total:" Height="33" HorizontalAlignment="Left" Margin="12,194,0,0" Name="label6" VerticalAlignment="Top" HorizontalContentAlignment="Right" Width="95" />
        <Label Content="Label" Height="28" HorizontalAlignment="Left" Margin="113,194,0,0" Name="label7" VerticalAlignment="Top" Width="402" />
        <TextBlock Height="28" HorizontalAlignment="Stretch" Name="textBlock1" Text="Tambah Item Penjualan" VerticalAlignment="Top" TextAlignment="Center" Margin="0,12,0,0" />

        <Grid.Background>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#FFB7CEFF" Offset="0.192" />
                <GradientStop Color="White" Offset="1" />
                <GradientStop Color="#FF1648AD" Offset="0" />
            </LinearGradientBrush>
        </Grid.Background>

    </Grid>
</Window>

Salah satu kelebihan WPF adalah saya bisa melakukan pengaturan setiap komponen secara leluasa dengan menggunakan style dan templates. Sebagai contoh, saya bisa membuat Button dan TextBox memiliki ujung yang membulat. Selain itu, WPF juga menyediakan efek seperti blur dan shadow. Pada XAML di atas, definisi form saya terletak di bagian <Grid>. Saya memisahkan style dan templates ke <Window.Resources> sehingga saya bisa melakukan pengaturan secara global. Bagi yang terbiasa memakai HTML, hal ini ibarat mendeklarasikan CSS pada bagian <head> untuk mengendalikan tampilan di <body>. Bila saya menjalankan program, saya akan memperoleh tampilan seperti berikut ini:

Tampilan View

Tampilan View

XAML di WPF memiliki kemampuan setara atau lebih dari HTML5, bukan? Sesungguhnya Microsoft sudah ‘beralih hati’ ke HTML5 di Windows 8! Sebuah subset WPF untuk kebutuhan web, yaitu Silverlight, telah di-‘bunuh’ oleh Microsoft (tidak akan dibuat versi barunya lagi) dan digantikan dengan HTML5. Walaupun demikian, WPF dan XAML-nya masih tetap pilihan yang masuk akal untuk membuat aplikasi desktop dengan tampilan menarik.

Pekerjaan untuk merancang tampilan di XAML, sama seperti merancang halaman web, sebenarnya lebih tepat dikerjakan oleh designer. Microsoft dulu bahkan memiliki produk designer untuk XAML yang disebut sebagai Microsoft Expression Blend. Targetnya adalah untuk para designer yang kreatif. Kini, Expression Blend sudah tidak dijual terpisah lagi melainkan terintegrasi di Visual Studo 2012 menjadi Blend for Visual Studio 2012. Perbedaan antara designer dan programmer adalah designer belum tentu dapat membuat kode program. Mereka umumnya lebih senang dengan tool visual sejenis Blend atau Photoshop. Oleh sebab itu, view pada pola MVVM tidak perlu mengandung kode program sehingga designer menjadi lebih nyaman dalam menuangkan kreatifitas mereka.

MVVM terdiri atas model, view dan view model. Saya sudah membuat model dan view. Sekarang, saya akan membuat view model. Pada dasarnya, view model adalah sesuatu yang menampung state/nilai yang ada diview sehingga command (dan controller bila ada) tidak perlu mengakses setiap control yang ada di view secara langsung. Sebagai contoh, saya akan membuat sebuah class baru dengan nama ItemPenjualanViewModel yang isinya seperti berikut ini:

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

namespace LatihanMVVM
{
    class ItemPenjualanViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private ItemPenjualan model;

        public ItemPenjualanViewModel(ItemPenjualan itemPenjualan = null)
        {
            this.model = itemPenjualan ?? new ItemPenjualan();
        }

        public string NamaBarang
        {
            get { return model.NamaBarang; }
            set
            {
                if (value != model.NamaBarang)
                {
                    model.NamaBarang = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("NamaBarang"));
                }
            }
        }

        public int Jumlah
        {
            get { return model.Jumlah; }
            set
            {
                if (value != model.Jumlah)
                {
                    model.Jumlah = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Jumlah"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public decimal Harga
        {
            get { return model.Harga; }
            set
            {
                if (value != model.Harga)
                {
                    model.Harga = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Harga"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public decimal DiskonPersen
        {
            get { return model.DiskonPersen; }
            set
            {
                if (value != model.DiskonPersen)
                {
                    model.DiskonPersen = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("DiskonPersen"));
                    PropertyChanged(this, new PropertyChangedEventArgs("Total"));
                }
            }
        }

        public string Total
        {
            get 
            {
                decimal? total = model.Total();
                if (!total.HasValue)
                {
                    return "-";
                }
                else
                {
                    return total.Value.ToString("C");
                }
            }
        }

        public ItemPenjualan Model
        {
            get { return this.model; }
        }
    }
}

Kode program view model di atas dikotori oleh kode program yang berhubungan dengan INotifyPropertyChanged. Tapi sayangnya ini adalah sebuah kebutuhan yang tidak dapat dihindari bila akan memakai binding. Untuk menyederhanakan kode program, saya dapat menggunakan framework atau membuat class infrastruktur. Sebagai contoh, Prism Library menyediakan class NotificationObject yang dapat dijadikan superclass untuk seluruh view model yang ada. Solusi yang paling elegan yang pernah saya temui adalah penggunaan annotation @Bindable yang kerap saya pakai di Griffon Framework (framework MVC untuk Groovy).

Teman saya yang sedang berkonsultasi mengutarakan pikirannya yang berbeda tentang view model. Saat mencoba memakai MVVM, ia menemukan banyak property yang sama di view model dan model. Lalu, ia mengambil sebuah jalan pintas: ia memakai model secara langsung tanpa membuat view model khusus pada beberapa screen yang sederhana. Walaupun hal ini tidak salah, solusi yang lebih konvensional adalah dengan tetap memakai view model yang hanya meng-ekspos satu property berisi model secara langsung ke view. Nantinya, view akan melakukan binding ke masing-masing property di model yang dibutuhkan. Solusi ini mensyaratkan agar model mengimplementasikan INotifyPropertyChanged. Itu sebabnya saya menghindari solusi ini: mengotori view model lebih baik daripada mengotori model 🙂 Namun solusi ini jauh lebih baik daripada melakukan binding langsung ke model tanpa melalui view model. Hal ini karena seiring dengan pertumbuhan aplikasi, kemungkinan besar suatu hari nanti screen akan memiliki state yang tidak ada di model dan lebih tepat bila dimiliki view state. Sebuah view state adalah superset dari model: selain memiliki semua property milik model, ia juga umumnya memiliki property lainnya untuk kebutuhan view.

Langkah berikutnya, saya perlu menghubungkan antara view, view model dan model. Karena model sudah dibuat secara langsung di constructor view model, maka sekarang saya perlu menghubungkan view dan view model. Saya men-double click file MainWindow.xaml.cs dan mengubah kode program yang ada menjadi seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace LatihanMVVM
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new ItemPenjualanViewModel();
        }
    }
}

Pada kode program di atas, saya menghubungkan view ke view model secara hardcoded. Ada sedikit rasa bersalah disini karena coupling-nya terlalu ‘tinggi’! Untuk solusi yang lebih baik, saya perlu menggunakan container yang menerapkan dependency injection. Kalau ini adalah Java, saya tidak akan berpikir dua kali untuk memakai Spring Framework. Lalu bagaimana dengan C#? Salah satu contoh dependency injenction container yang dapat dipakai di C# adalah Unity Container buatan Microsoft. Agar tetap sederhana, saya akan tetap melanjutkan dengan cara manual.

Salah satu ciri khas MVVM adalah perubahan pada view akan langsung diperbaharui pada view model dan begitu juga sebaliknya. Jadi, pada kode program, saya tidak perlu mengakses sebuah TextBox untuk mendapatkan nilai harga. Saya hanya perlu mengakses property Harga milik view model. Saya bahkan tidak perlu tahu apa komponen UI-nya, bisa saja bukan sebuah TextBox dan saya tidak peduli, karena yang saya butuhkan adalah nilai harganya! Agar hal ini terwujud, saya perlu melakukan apa yang disebut sebagai data binding. WPF sudah menyediakan fasilitas untuk melakukan binding. Sebagai contoh, saya mengubah MainWindow.xaml menjadi seperti berikut ini:

...
<Label Content="Nama Barang:" ... />
<TextBox Name="textBox1" ... Text="{Binding Path=NamaBarang}"/>

<Label Content="Jumlah:" ... />
<TextBox ... Text="{Binding Path=Jumlah, StringFormat={}{0:#,0}}"/>

<Label Content="Harga:" ... />
<TextBox ... Text="{Binding Path=Harga, StringFormat={}{0:C}}"/>                

<Label Content="Diskon (%):" ... />
<TextBox ... Text="{Binding Path=DiskonPersen, StringFormat={}{0:#.#}}"/>

<Label Content="Total:" ... />
<Label .... Content="{Binding Path=Total}" />
...

Cara melakukan binding di WPF boleh dibilang merupakan sesuatu yang mudah. Saya hanya perlu memberikan markup extension berupa Binding di property yang hendak di-bind. Sebagai contoh, pada kode program di atas, saya melakukan binding pada property Text milik TextBox untuk mengisi nama barang dengan property NamaBarang milik ItemPenjualanModelView selaku source-nya. Secara default, binding pada TextBox adalah two-way binding. Dengan demikian, setiap kali pengguna mengubah nilai TextBox tersebut , maka (setelah lost focus) nilai property ItemPenjualModelView.NamaBarang akan diperbaharui. Begitu juga sebaliknya, bila saya melakukan perubahan nilai ItemPenjualanModelView.NamaBarang di kode program, maka isi TextBox tersebut akan diperbaharui.

Menariknya, Binding memberikan keleluasaan untuk mengatur format dengan memakai property StringFormat. Saya juga dapat mendefinisikan nilai Converter bila perlu untuk menerjemahkan nilai secara manual. Untuk saat ini, saya secara otomatis sudah memakai type converter default yang akan menerjemahkan data angka menjadi string. Bila saya menjalankan program dan mengisi data, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan view setelah binding dengan view model

Tampilan view setelah binding dengan view model

Selain itu, sudah ada validasi bawaan bila saya memasukkan tipe data yang salah, seperti yang terlihat pada gambar berikut ini:

Tampilan view bila terjadi kesalahan konversi

Tampilan view bila terjadi kesalahan konversi

Bila garis kotak merah secara default terasa kurang indah, saya dapat mengubah view dibagian dimana saya mendeklarasikan style untuk TextBox menjadi seperti berikut ini:

...
<Style TargetType="TextBox">
   ...
   <Setter Property="Validation.ErrorTemplate">
      <Setter.Value>
         <ControlTemplate>
            <StackPanel Orientation="Horizontal">
               <AdornedElementPlaceholder />                                                            
               <TextBlock Text="Perlu diperbaiki!" Padding="3" Foreground="Red" />
            </StackPanel>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   ...
</Style>
...

Sesuai dengan konsep MVC atau MVVM, perubahan hanya perlu saya lakukan di view tanpa mempengaruhi model atau view model karena memang perubahan tersebut hanya berkaitan dengan tampilan. Bila terjadi kesalahan konversi data, saya akan memperoleh hasil seperti pada gambar berikut ini:

Kustomisasi notifikasi kesalahan di view

Kustomisasi notifikasi kesalahan di view

Berikutnya, saya perlu menyimpan model ke database. Ini adalah sebuah tugas yang jauh berbeda dari sebelumnya. Program sederhana di artikel ini, walaupun sudah bisa dijalankan dan bekerja, hanyalah sebatas tampilan. Pola MVVM memang dipakai untuk meng-implementasi-kan presentation layer. Menyimpan data ke database merupakan sesuatu yang diluar tanggung jawab MVC atau MVVM. Ia adalah tanggung jawab dari data access layer atau persistence layer. Pada .NET, saya dapat memakai Entity Framework untuk membuat persistence layer dengan mudah.

Saya sudah menuliskan bagaimana membuat persistence layer di artikel Memakai Entity Framework Di Visual Studio 2010. Seperti sebelumnya, saya mulai dengan men-install Entity Framework untuk proyek ini. Karena kali ini saya akan memakai database MySQL, saya perlu mengubah file App.config yang dihasilkan menjadi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <entityFramework>    
    <providers>
      <provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6" />      
    </providers>    
  </entityFramework>
  <connectionStrings>
    <add name="LatihanContext" connectionString="server=localhost; database=latihan; uid=steven; password=12345" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>
</configuration>

Untuk memakai database MySQL dengan pendekatan code first di EF 6, saya minimal harus memakai Connector/.NET versi 6.8 yang dapat di-download di http://dev.mysql.com/downloads/connector/net/6.8.html. Setelah proses instalasi selesai, saya perlu menambahkan referensi ke Connector/.NET dengan men-klik kanan nama proyek dan memilih menu Add Reference…. Pada tab .NET, saya memilih MySql.Data.Entity for EF6 dan men-klik tombol OK.

Selain itu, saya juga menambahkan sebuah atribut di model agar nilai property Id dihasilkan secara otomatis oleh database (melalui auto number):

...
using System.ComponentModel.DataAnnotations.Schema;

namespace LatihanMVVM
{
    public class ItemPenjualan
    {

        ...

        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long Id { get; set; }

        [StringLength(50)]
        public string NamaBarang { get; set; }

        ...

    }
}

Bila Visual Studio 2010 komplain karena tidak dapat menemukan namespace untuk atribut di atas, saya perlu menjalankan proyek (atau men-compile) agar Visual Studio 2010 memperbaharui dependencies proyek.

Berikutnya saya akan membuat class persistence context yang isinya seperti berikut ini:

using System.Data.Entity;

namespace LatihanMVVM
{
    class LatihanContext : DbContext
    {
        public DbSet<ItemPenjualan> DaftarItemPenjualan { get; set; }
    }
}

Pada DbContext, saya memakai CreateDatabaseIfNotExists sebagai initializer sehingga Connector/.NET akan membuat tabel berdasarkan model bila tabel belum ada.

Khusus untuk MySQL, mungkin karena dukungan EF 6 yang masih awal, tabel __migrationhistory yang dihasilkan secara otomatis malah memiliki kolom yang terlalu besar untuk di-index. Dengan demikian, setiap kali menyimpan data, saya akan memperoleh pesan kesalahan berupa Specified key was too long; max key length is 767 bytes. Untuk mengatasi permasalahan ini, saya perlu menambahkan sebuah HistoryContext yang isinya seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity.Migrations.History;
using System.Data.Common;
using System.Data.Entity;

namespace LatihanMVVM
{
    public class MyHistoryContext : HistoryContext
    {
        public MyHistoryContext(DbConnection dbConnection, string defaultSchema)
            : base(dbConnection, defaultSchema)
        {
        }

        protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<HistoryRow>().Property(p => p.MigrationId).HasMaxLength(100).IsRequired();
            modelBuilder.Entity<HistoryRow>().Property(p => p.ContextKey).HasMaxLength(200).IsRequired();
        }
    }

    public class ModelConfiguration : DbConfiguration
    {
        public ModelConfiguration()
        {
            SetHistoryContext("MySql.Data.MySqlClient", (c, s) => new MyHistoryContext(c, s));
        }
    }
}

Sekarang persistence layer atau data access layer sudah siap. Yang perlu saya lakukan adalah menghubungkan presentation layer ke data access layer. Proses penyimpanan ke database akan dilakukan bila pengguna men-klik tombol simpan. Sekarang, fokus pekerjaan sudah kembali lagi ke MVVM di presentation layer. Pertanyaannya adalah dimana meletakkan kode program yang berisi aksi yang dikerjakan saat tombol simpan di-klik pengguna?

Sebuah godaan besar yang kerap datang adalah dengan men-double click tombol simpan di designer dan membuat kode program di event handler yang dihasilkan. Ini adalah cara yang bertentangan dengan pola MVVM! Mengapa demikian? Karena kode program event handler tersebut masih merupakan bagian dari view. Secara ideal, sebuah view tidak boleh mengandung kode program. Bisa saja ia akan dikerjakan oleh designer yang tidak mengerti kode program. Oleh sebab itu, saya perlu meletakkan kode program di model view (atau di controller bila ada).

Salah satu cara yang disarankan adalah dengan menggunakan ICommand (sejenis Action di Java Swing). Tapi masalahnya adalah WPF tidak memiliki ICommand yang tepat untuk pola MVVM. Oleh sebab itu, framework MVVM seperti Prism biasanya memiliki implementasi ICommand yang dapat dipakai. Karena saya tidak memakai framework, maka saya perlu membuat sebuah implementasi ICommand secara manual. Perlu diingat bahwa pada kasus yang realistis, harus ada framework atau minimal implementasi sejenis DelegateCommand. Berikut ini adalah perubahan yang saya lakukan pada file ItemPenjualanViewModel.cs:

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

namespace LatihanMVVM
{
    public class ItemPenjualanViewModel : INotifyPropertyChanged
    {
    ...

        private ICommand simpanCommand;

        ...

        public ICommand SimpanCommand
        {
            get
            {
                if (this.simpanCommand == null)
                {
                    this.simpanCommand = new SimpanCommand(this);
                }
                return this.simpanCommand; 
            }
        }

    }


    public class SimpanCommand : ICommand
    {

        private ItemPenjualanViewModel viewModel;

        public SimpanCommand(ItemPenjualanViewModel viewModel)
        {
            this.viewModel = viewModel;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return viewModel.Model.Total() > 0;
        }

        public void Execute(object parameter)
        {
            using (var db = new LatihanContext())
            {
                db.Database.Log = Console.Write;
                db.DaftarItemPenjualan.Add(viewModel.Model);
                db.SaveChanges();
        MessageBox.Show("Data berhasil disimpan ke database");
            }
        }

    }
}

Pada ICommand di atas, method CanExecute() menentukan apakah aksi boleh dikerjakan atau tidak. Bila method ini mengembalikan nilai false, maka button akan di-disable sehingga pengguna tidak dapat men-klik. Method Execute() adalah kode program yang akan dikerjakan bila button di-click. Lagi-lagi saya melakukan hardcoding disini! Cara yang lebih baik adalah menempelkan presentation layer dan persistence layer melalui dependency injection container seperti Unity (mirip Spring Framework di Java).

Terakhir, saya perlu melakukan binding dari button ke ICommand yang saya buat. Untuk itu saya mengubah kode view menjadi seperti berikut ini:

...
<Button Content="Simpan" ... Command="{Binding SimpanCommand}"/>
...

Sekarang, bila saya menjalankan program, saya dapat melakukan eksekusi seperti pada gambar berikut ini:

Menyimpan objek ke database

Menyimpan objek ke database

Saya telah membuat sebuah aplikasi dimana presentation layer-nya memakai teknologi WPF dan pola MVVM. Persistence layer dari aplikasi ini memakai Entity Framework 6. Berikut ini adalah class diagram yang menunjukkan apa yang sudah saya buat sampai disini:

Class diagram

Class diagram

Memakai Entity Framework Dengan Pendekatan Database First

Pada artikel sebelumnya (Memakai Entity Framework Di Visual Studio 2010), saya memakai Entity Framework (EF) dengan pendekatan code first. Prosesnya dimulai dengan membuat domain class yang kemudian secara otomatis menghasilkan tabel di database. Kali ini saya akan mencoba pendekatan database first. Syaratnya: tabel sudah dibuat di database. Entity Framework akan menghasilkan domain class berdasarkan informasi dari tabel. Saya akan memakai database MySQL Server.

Sebelum mulai memakai MySQL di .NET, saya memastikan terlebih dahulu bahwa Connector/.NET sudah ter-install. Untuk men-install Connector/.NET, saya bisa memberikan tanda centang untuk item tersebut pada saat melakukan instalasi MySQL Server seperti yang terlihat pada gambar berikut ini:

Melakukan instalasi Connector/.NET

Melakukan instalasi Connector/.NET

Saya kemudian membuat tabel di database MySQL dengan perintah SQL seperti berikut ini:

CREATE DATABASE latihan;

GRANT ALL ON latihan.* to steven@localhost;

CREATE TABLE latihan.penjualan (
   id BIGINT(20) AUTO_INCREMENT PRIMARY KEY,
   nomor CHAR(5) NOT NULL,
   tanggal DATE NOT NULL,
   konsumen VARCHAR(100) NOT NULL,
   diskon DECIMAL DEFAULT 0
);

CREATE TABLE latihan.itemPenjualan (
   id BIGINT(20) AUTO_INCREMENT PRIMARY KEY,
   namaBarang VARCHAR(200) NOT NULL,
   jumlah INT NOT NULL,
   harga DECIMAL(18,2) NOT NULL,
   penjualanId BIGINT(20) NOT NULL,
   CONSTRAINT `FK_PENJUALAN_ID` FOREIGN KEY (penjualanId) REFERENCES penjualan(id)
);

Berikutnya, saya membuat sebuah proyek Console Application baru di Visual Studio 2010. Saya kemudian memilih menu Project, Manage NuGet Packages… untuk memakai Entity Framework versi terbaru di proyek ini.

Setelah memastikan Entity Framework telah ter-install, saya memilih men-klik kanan nama proyek dan memilih menu Add New Item... Pada dialog yang muncul, saya memilih Data, ADO.NET Entity Data Model seperti yang terlihat pada gambar berikut ini:

Membuat model baru

Membuat model baru

Saya mengisi nama model dengan TransaksiModel dan men-klik tombol Add. Pada dialog yang muncul, saya memilih Generate from database dan men-klik tombol Next seperti yang terlihat pada gambar berikut ini:

Melakukan reverse engineering dari database

Melakukan reverse engineering dari database

Visual Studio akan menampilkan dialog Choose Your Data Connection. Saya kemudian men-klik tombol New Connection…. Akan muncul dialog Connection Properties. Saya kemudian men-klik tombol Change.. pada Data source untuk memilih MySQL Database (MySQL Data Provider). Saya kemudian mengisi informasi database seperti yang terlihat pada gambar berikut ini:

Menambahkan koneksi ke MySQL Server

Menambahkan koneksi ke MySQL Server

Setelah men-klik tombol Test Connection untuk memastikan koneksi database dapat dibuat, saya kemudian men-klik tombol OK. Saya memilih Yes, include the sensitive data in the connection string.. Selain itu, pada bagian Save entity connection settings in App.Config as, saya mengisinya dengan TransaksiContext. Setelah selesai, saya men-klik tombol Next.

Saya memberi tanda centang seluruh tabel yang ada, mengisi nama namespace dan men-klik tombol Finish untuk selesai, seperti yang terlihat pada gambar berikut ini:

Memilih tabel yang akan di-reverse engineer

Memilih tabel yang akan di-reverse engineer

Visual Studio 2010 akan menampilkan diagram seperti yang terlihat pada gambar berikut ini:

Model hasil reverse engineering

Model hasil reverse engineering

Saya melakukan sedikit perubahan nama pada kedua entity tersebut agar diawali dengan huruf kapital. Caranya adalah dengan men-klik entity dan mengubah property Entity Set Name dan Name di window Properties.

Kode program yang dihasilkan (context dan entity class) terletak di sebuah file yang dapat dilihat di:

Kode program yang dihasilkan berdasarkan isi database

Kode program yang dihasilkan berdasarkan isi database

Setelah ini, saya dapat menggunakan operasi Entity Framework seperti pada sebelumnya. Sebagai contoh, kode program berikut ini akan menyimpan sebuah object ke dalam database:

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

namespace LatihanOOP
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new TransaksiContext())
            {
                Penjualan p1 = new Penjualan()
                {
                    nomor = "FJ001",
                    tanggal = new DateTime(2014, 1, 10),
                    konsumen = "Solid Snake",
                };
                p1.itempenjualan.Add(new ItemPenjualan() { namaBarang = "BRG1", jumlah = 1, harga = 100 });
                p1.itempenjualan.Add(new ItemPenjualan() { namaBarang = "BRG2", jumlah = 5, harga = 20 });

                db.Penjualan.AddObject(p1);
                db.SaveChanges();
            }
        }
    }
}

Setelah program di atas dijalankan, isi tabel di database akan terlihat seperti pada gambar berikut ini:

Sebuah objek yang tersimpan di database

Sebuah objek yang tersimpan di database

Memakai Entity Framework Di Visual Studio 2010

Dahulu kala, para programmer senang menulis ke database secara langsung melalui SQL, kapanpun dan dimanapun. Tidak lama kemudian, beberapa dari mereka merasakan ‘hukuman’ atas wewenang SQL yang disalahgunakan. Ibarat pemerintah yang mengelola negara selama sepuluh tahun tanpa aturan, visi dan misi yang jelas, beberapa dari programmer ini mulai pusing melihat kode programnya sendiri di saat tugas mereka kian berat. Apakah pada akhirnya para programmer menjadi punah? Tidak! Trend object oriented programming (OOP) kemudian booming dan banyak disebut dimana-mana. Programmer mulai melakukan enkapsulasi kode program pada class. Mereka juga menciptakan aplikasi berlapis (layer), class untuk framework (infrastruktur), dan sebagainya. Teknik OOP yang digunakan secara benar membuat para programmer menjadi disiplin. Mereka mendidik diri sendiri untuk mematuhi aturan yang mereka (atau tim mereka) buat. Walaupun mereka kehilangan keleluasaan untuk bertindak sesuka hati, pada akhirnya kode program mereka menjadi rapi dan memiliki pola yang dapat ditelusuri secara mudah. OOP ibarat polisi yang tidak segan memberi hukuman bila ada yang melanggar aturan.

Salah satu ciri penerapan OOP adalah adanya kumpulan class yang disebut sebagai domain class yang mewakili permasalahan bisnis yang sedang dihadapi. Sesungguhnya apa yang sering diberikan sebagai contoh pada mata kuliah atau buku OOP seperti Kucing, Tikus, Motor, dan Pesawat adalah bagian dari domain class. Mereka juga kerap disebut sebagai entity class. Istilah entity disini mirip seperti pada Entity-relationship model (ER model) yang dipakai untuk mendeskripsikan isi database.

Sebuah domain class atau entity class saja tidak cukup untuk membuat sebuah aplikasi. Masih banyak hal lain dalam sebuah aplikasi seperti presentation (layar form), penyimpanan data, dan sebagainya. Sebuah entity yang telah diciptakan oleh aplikasi perlu disimpan ke dalam database. Aplikasi juga perlu menampilkan satu atau lebih entity yang tersimpan di dalam database. Agar proses pengambilan dan penyimpanan data berlangsung secara transparan, saya dapat memakai Object-relational Mapping (ORM). Dengan memakai ORM, saya memiliki sebuah lapisan abstraksi dimana saya tidak perlu memberikan perintah SQL secara langsung. Tujuannya adalah agar saya bisa lebih berfokus pada object yang ada (karena ini adalah OOP)!

Selama ini, saya selalu memakai Java Persistence API (JPA) dengan Hibernate sebagai providernya. JPA adalah salah satu bagian dari Java Enterprise Edition (Java EE). Lalu bagaimana dengan C# di .NET Platform? Solusi resmi dari Microsoft adalah Entity Framework (EF). Versi awal dari EF telah disertakan sejak .NET Framework 3.5 Service Pack 1. Versi keduanya mengalami pelonjakan nomor versi yang drastis (dari versi 1 menjadi 4), Entity Framework 4.0 . EF 4.0 adalah bagian dari .NET Framework 4.0.

Bahasa Java berjalan pada Java Platform yang versi terakhirnya adalah Java 7. Tidak berbeda jauh, hasil program yang memakai fasilitas .NET harus dijalankan pada komputer yang memiliki .NET Framework. Apa versi terakhir dari .NET Framework yang ter-install di komputer saya? Untuk menjawab pertanyaan tersebut, saya dapat memberikan perintah seperti yang terlihat seperti pada gambar berikut ini:

Memeriksa versi .NET Framework

Memeriksa versi .NET Framework

Nilai Release berupa 0x5c786 (atau desimal 378758) menunjukkan bahwa saya telah men-install .NET Framework 4.5.1. Perlu diperhatikan bahwa .NET Framework dapat di-download dan di-install secara gratis tanpa pungutan biaya. Tapi saya tidak bisa senang dulu!! Walaupun .NET Framework gratis, Visual Studio adalah produk berbayar. Masalahnya: Setiap rilis Visual Studio dikaitkan dengan .NET Framework versi tertentu! Sebagai contoh, Visual Studio 2005 mendukung .NET 2.0, Visual Studio 2008 mendukung .NET 3.5, Visual Studio 2010 mendukung hingga maksimal .NET 4, dan Visual Studio 2013 yang mendukung .NET 4.5.1. Ini artinya, untuk mengembangkan aplikasi yang memakai .NET 4.5, saya harus memiliki Visual Studio 2012 atau Visual Studio 2013.

Untuk membuktikannya, saya men-klik kanan sebuah proyek dan memilih menu Properties. Kemudian, pada tab Application, di bagian Target Framework, saya hanya akan menemukan maksimal nilai .NET Framework 4 (walaupun saya sudah men-install .NET Framework 4.5.1), seperti yang terlihat pada gambar berikut ini:

Mengubah Target Framework Di Visual Studio 2010

Mengubah Target Framework Di Visual Studio 2010

Versi terbaru dari Entity Framework adalah EF 6.0.1. Lalu, bila saya tetap memakai Visual Studio 2010, apa versi Entity Framework yang dipakai? Visual Studio 2010 dilengkapi dengan EF 4.0 secara bawaan. Jangan tertipu dengan namanya: EF 4.0 adalah versi kedua karena Microsoft langsung loncat 3 angka agar nomor versi EF lebih sinkron dengan versi .NET Framework-nya. Walaupun saya men-download EF 6.0.1, saya tidak akan memperoleh fitur maksimal yang membutuhkan .NET 4.5 karena Visual Studio 2010 hanya mendukung hingga .NET 4. Selain itu, saya juga tidak bisa memperoleh tooling untuk perancangan visual terbaru (minimal harus Visual Studio 2012). Tapi setidaknya mungkin saya bisa memperoleh perbaikan di sisi runtime.

Langkah pertama yang saya lakukan untuk men-upgrade Entity Framework di Visual Studio 2010 adalah men-download NuGet Package Manager di http://visualstudiogallery.msdn.microsoft.com/27077b70-9dad-4c64-adcf-c7cf6bc9970c. NuGet adalah repository (di http://www.nuget.org/packages) semacam Apache Maven di Java. Entity Framework dan Microsoft ASP.NET MVC adalah salah satu contoh proyek yang dapat dipakai melalui NuGet. Setelah proses download selesai, saya men-double click file NuGet.Tools.vsix untuk men-install extension tersebut di Visual Studio 2010. Setelah proses instalasi selesai, bila saya membuka menu Tools, Extension Manager… di Visual Studio, saya akan menemukan sebuah entry bernama NuGet Package Manager.

Langkah berikutnya, saya akan membuat sebuah proyek Console Application untuk Visual C#. Setelah proyek selesai dibuat, bila saya memilih menu Project, saya akan menemukan item menu baru seperti yang terlihat pada gambar berikut ini:

Menu Dari NuGet

Menu Dari NuGet

Saya kemudian memilih menu Manage NuGet Packages… (saya memastikan bahwa komputer telah terhubung ke internet sebelum memilih menu ini). Saya kemudian men-klik menu Online, nuget.org untuk melihat daftar packages yang dapat saya pakai. Saya kemudian mencari Entity Framework dan men-klik tombol Install seperti yang terlihat pada gambar berikut ini:

Men-install Entity Framework Ke Proyek

Men-install Entity Framework Ke Proyek

Setelah proses instalasi selesai, saya kini siap untuk mencoba membuat program yang memakai Entity Framework. Sama seperti pada saat memakai JPA, ada beberapa pendekatan dalam memulai sebuah proyek yang memakai Entity Framework yaitu code first (membuat class terlebih dahulu), database first (membuat tabel di database terlebih dahulu), atau model first (menggambar model di Visual Studio yang kemudian akan menghasilkan database dan class). Selama ini, saya sudah terbiasa membuat kode program sehingga saya akan memakai pendekatan code first.

Saya mulai dengan membuat class ItemPenjualan yang isinya seperti berikut ini:

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

namespace LatihanOOP
{
    class ItemPenjualan
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int ID { get; set; }

        [Required]
        [MinLength(5), MaxLength(100)]
        public string NamaBarang { get; set; }

        public int Jumlah { get; set; }

        public decimal Harga { get; set; }

        public decimal Total()
        {
            return Jumlah * Harga;
        }
    }
}

Berikutnya, saya membuat sebuah class Penjualan yang mengandung (komposisi) satu atau lebih ItemPenjualan seperti yang terlihat pada kode program berikut ini:

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

namespace LatihanOOP
{
    class Penjualan
    {        
        public Penjualan()
        {
            DiskonPersen = 0;            
        }

        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int ID { get; set; }

        [MinLength(4), MaxLength(4)]
        public string Nomor { get; set; }

        [Required]
        public DateTime Tanggal { get; set; }

        [Required]
        [MinLength(3), MaxLength(200)]
        public string Konsumen { get; set; }

        public decimal DiskonPersen { get; set; }

        public virtual List<ItemPenjualan> ListItemPenjualan { get; set; }              

        public decimal Total()
        {
            decimal total = ListItemPenjualan.Sum(i => i.Total());
            return total - (DiskonPersen / 100 * total);
        }

        public override string ToString()
        {
            return string.Format("Nomor: {0}, Tanggal: {1}, Konsumen: {2}",
                Nomor, Tanggal.ToString("dd-MM-yyyy"), Konsumen);
        }
    }
}

Pada pendekatan code first, sebuah property di class yang namanya ID atau nama class diikuti dengan ID akan dianggap sebagai primary key. Dengan demikian, pada kode program di atas PenjualanID akan menjadi primary key di tabel yang dihasilkan nanti. Selain itu, property ListItemPenjualan memiliki keyword virtual sehingga dapat di-override untuk mengimplementasikan proxy sehingga menciptakanlazy loading. Dalam kosa kata ORM, lazy loading berarti nilai property ListItemPenjualan tidak di-isi dari database saat sebuah Penjualan di-query. Nilai property tersebut baru akan diambil dari database hanya bila ia dibutuhkan (saat di-akses melalui getter-nya).

Salah satu hal penting yang perlu diperhatikan pada ORM adalah nagivasi. Sebuah entity umumnya dapat melakukan navigasi ke entity lainnya melalui property. Hal ini mirip seperti join antar tabel pada database relasional. Sebagai contoh, sebuah entity Penjualan dapat melakukan navigasi ke entity ItemPenjualan yang dimilikinya melalui property ListItemPenjualan. Akan tetapi hal sebaliknya tidak berlaku: sebuah entity ItemPenjualan tidak dapat melakukan navigasi ke Penjualan yang berhubungan dengannya. Navigasi seperti ini disebut sebagai unidirectional. Bila navigasi berlaku dua arah dimana ItemPenjualan juga dapat mengakses Penjualan, maka ia disebut sebagai bidirectional.

Berikutnya, saya membuat sebuah class yang diturunkan dari DbContext seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;

namespace LatihanOOP
{
    class LatihanContext : DbContext
    {
        public LatihanContext() : base("DatabaseLatihan") { }

        public DbSet<Penjualan> DaftarPenjualan { get; set; }

    }
}

Instance dari DbContext ini akan mewakili seluruh operasi yang berkaitan dengan baca tulis object ke database. Pada JPA, class ini setara dengan sebuah persistence context yang diwakili oleh EntityManagerFactory. Pada kode program di atas, saya memanggil superclass dengan nilai constructor "DatabaseLatihan". Dengan demikian, EF akan menggunakan database dengan nama DatabaseLatihan. Lalu bagaimana dengan pengaturan lainnya seperti database apa yang akan dipakai, nama user, password, dan sebagainya? EF akan memakai salah satu database Microsoft SQL Server Express atau LocalDB yang ditemukan. Karena Visual Studio 2010 memiliki database SQL Server 2008 Express secara bawaan, maka database tersebut secara otomatis akan dipakai. Kenapa bukan MySQL atau JavaDB yang otomatis dipakai? Tidak ada yang protes soal monopoli Microsoft lagi? 😉

Saya dapat melihat informasi lebih detail dengan membuka file App.config. Pada proyek saya, isi file tersebut terlihat seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" ... />
  </configSections>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />    
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>

Untuk mengubah database yang dipakai, saya perlu memodifikasi nilai atribut type pada node defaultConnectionFactory. Pada konfigurasi saya, yang dipakai adalah class System.Data.Entity.Infrastructure.SqlConnectionFactory. Class factory ini akan memakai database Microsoft SQL Server dengan nama .\SQLEXPRESS. Bila ingin memakai instance database lain, saya perlu menambahkan node parameters untuk mengisi constructor factory tersebut dengan nama instance SQL Server atau mendeklarasikan connection string secara eksplisit.

Berikutnya, saya akan membuat kode program yang menyimpan beberapa object ke database seperti berikut ini:

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

namespace LatihanOOP
{
    class Program
    {
        static void Main(string[] args)
        {
            Penjualan p1 = new Penjualan() { Nomor = "F001", Konsumen = "Snake", Tanggal = new DateTime(2014, 1, 1) };
            p1.ListItemPenjualan = new List<ItemPenjualan>()
            {
                new ItemPenjualan() { NamaBarang = "SOCOM Pistol", Jumlah = 1, Harga = 10 },
                new ItemPenjualan() { NamaBarang = "ID Card", Jumlah = 2, Harga = 20 },
                new ItemPenjualan() { NamaBarang = "Optical Disc", Jumlah = 1, Harga = 50 }
            };           

            Penjualan p2 = new Penjualan() { Nomor = "F002", Konsumen = "Liquid Snake", Tanggal = new DateTime(2014, 1, 5) };
            p2.ListItemPenjualan = new List<ItemPenjualan>()
            {
                new ItemPenjualan() { NamaBarang = "Ration", Jumlah = 10, Harga = 5 },
                new ItemPenjualan() { NamaBarang = "Stealth Camouflage", Jumlah = 1, Harga = 300 }
            };
            p2.DiskonPersen = 5;

            Penjualan p3 = new Penjualan() { Nomor = "F003", Konsumen = "Big Boss", Tanggal = new DateTime(2014, 3, 15) };
            p3.ListItemPenjualan = new List<ItemPenjualan>()
            {
                new ItemPenjualan() { NamaBarang = "Thermal Goggles", Jumlah = 1, Harga = 100 },
                new ItemPenjualan() { NamaBarang = "Night Vision Goggles", Jumlah = 1, Harga = 120 },
                new ItemPenjualan() { NamaBarang = "Body Armour", Jumlah = 20, Harga = 10 },
                new ItemPenjualan() { NamaBarang = "Cigarettes", Jumlah = 100, Harga = 2 }
            };
            p3.DiskonPersen = 10.5M;

            Console.WriteLine("Sedang menyimpan object ke database...");

            using (var db = new LatihanContext())
            {
                db.DaftarPenjualan.Add(p1);
                db.DaftarPenjualan.Add(p2);
                db.DaftarPenjualan.Add(p3);
                db.SaveChanges();
            }

            Console.WriteLine("Object berhasil disimpan ke database.");
            Console.ReadKey();
        }
    }
}

Pada saat program di atas dijalankan, Entity Framework akan membuat sebuah database dengan nama DatabaseLatihan (sesuai nilai constructor untuk superclass di LatihanContext). Ia juga akan membuat tabel yang dibutuhkan, seperti yang terlihat pada tampilan Microsoft SQL Server Management Studio seperti pada gambar berikut ini:

Schema yang dihasilkan oleh Entity Framework

Schema yang dihasilkan oleh Entity Framework

Tiga object Penjualan pada program di atas akan tersimpan dalam bentuk record di tabel dan setiap tabel dapat memiliki hubungan dengan tabel lainnya (database relasional) seperti yang terlihat pada gambar berikut ini:

Isi tabel setelah penyimpanan object

Isi tabel setelah penyimpanan object

Sekarang, saya akan mencoba membuat kode program yang mengambil object dari database seperti berikut ini:

using (var db = new LatihanContext())
{
    var hasil = from p in db.DaftarPenjualan
                where p.Tanggal.Month == 1 && p.Tanggal.Year == 2014
                select p
                ;

    Console.WriteLine("Daftar Penjualan Di Bulan Januari 2014:");
    foreach (Penjualan p in hasil)
    {
        Console.WriteLine(p);
    }
}
Console.ReadKey();

Hasil dari kode program di atas adalah:

Daftar Penjualan Di Bulan Januari 2014:
Nomor: F001, Tanggal: 01-01-2014, Konsumen: Snake
Nomor: F002, Tanggal: 05-01-2014, Konsumen: Liquid Snake

Pada kode program di atas, saya memakai LINQ untuk mencari seluruh object Penjualan di bulan Januari 2014. Sangat sederhana bukan? Tapi seorang paranoid akan berkata: “LINQ memang indah, tapi jelas ia tidak dapat menggantikan SQL!” Oleh sebab itu, Entity Framework juga tetap memberikan kesempatan untuk menggunakan perintah SQL. Sebagai contoh, kode program berikut ini memiliki hasil yang sama seperti sebelumnya, tetapi tidak memakai LINQ melainkan SQL:

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

namespace LatihanOOP
{
    class Program
    {
        static void Main(string[] args)
        {

            using (var db = new LatihanContext())
            {

                var hasil = db.DaftarPenjualan.SqlQuery(
                    "SELECT * FROM Penjualans WHERE MONTH(Tanggal) = 1 AND YEAR(Tanggal) = 2014");

                Console.WriteLine("Daftar Penjualan Di Bulan Januari 2014:");
                foreach (Penjualan p in hasil)
                {
                    Console.WriteLine(p);
                }

            }

            Console.ReadKey();
        }
    }
}

Melihat LINQ Di .NET Dari Sudut Pandang Groovy

LINQ (Language-Integrated Query) dieja menyerupai ‘link’ adalah sebuah fitur unik di .NET Framework yang berkaitan dengan lambda expression. LINQ diperkenalkan sejak .NET Framework 3.5 dan Visual Studio 2008. Fitur ini dapat dipakai di bahasa untuk .NET Framework seperti C# dan Visual Basic. Pada kesempatan ini, saya akan mencoba memakai LINQ di C#. Karena fitur seperti ini tidak ada di Java 7 (yang belum mendukung lambda expression), maka saya akan mencoba membandingkannya dengan fitur serupa di Groovy.

Satu hal yang perlu disadari dari awal adalah LINQ hanyalah sebuah spesifikasi atau API. Programmer yang memakai LINQ harus menggunakan salah satu provider yang mengimplementasikan LINQ. Pada C# terdapat beberapa provider LINQ seperti LINQ to Objects, LINQ to XML, LINQ to SQL, dan LINQ to DataSets. Bila programmer merasa ini tidak cukup, ia boleh saja membuat provider baru yang menimplementasikan interface yang disediakan oleh LINQ. Pada artikel ini, saya akan memakai provider LINQ to Objects.

Saya akan mulai dengan membuat sebuah class, misalnya class ItemPenjualan yang isinya seperti berikut ini:

class ItemPenjualan
{
    public string NamaBarang { get; set; }

    public int Jumlah { get; set; }

    public decimal Harga { get; set; }

    public decimal Total()
    {
        return Jumlah * Harga;
    }
}

Class di atas mewakili setiap baris dalam faktur. Tentunya baris-baris faktur itu tidak dapat berdiri sendiri bila tidak ditampung oleh sebuah class yang mewakili faktur, misalnya Penjualan, seperti berikut ini:

class Penjualan
{
    private List<ItemPenjualan> listItemPenjualan;

    public Penjualan()
    {
        DiskonPersen = 0;
        listItemPenjualan = new List<ItemPenjualan>();
    }

    public string Nomor { get; set; }

    public DateTime Tanggal { get; set; }

    public string Konsumen { get; set; }

    public decimal DiskonPersen { get; set; }

    public List<ItemPenjualan> ListItemPenjualan
    {
        get
        {
            return listItemPenjualan;
        }
    }

    public void TambahItemPenjualan(ItemPenjualan itemPenjualan)
    {
        listItemPenjualan.Add(itemPenjualan);
    }

    public decimal Total()
    {
        decimal total = ListItemPenjualan.Sum(i => i.Total());
        return total - (DiskonPersen / 100 * total);
    }

    public override string ToString()
    {
        return string.Format("Nomor: {0}, Tanggal: {1}, Konsumen: {2}, Total: {3}",
            Nomor, Tanggal.ToString("dd-MM-yyyy"), Konsumen, Total());
    }
}

Sekarang, pada Program.cs (definisi class utama), saya mendeklarasikan beberapa object dari kedua class di atas, seperti berikut ini:

class Program
{
    static void Main(string[] args)
    {
        Penjualan p1 = new Penjualan() { Nomor = "F001", Konsumen = "Snake", Tanggal = new DateTime(2014, 1, 1) };
        p1.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "SOCOM Pistol", Jumlah = 1, Harga = 10 });
        p1.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "ID Card", Jumlah = 2, Harga = 20 });
        p1.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Optical Disc", Jumlah = 1, Harga = 50 });            

        Penjualan p2 = new Penjualan() { Nomor = "F002", Konsumen = "Liquid Snake", Tanggal = new DateTime(2014, 1, 5) };
        p2.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Ration", Jumlah = 10, Harga = 5 });
        p2.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Stealth Camouflage", Jumlah = 1, Harga = 300 });
        p2.DiskonPersen = 5;

        Penjualan p3 = new Penjualan() { Nomor = "F003", Konsumen = "Big Boss", Tanggal = new DateTime(2014, 3, 15) };
        p3.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Thermal Goggles", Jumlah = 1, Harga = 100 });
        p3.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Night Vision Goggles", Jumlah = 1, Harga = 120 });
        p3.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Body Armour", Jumlah = 20, Harga = 10 });
        p3.TambahItemPenjualan(new ItemPenjualan() { NamaBarang = "Cigarettes", Jumlah = 100, Harga = 2 });
        p3.DiskonPersen = 10.5M;

        List<Penjualan> semuaPenjualan = new List<Penjualan>() { p1, p2, p3 };

    }
}

Pada kode program di atas, terdapat tiga (3) object Penjualan yaitu p1, p2, p3. Ketiga object tersebut ditampung dalam sebuah List yang diberi nama semuaPenjualan. Pada kasus nyata, List akan diperoleh dari berbagai sumber atau setelah melewati berbagai pengolahan. Tapi yang pasti, setelah diperoleh, List tersebut perlu dipakai untuk menjawab pertanyaan, bukan?

Sebagai contoh, saya ingin mengetahui Penjualan apa saja yang terjadi di bulan Januari 2014. Saya dapat melakukannya dengan memanggil method LINQ seperti berikut ini:

var hasilQuery = semuaPenjualan.Where(p => p.Tanggal.Month == 1 && p.Tanggal.Year == 2014);

foreach (var p in hasilQuery)
{
    Console.WriteLine(p);
}

Output dari kode program di atas adalah:

Nomor: F001, Tanggal: 01-01-2014, Konsumen: Snake, Total: 100
Nomor: F002, Tanggal: 05-01-2014, Konsumen: Liquid Snake, Total: 332,50

Hal ini tidak dapat dilakukan di Java. Tapi ini adalah hal biasa di Groovy! Sebagai contoh, pada Groovy, saya dapat membuat kode program seperti berikut ini:

import groovy.transform.ToString
import java.text.SimpleDateFormat

@ToString
class ItemPenjualan {

    String namaBarang

    Integer jumlah

    BigDecimal harga

    public BigDecimal total() {
        jumlah * harga;
    }

}

@ToString(excludes = 'listItemPenjualan')
class Penjualan {

    String nomor

    Date tanggal

    String konsumen

    Double diskonPersen = 0

    List<ItemPenjualan> listItemPenjualan = []

    void tambahItemPenjualan(ItemPenjualan itemPenjualan) {
        listItemPenjualan << itemPenjualan;
    }

    BigDecimal total() {
        def total = listItemPenjualan.sum { it.total() }
        total - (diskonPersen / 100 * total)
    }

}

SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy")

Penjualan p1 = new Penjualan(nomor: "F001", konsumen: "Snake", tanggal: df.parse("01-01-2014"))
p1.tambahItemPenjualan(new ItemPenjualan(namaBarang: "SOCOM Pistol", jumlah: 1, harga: 10))
p1.tambahItemPenjualan(new ItemPenjualan(namaBarang: "ID Card", jumlah: 2, harga: 20))
p1.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Optical Disc", jumlah: 1, harga: 50))

Penjualan p2 = new Penjualan(nomor: "F002", konsumen: "Liquid Snake", tanggal: df.parse("05-01-2014"))
p2.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Ration", jumlah: 10, harga: 5))
p2.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Stealth Camouflage", jumlah: 1, harga: 300))
p2.diskonPersen = 5

Penjualan p3 = new Penjualan(nomor: "F003", konsumen: "Big Boss", tanggal: df.parse("15-02-2014"))
p3.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Thermal Goggles", jumlah: 1, harga: 100))
p3.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Night Vision Goggles", jumlah: 1, harga: 120))
p3.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Body Armour", jumlah: 20, harga: 10))
p3.tambahItemPenjualan(new ItemPenjualan(namaBarang: "Cigarettes", jumlah: 100, harga: 2))
p3.diskonPersen = 10.5

List<Penjualan> semuaPenjualan = [p1, p2, p3]

Calendar c = Calendar.getInstance()

semuaPenjualan.findAll {
    c.setTime(it.tanggal)
    c.get(Calendar.MONTH)==0 && c.get(Calendar.YEAR)==2014
}.each {
    println it
}

Walaupun kode program untuk penanganan tanggal di Groovy terlihat lebih kompleks (itu sebabnya saya selalu memakai Joda Time dan ini akan diperbaiki di Java 8 nanti), pada dasarnya proses query di Groovy hampir sama seperti pada penggunaan LINQ.

Tapi ada satu hal yang membuat LINQ unik. Ia juga dapat dipakai dalam bentuk syntax seperti SQL. Berbeda dengan SQL yang kerap diwakili sebuah string, syntax LINQ merupakan bagian dari syntax bahasa. Sebagai contoh, saya dapat menulis ulang versi LINQ sebelumnya menjadi seperti kode program C# berikut ini:

var hasilQuery = from p in semuaPenjualan
                 where 
                    p.Tanggal.Month == 1 && 
                    p.Tanggal.Year == 2014
                 select p                     
                 ;

foreach (var p in hasilQuery)
{
    Console.WriteLine(p);
}

Keyword from, where dan select pada LINQ di atas adalah bagian dari struktur bahasa C#. Microsoft menyarankan agar LINQ dipakai melalui syntax seperti di atas. Walaupun demikian, saya sering merasa penggunaan syntax LINQ seperti di atas membuat kode program terlihat lebih ‘gendut’.

Saya tidak dapat melakukan hal serupa di Groovy. Yang paling mendekati adalah dengan membuat DSL (domain specific language) sendiri seperti yang saya lakukan di simple-jpa, tapi saya tetap terbatas pada syntax dan semantic Groovy.

LINQ terlihat agak mirip SQL, bukan? Sebagai contoh, berikut ini adalah kode program C# yang menghitung total penjualan di bulan Januari 2014:

decimal totalJanuari =
    (from p in semuaPenjualan
     where
        p.Tanggal.Month == 1 &&
        p.Tanggal.Year == 2014
     select p).Sum(p => p.Total());   

Console.WriteLine("Total penjualan Januari 2014 adalah {0}", totalJanuari);

Kode program di atas setera dengan kode program Groovy berikut ini:

def totalJanuari = semuaPenjualan.findAll {
        c.setTime(it.tanggal)
        c.get(Calendar.MONTH)==0 && c.get(Calendar.YEAR)==2014
    }.sum { it.total() }
println "Total penjualan Januari 2014 adalah $totalJanuari"

Bila saya ingin mengetahui total penjualan per bulan, saya dapat menggunakan LINQ seperti pada kode program C# berikut ini:

var hasilQuery = from p in semuaPenjualan
                 group p by p.Tanggal.Month into groupBulan
                 select new { bulan = groupBulan.Key, total = groupBulan.Sum(p => p.Total()) }
                 ;

foreach (var p in hasilQuery)
{
    Console.WriteLine(p);
}

Hasil dari kode program C# di atas akan terlihat seperti berikut ini:

{ bulan = 1, total = 432,50 }
{ bulan = 3, total = 554,900 }

Karena Groovy tidak memiliki fitur serupa, yang paling mendekati adalah kode program seperti berikut ini:

def hasil = semuaPenjualan.groupBy {
    c.setTime(it.tanggal)
    c.get(Calendar.MONTH)
}.collect {
    new Expando(bulan: it.key, total: it.value.sum { it.total() })
}

println hasil

Hasil dari kode program Groovy di atas adalah:

[{bulan=0, total=432.5}, {bulan=1, total=554.8999999999999772626324556767940521240234375}]

Bagi yang sudah terbiasa dengan syntax SQL, kode program C# yang memakai LINQ akan terlihat lebih mudah dipahami.

Memakai Type Converter Di User Control C#

Komponen UI di .NET biasanya bersifat user-friendly dan mudah dipakai. Platform .NET bahkan menyertakan fasilitas untuk memprogram perilaku user control pada saat design time. Salah satu contohnya adalah fasilitas type converter yang akan saya pakai pada artikel ini. Pada artikel sebelumnya (Membuat User Control Untuk Windows Form Di C#), saya membuat sebuah user control bernama SimpleObjectEditor yang memiliki sebuah property bernama DomainObject. Saya tidak dapat mengisi nilai property tersebut melalui Properties window di Visual Studio karena nilai yang diberikan harus berupa sebuah objek. Dengan demikian, pengguna harus menjalankan program agar memperoleh visualisasi dari user control ini. Catatan: pengguna user control adalah programmer lain yang memakai user control tersebut bukan pengguna aplikasi yang dihasilkan.

Salah satu solusi agar pengguna dapat mengisi property DomainObject saat merancang form adalah dengan membuat sebuah type converter yang dapat menerjemahkan string berupa nama class menjadi instance dari class tersebut. Untuk itu saya perlu membuat sebuah class baru yang diturunkan dari TypeConverter. Caranya adalah dengan men-klik kanan nama proyek SimpleObjectEditor, memilih menu Add, Class… dan mengisi nama class dengan DomainObjectConverter. Saya kemudian membuat kode program seperti berikut ini:

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

namespace SimpleObjectEditor
{
    class DomainObjectConverter : TypeConverter
    {        
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }
            return base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            if (value is string)
            {
                ConstructorInfo c = Type.GetType(value as string).GetConstructor(Type.EmptyTypes);
                return c.Invoke(new object[]{});
            }
            return base.ConvertFrom(context, culture, value);
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                return value.GetType().FullName;
            }
            return base.ConvertTo(context, culture, value, destinationType);
        }

    }
}

Pada TypeConverter di atas, saya akan membuat instance baru berdasarkan nama class yang diberikan. Programmer harus mengetikkan nama class yang lengkap beserta dengan namespace. Selain itu, domain object yang dipakai juga minimal harus memiliki default constructor (sebuah constructor tanpa parameter).

Langkah berikutnya, saya perlu memberitahu user control agar memakai TypeConverter yang barusan saya buat dengan menggunakan attribute. Pada Java, attribute sangat mirip seperti annotations. Bedanya, di C#, sebuah attribute diawali dan ditutup dengan kurung siku, seperti ([...]). Sebagai contoh, attribute [Obsolete] di C# mirip seperti @Deprecated di Java. Agar user control SimpleObjectEditor memakai TypeConverter di atas, saya perlu menggunakan attribute TypeConverter seperti yang terlihat seperti berikut ini:

public partial class SimpleObjectEditor : UserControl
{
   ...

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

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

   ...
}

Selain itu, saya boleh menghapus variabel designMode. Saya perlu memperbaharui method UpdateComponents() sehingga perilaku SimpleObjectEditor tidak perlu berbeda lagi saat design time dan run time:

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

    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();
        }

    };

}

Sekarang, bila saya melihat property milik SimpleObjectEditor di window Properties, saya akan menemukan hasil seperti pada gambar berikut ini:

Tampilan di window Property

Tampilan di window Property

Penggunaan attribute [ParenthesizePropertyName(true)] menyebabkan property DomainObject memiliki tanda kurung dan ditampilkan pada baris awal. Ini sangat berguna untuk menandakan bahwa property tersebut adalah sebuah property penting. Selain itu, penggunaan attribute [Description] akan memperlihatkan teks deskripsi di bagian bahwa window Properties.

Sekarang, saya dapat mengetik dan mengisi property DomainObject dengan nilai SimpleObjectEditorTest.Mahasiswa seperti yang terlihat pada gambar berikut ini:

Mengisi property saat design time

Mengisi property saat design time

Perhatikan bahwa kini tampilan SimpleObjectEditor akan langsung diperbaharui sesuai dengan nilai yang barusan saya berikan sebelumnya.

Bila saya mengubah property DomainObject dengan nilai lain, misalnya SimpleObjectEditorTest.ItemPenjualan, saya akan memperoleh tampilan SimpleObjectEditor yang berbeda seperti yang terlihat pada gambar berikut ini:

Mengisi property saat design time

Mengisi property saat design time

Kesimpulannya: Dukungan design time tidak tersedia di platform Java, melainkan dibuat oleh masing-masing IDE seperti NetBeans atau IntelliJ IDEA! .NET memiliki sifat sebaliknya. Dukungan design time bukan bagian dari Visual Studio, melainkan bagian dari .NET. Tujuannya adalah agar IDE lain juga dapat memanfaatkan dukungan design time yang ada secara seragam. Walaupun demikian, boleh dibilang Visual Studio merupakan satu-satunya IDE yang paling sering dipakai oleh programmer .NET.