Memakai UserName Authentication Di WSHttpBinding Pada WCF

Pada artikel ini, saya akan mencoba memakai fasilitas authentication dari WCF untuk membatasi agar hanya pengguna tertentu saja yang boleh mengakses web service. Untuk itu, saya mulai dengan membuat sebuah console application C# yang berfungsi sebagai web service provider. Saya tidak lupa menambahkan referensi ke System.ServiceModel dengan men-klik kanan proyek dan memilih menu Add Reference…. Saya kemudian membuat kode program sederhana seperti berikut ini:

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

namespace LatihanWCFAuthentication
{
    [ServiceContract]
    public class Hitung
    {
        [OperationContract]
        public int tambah(int angka1, int angka2)
        {
            return angka1 + angka2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Uri baseAddress = new Uri("http://localhost:80/Latihan");
            ServiceHost selfHost = new ServiceHost(typeof(Hitung), baseAddress);
            try
            {
                WSHttpBinding binding = new WSHttpBinding();                
                selfHost.AddServiceEndpoint(typeof(Hitung), binding, "LatihanService");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;
                selfHost.Description.Behaviors.Add(smb);                
                selfHost.Open();
                Console.WriteLine("Web services 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();
        }
    }
}

Berikutnya, saya membuat sebuah proyek Console Application baru yang akan berperan sebagai web service consumer. Untuk menambahkan client proxy ke proyek baru ini, saya perlu menjalankan proyek yang berperan sebagai web service provider terlebih dahulu dengan memilih menu Debug, Start Without Debugging…. Setelah itu, saya men-klik kanan pada proyek web service consumer dan memilih menu Add Service Reference…. Saya memasukkan nilai http://localhost:80/Latihan pada Address dan WebServiceLatihan pada Namespace. Setelah itu, saya men-klik tombol OK. Saya kemudian mengubah isi Program.cs menjadi seperti berikut ini:

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

namespace Consumer
{
    class Program
    {
        static void Main(string[] args)
        {
            HitungClient client = new HitungClient();
            int angka1 = 10, angka2 = 20;
            int hasil = client.tambah(angka1, angka2);
            Console.WriteLine("Hasil = {0}", hasil);
        }
    }
}

Bila saya menjalankan program (dengan menekan tombol Ctrl+F5), saya akan memperoleh hasil seperti:

Hasil = 30

Terlihat bahwa web services dapat dipanggil oleh siapa saja secara bebas. Ada kalanya dibutuhkan pembatasan agar web service hanya boleh dipanggil oleh pihak berwenang. Salah satu solusi yang bisa dipakai adalah fasilitas authentication bawaan dari WCF. Fasilitas security yang ditawarkan oleh WCF berupa keamanan pada transport layer (HTTPS), message, atau keduanya. Fasilitas tersebut masing-masing diwakili oleh nilai System.ServiceModel.SecurityMode seperti None, Transport, Message dan TransportWithMessageCredential. Nilai SecurityMode.Transport menawarkan keamanan yang paling transparan karena memakai SSL. Selain SecurityMode.Transport, juga ada SecurityMode.Message yang menambahkan security data pada setiap service yang dipanggil (bekerja pada application layer bukan transport layer).

Karena ingin authentication berdasarkan username, saya perlu memakai SecurityMode.Message dengan menambahkan baris berikut ini pada web service provider:

binding.Security.Mode = SecurityMode.Message;

Berikutnya, saya perlu menentukan jenis authentication yang perlu dilakukan. Karena saya ingin authentication berdasarkan nama user di sistem operasi Windows, saya bisa menambahkan baris berikut ini:

binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;

Salah satu hal yang cukup merepotkan adalah fasilitas keamanan SecurityMode.Message yang membutuhkan sertifikat digital X.509. Sertifikat ini dibutuhkan untuk melakukan enkripsi data. Hal ini memang lebih aman karena data yang dikirim tidak dapat dilihat dengan mudah oleh pihak ketiga (misalnya melalui Wireshark), tapi juga berarti saya harus melakukan setup yang lebih rumit. Untuk membuat sebuah sertifikat sementara yang dipakai untuk keperluan percobaan, saya akan membuka Visual Studio 2010 Command Prompt dan memberikan perintah berikut ini:

MakeCert -n "CN=TestingCA" -r -sv TestingCA.pvk TestingCA.cer

Saya perlu membuat sebuah password baru pada dialog yang muncul. Setelah perintah ini selesai dikerjakan, saya akan menemukan private key di file TestingCA.pvk dan sebuah public key di TestingCA.cer. Digital certificate ini akan berperan sebagai CA Root Certificate. Bila saya men-double click file TestingCA.cer, saya akan memperoleh tampilan seperti pada gambar berikut ini:

Self-signed root certificate digital

Self-signed root certificate digital

Windows memberikan pesan peringatan karena Testing CA bukanlah penyedia root certificate yang terpercaya. Pada lingkungan produksi, saya perlu membeli sertifikat digital dari penyedia terpercaya seperti VeriSign, GoDaddy, dan sebagainya. Karena ini hanya untuk latihan, saya tetap men-klik tombol Install Certificate…. Pada wizard yang muncul, saya men-klik tombol Next hingga Finish tanpa melakukan perubahan. Setelah selesai, root certificate akan ter-install pada komputer server. Bila komputer client adalah komputer yang terpisah, saya juga perlu men-install root certificate ini pada komputer client.

Setelah itu, saya perlu membuat sertifikat digital lain yang di-sign oleh private key milik root certificate yang dibuat sebelumnya. Untuk itu, saya memberikan perintah berikut ini:

MakeCert -sk SolidSnake -iv TestingCA.pvk -n "CN=SolidSnake" -ic TestingCA.cer -ss My -sky exchange -pe

Saya akan diminta untuk memasukkan password yang isinya harus sama dengan password yang saya berikan pada root certificate.

Penggunaan -sky exchange menunjukkan bahwa sertifikat digital ini dipakai untuk enkripsi data bukan sebagai digital signature (tanda tangan digital). Sertifikat digital ini akan langsung di-install pada lokasi Personal (terlihat dari penggunaan -ss My). Penggunaan -pe akan membuat private key disertakan di dalam sertifikat digital.

Untuk memakai sertifikat digital tersebut sebagai service certificate, saya dapat menambahkan baris program berikut ini:

selfHost.Credentials.ServiceCertificate.SetCertificate(StoreLocation.CurrentUser,
   StoreName.My, X509FindType.FindBySubjectName, "SolidSnake");                

Kode program web service provider saya secara lengkap kini akan terlihat seperti pada contoh berikut ini:

using System.ServiceModel;
using System;
using System.ServiceModel.Description;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Security.Cryptography.X509Certificates;

namespace LatihanWCFAuthentication
{
    [ServiceContract]
    public class Hitung
    {
        [OperationContract]
        public int tambah(int angka1, int angka2)
        {
            return angka1 + angka2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Uri baseAddress = new Uri("http://localhost:80/Latihan");
            ServiceHost selfHost = new ServiceHost(typeof(Hitung), baseAddress);
            try
            {
                WSHttpBinding binding = new WSHttpBinding();
                binding.Security.Mode = SecurityMode.Message;
                binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
                selfHost.AddServiceEndpoint(typeof(Hitung), binding, "LatihanService");
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;
                selfHost.Credentials.ServiceCertificate.SetCertificate(StoreLocation.CurrentUser,
                    StoreName.My, X509FindType.FindBySubjectName, "SolidSnake");                
                selfHost.Description.Behaviors.Add(smb);                
                selfHost.Open();
                Console.WriteLine("Web services sudah aktif dan siap melayani request.");
                while (true)
                {
                    System.Threading.Thread.Sleep(50);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Telah terjadi kesalahan: {0}", ex);                
                selfHost.Abort();
            }
            Console.ReadLine();
        }
    }
}

Saya kemudian menjalankan kode program web service provider dan memastikan bahwa tidak ada pesan kesalahan yang muncul.

Yang berbeda kali ini adalah bila saya menjalankan kode program web service consumer, saya akan memperoleh pesan kesalahan. Saya tidak bisa lagi mengerjakan service begitu saja. Web service consumer kini perlu melakukan authentication berdasarkan nama user dan password yang dapat dipakai untuk login ke sistem operasi Windows di web service provider. Tapi sebelumnya, saya perlu memperbaharui client proxy dengan men-klik kanan nama web service di proyek web service consumer dan memilih menu Update Service Reference. Setelah itu, saya mengubah kode program web service consumer menjadi seperti berikut ini:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Consumer.WebServiceLatihan;
using System.ServiceModel;
using System.ServiceModel.Security;

namespace Consumer
{
    class Program
    {
        static void Main(string[] args)
        {
            HitungClient client = new HitungClient();
            WSHttpBinding binding = (WSHttpBinding) client.Endpoint.Binding;
            binding.Security.Mode = SecurityMode.Message;
            binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
            client.ClientCredentials.UserName.UserName = "nama_user_di_komputer_provider";
            client.ClientCredentials.UserName.Password = "password_untuk_user_tersebut";
            client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = 
                X509CertificateValidationMode.None;

            int angka1 = 10, angka2 = 20;
            int hasil = client.tambah(angka1, angka2);
            Console.WriteLine("Hasil = {0}", hasil);
        }
    }
}

Pada kode program di atas, saya menyertakan nama dan password untuk sebuah user yang dapat login di sistem operasi Windows di web service provider. Selain itu, karena saya memakai root certificate yang saya buat sendiri (self-signed), maka saya mematikan fasilitas validasi sertifikat dengan mengubah CertificateValidationMode menjadi X509CertifateValidationMode.NONE. Bila saya menjalankan program, maka web service kembali dapat dipanggil seperti biasa. Akan tetapi, bila memasukkan nama atau password yang salah, saya akan memperoleh pesan kesalahan seperti berikut ini:

Unhandled Exception: System.ServiceModel.Security.MessageSecurityException: An unsecured or incorrectly secured fault was received from the other party. See the
 inner FaultException for the fault code and detail. ---> System.ServiceModel.FaultException: At least one security token in the message could not be validated.
Iklan

Membuat Program C# Yang Membaca Data UserAssist

Sistem operasi Windows 7 adalah sebuah sistem operasi yang ‘pintar’. Contohnya, Windows memiliki fitur SuperFetch yang akan menganalisa pola penggunaan program berdasarkan hari dan jam sehingga Windows dapat menebak file-file apa saja yang mungkin akan dipakai pengguna. Berdasarkan informasi tersebut, Windows akan mengisi file ke memori sebelum dipakai sehingga mengurangi kemungkinan page fault. Selain itu, Windows juga terkadang sibuk mengurusi log NTFS di balik layar. Tergantung pada penggunanya, perilaku ‘pintar’ tidak selalu positif. Beberapa pengguna yang ingin punya kendali penuh bisa saja tidak senang dengan Windows yang suka sibuk sendiri tanpa disuruh.

Contoh ‘kepintaran’ Windows 7 yang bisa menuju ke arah berbahaya adalah ia selalu mencatat jumlah eksekusi sebuah program serta kapan sebuah program terakhir kali dijalankan ke lokasi registry HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist. Walaupun program dihapus, catatan ini tetap akan ada selama-lamanya 🙂 Dengan fitur ini, Windows 7 dapat menampilkan program populer yang sering dipakai di menu Start. Masalahnya adalah banyak yang tidak tahu bahwa Windows 7 menyimpan catatan seperti ini. Sepertinya Microsoft berusaha menyembunyikan isi registry tersebut dari tangan jahil karena ia menyamarkan isi registry UserAssist tersebut dengan enkripsi ROT13. Ini adalah jenis enkripsi tidak aman yang sangat mudah dipecahkan; ROT13 hanya melakukan subtitusi huruf seperti A menjadi N, B menjadi O, C menjadi P, dan seterusnya. Walaupun mudah dipecahkan, penggunakan enkripsi ROT13 dan nilai dalam bentuk binary membuat saya sulit mencerna isi dari registry UserAssist dengan mudah.

Oleh sebab itu, saya akan mencoba membuat sebuah program C# yang akan membaca isi registry UserAssist tersebut dan menampilkan informasi dalam bentuk tabel yang lebih mudah dicerna. Program ini adalah sebuah aplikasi WPF yang saya ujikan pada Windows 7 32-bit dan 64-bit. Kode program secara lengkap dapat ditemukan di http://github.com/JockiHendry/ProgramExecutionCounter.

Kode program yang saya buat untuk membaca dari registry terlihat seperti berikut ini:

...
private void OnSearch()
{            
   countEntries.Clear();

   RegistryKey reg = Registry.CurrentUser.OpenSubKey(SelectedSourceType.Key);
   foreach (string valueName in reg.GetValueNames())
   {                
      CountEntry entry = new CountEntry();
      entry.Name = valueName;

      // filter by name
      if (!string.IsNullOrEmpty(NameFilter))
      {
          if (!entry.DecodedName.ToUpper().Contains(NameFilter.ToUpper())) continue;
      }

      entry.Value = (byte[]) reg.GetValue(valueName);
      entry.RegKey = reg.ToString();
      ...
      countEntries.Add(entry);
   }
}  
...      

Pada kode program di atas, saya memakai class Registry dari namespace Microsoft.Win32 untuk membaca isi registry. Saya kemudian melakukan konversi masing-masing value yang saya temukan menjadi sebuah object CountEntry.

Pada class CountEntry, saya menerjemahkan nama yang dienkripsi dengan algoritma ROT13 dengan menggunakan method dictionary lookup. Saya memilih cara ini karena lebih mudah dan jumlah kombinasi yang ada sangat sedikit (A-Z, a-z). Selain itu, nama di registry UserAssist juga mengandung GUID yang mewakili folder spesial di Windows (seperti MyComputer, Program Files, dan sebagainya). Saya bisa memperoleh informasi GUID untuk folder spesial di Windows dengan melihat isi header KnownFolders.h yang terdapat di folder Include di Windows SDK. Setelah membuat variabel static yang berisi dictionary lookup untuk ROT13 dan daftar terjemahan GUID folder, saya kemudian memulai proses dekripsi dengan kode program seperti berikut ini:

...
public String DecodedName
{
    get
    {
        if (string.IsNullOrEmpty(Name))
        {
            return "";
        }
        else
        {
            string result = new string(Name.ToCharArray().Select(c =>
            {
                return lookupTable.ContainsKey(c) ? lookupTable[c] : c;
            }).ToArray());
            foreach (var f in folderGUID)
            {
                if (result.Contains(f.Key)) result = result.Replace(f.Key, f.Value);
            }
            return result;
        }
    }
}
...

Nilai dari setiap key di registry UserAssist adalah deretan byte sebesar 72 byte. Tidak ada yang tahu persis apa saja informasi yang tersimpan, selain Microsoft selaku pencipta Windows (rasa waspada ini tidak perlu ada bila memakai sistem operasi open-source 😉 ). Berdasarkan informasi yang diperoleh dari hasil pencarian Google, saya hanya akan mengambil 4 byte mulai dari posisi ke-4 yang mewakili jumlah eksekusi program dalam bilangan integer 32-bit. Untuk mengubah deretan byte menjadi sebuah int, saya memakai class BitConverter seperti yang terlihat pada kode program berikut ini:

...
public byte[] Value
{
    get
    {
        return this.value;
    }

    set
    {
        this.value = value;
        executionCount = BitConverter.ToInt32(value, 4);
    }
}
...

Tool sederhana ini juga memungkinkan pengguna untuk mengubah jumlah eksekusi secara langsung dari tabel. Untuk menerjemahkan bilangan int yang dimasukkan oleh pengguna menjadi byte array, saya kembali menggunakan class BitConverter dengan memanggil method GetBytes() seperti yang terlihat pada kode program berikut ini:

...
if (e.PropertyName == "ExecutionCount")
{
    try
    {                            
        byte[] newCount = BitConverter.GetBytes(countEntry.ExecutionCount);
        newCount.CopyTo(countEntry.Value, 4);
        Registry.SetValue(entry.RegKey, entry.Name, entry.Value);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Error Updating Registry: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}
...

Pada kode program diatas, saya memanggil method SetValue() dari class Registry untuk menulis perubahan ke registry.

Selain mengubah, tool ini juga memungkinkan pengguna untuk menghapus entry yang dipilihnya. Untuk itu saya perlu memanggil method DeleteValue() dari sebuah RegistryKey untuk menghapus nilai yang dipilih oleh pengguna, seperti yang terlihat pada kode program berikut ini:

public void OnDelete()
{
    if (MessageBox.Show("Do you really want delete this entry?", "Delete Confirmation",
        MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)
    {
        RegistryKey key = Registry.CurrentUser.OpenSubKey(RegKey.Replace(@"HKEY_CURRENT_USER", ""), true);
        if (key != null)
        {
            try
            {
                key.DeleteValue(Name);
                PropertyChanged(this, new PropertyChangedEventArgs("DeleteCommand"));
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error Deleting Registry: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

    }
}

Sekarang, saya akan mencoba menjalankan program. Pada saat saya men-klik tombol Search, saya akan memperoleh hasil seperti pada gambar berikut ini:

Menampilkan seluruh entry yang ada

Menampilkan seluruh entry yang ada

Saya dapat langsung mengubah nilai jumlah eksekusi dengan men-double klik pada kolom angka, mengisi angka baru dan menekan tombol Enter, seperti yang terlihat pada gambar berikut ini:

Mengubah nilai registry untuk jumlah eksekusi

Mengubah nilai registry untuk jumlah eksekusi

Selain itu, saya dapat langsung menghapus sebuah entry dengan men-klik icon dengan tanda silang di baris yang bersesuaian, seperti yang terlihat pada gambar berikut ini:

Menghapus nilai dari registry

Menghapus nilai dari registry

Saya juga dapat menampilkan informasi detail untuk sebuah baris dengan men-klik icon kaca pembesar, seperti yang terlihat pada gambar berikut ini:

Menampilkan informasi detail untuk sebuah entry

Menampilkan informasi detail untuk sebuah entry

Lalu, apa manfaat dari tool sendiri ini? Karena registry UserAssist ini akan tetap menyimpan informasi program yang dijalankan walaupun program sudah dihapus atau di-uninstall, maka saya dapat menggunakannya untuk memeriksa apakah pengguna pernah menjalankan sebuah program atau tidak. Fungsi lainnya, misalnya saya dapat memeriksa apa saja program yang pernah dijalankan langsung melalui flash disk (dengan men-klik di Explorer) dengan fasilitas pencarian seperti pada gambar berikut ini:

Melakukan pencarian

Melakukan pencarian

Apakah kali ini ‘kepintaran’ Windows dirasa membuat pengguna khawatir dengan privasi mereka? Beruntungnya, Windows menyediakan fitur untuk mematikan fasilitas ini. Saya dapat menghilangkan pencatatan ini dengan men-klik kanan pada task bar yang kosong, memilih menu Properties. Pada dialog yang muncul, saya memilih tab Start Menu dan menghilangkan tanda centang pada Store and display recently opened programs in the Start menu seperti yang terlihat pada gambar berikut ini:

Menghilangkan pencatatan pada registry UserAssist

Menghilangkan pencatatan pada registry UserAssist

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

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 Persistence Di Windows Workflow

Pada artikel Memakai Bookmark Di Windows Workflow (WF), saya membuat sebuah aplikasi yang memakai Windows Workflow (WF). Bila aplikasi tersebut ditutup, seluruh state workflow akan hilang, walaupun workflow belum mencapai tahap terakhir. Benarkah demikian? Untuk membuktikannya, saya akan membuat sebuah Windows Form dengan men-klik kanan nama proyek dan memilih menu Add, Windows Form…. Saya mengisi nama form berupa FormKendali. Saya kemudian menambahkan tiga Button masing-masing untuk mewakili resume untuk bookmark MulaiDimasak, SelesaiDimasak dan PelangganMembayar. Saya juga menambahkan sebuah Timer ke dalam form tersebut untuk men-enabled Button sesuai dengan bookmark yang sedang aktif. Saya kemudian menambahkan event handler dan constructor pada form seperti yang terlihat pada kode program berikut ini:

using System;
using System.Windows.Forms;
using System.Activities;
using System.Activities.Hosting;

namespace LatihanBookmark
{
    public partial class FormKendali : Form
    {
        private WorkflowApplication workflowApp;        

        public FormKendali(WorkflowApplication app)
        {
            InitializeComponent();
            this.workflowApp = app;
            timer1.Enabled = true;
            button1.Enabled = false;
            button2.Enabled = false;
            button3.Enabled = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {            
            workflowApp.ResumeBookmark("MulaiDimasak", "Master Chef");
            ((Button)sender).Enabled = false;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            workflowApp.ResumeBookmark("SelesaiDimasak", "Master Chef");
            ((Button)sender).Enabled = false;
        }

        private void button3_Click(object sender, EventArgs e)
        {
            workflowApp.ResumeBookmark("PelangganMembayar", "Cute Girl");
            timer1.Enabled = false;                        
            ((Button)sender).Enabled = false;            
        }

        private void timer1_Tick(object sender, EventArgs e)
        {                        
            foreach (BookmarkInfo bookmark in workflowApp.GetBookmarks())
            {
                switch (bookmark.BookmarkName)
                {
                    case "MulaiDimasak":                        
                        button1.Enabled = true;
                        break;
                    case "SelesaiDimasak":
                        button2.Enabled = true;                                                
                        break;
                    case "PelangganMembayar":
                        button3.Enabled = true;                        
                        break;
                }
            }
        }

    }
}

Berikutnya, saya mengubah kode program Program.cs agar ia menampilkan form di atas, seperti yang terlihat pada kode program berikut ini:

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

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()
            {
                { "order", contohOrder }
            };


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

            Application.EnableVisualStyles();
            Application.Run(new FormKendali(app));

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

            Console.ReadLine();
        }
    }
}

Bila saya menjalankan aplikasi, saya akan menjumpai hasil seperti pada gambar berikut ini:

Menunggu bookmark di-resume

Menunggu bookmark di-resume

Menunggu bookmark di-resume

Menunggu bookmark di-resume

Menunggu bookmark di-resume

Menunggu bookmark di-resume

Bila saya menutup aplikasi pada saat workflow baru sampai pada tahap Mulai Dimasak, maka saat saya menjalankan ulang aplikasi, workflow akan kembali mulai dari awal. Bukankah proses pada workflow adalah proses yang berlangsung pada jangka waktu relatif lama? Walaupun aplikasi ditutup atau server dimatikan, sebaiknya workflow engine harus bisa mengingat state workflow sebelumnya. Salah satu cara yang mungkin adalah dengan menyimpan state workflow pada database. Untuk menyimpan workflow pada database SQL Server, saya dapat menggunakan class SqlWorkflowInstanceStore.

Untuk menyimpan workflow pada database, saya perlu membuat tabel yang dibutuhkan. .NET Framework 4 sudah menyediakan script SQL yang bisa langsung saya kerjakan di SQL Server. Script SQL tersebut terletak di C:\Windows\Microsoft.NET\Framework\v4.0\SQL\en. Saya perlu mengerjakan script SqlWorkflowInstanceStoreSchema.sql terlebih dahulu baru diikuti dengan SqlWorkflowInstanceStoreLogic.sql. Setelah mengerjakan kedua script tersebut, saya akan menemukan tabel dan stored procedure baru yang terlihat seperti pada gambar berikut ini:

Tabel untuk menyimpan workflow

Tabel untuk menyimpan workflow

Berikutnya, saya perlu menambahkan referensi ke System.Activities.DurableInstancing dan System.Runtime.DurableInstancing dengan men-klik kanan nama proyek dan memilih menu Add Reference….

Setelah itu, saya mengubah kode program di Program.cs menjadi seperti berikut ini:

using System;
using System.Activities;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Data.SqlClient;

namespace LatihanBookmark
{

    class Program
    {
        private static readonly string CONNECTION_STRING = "Server=.\\SQLEXPRESS;Initial Catalog=LatihanWorkflow;Integrated Security=SSPI";

        public static Guid? GetSavedInstanceId()
        {            
            using (SqlConnection cn = new SqlConnection(CONNECTION_STRING))
            {
                cn.Open();
                SqlCommand cmd = new SqlCommand("SELECT Id FROM [System.Activities.DurableInstancing].[InstancesTable]", cn);
                return (Guid?) cmd.ExecuteScalar();
            }            
        }

        static void Main(string[] args)
        {
            WorkflowApplication app;
            ProsesPemesanan workflow = new ProsesPemesanan();
            InstanceStore store = new SqlWorkflowInstanceStore(CONNECTION_STRING);
            InstanceHandle handle = store.CreateInstanceHandle();
            CreateWorkflowOwnerCommand createOwnerCmd = new CreateWorkflowOwnerCommand();
            var view = store.Execute(handle, createOwnerCmd, TimeSpan.FromSeconds(30));
            store.DefaultInstanceOwner = view.InstanceOwner;

            Guid? instanceId = GetSavedInstanceId();

            if (instanceId != null)
            {
                //
                // Lanjutkan workflow yang sudah ada
                //

                app = new WorkflowApplication(workflow);
                app.InstanceStore = store;
                app.Load(instanceId.Value);                
            }
            else
            {
                //
                // Membuat instance workflow baru
                //

                Order contohOrder = new Order
                {
                    Id = "ORDER1",
                    MenuMakanan = "Tasty Food",
                    NamaPelanggan = "A Snake"
                };

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

                app = new WorkflowApplication(workflow, argumen);
                app.InstanceStore = store;
                app.Run();
            }

            Application.EnableVisualStyles();
            Application.Run(new FormKendali(app));

            DeleteWorkflowOwnerCommand deleteOwnerCmd = new DeleteWorkflowOwnerCommand();
            store.Execute(handle, deleteOwnerCmd, TimeSpan.FromSeconds(30));

            Console.ReadLine();
        }
    }
}

Setiap instance workflow selalu memiliki id yang unik. Pada kode program di atas, saya memeriksa nilai id dari instance pertama yang disimpan di tabel InstancesTable. Bila ada, maka saya akan menggunakannya untuk melanjutkan workflow yang sudah tersimpan tersebut dengan memanggil method Load(). Bila tabel InstancesTable masih kosong, saya akan membuat instance workflow baru.

Selain itu, saya juga menambahkan event handler berikut pada file FormKendali.cs yang isinya berupa:

...
private void FormKendali_FormClosed(object sender, FormClosedEventArgs e)
{            
   workflowApp.Persist();
}        
...

Kode program di atas akan menyimpan workflow ke dalam database setiap kali saya menutup form. Dengan demikian, bila saya menutup aplikasi pada saat sedang berada di activity tertentu dari workflow, maka saat saya membuka aplikasi, workflow akan kembali ke activity terakhir pada saat saya menutup form. Bukan hanya itu, instance workflow tersebut juga akan tetap mengingat nilai argumen order yang telah diberikan padanya sebelumnya.

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

Melakukan Binding Collection Di WPF

Pada artikel sebelumnya, Menerapkan MVVM Di Windows Presentation Foundation (WPF), saya sudah membuat sebuah form dengan memakai Windows Presentation Foundation (WPF) untuk menyimpan sebuah objek ItemPenjualan. Sekarang, saya ingin menambahkan sebuah DataGrid guna menampilkan seluruh objek ItemPenjualan yang sudah tersimpan di form yang sama. Dengan pola MVVM dan pembagian peran yang rapi, saya dapat melakukan perubahan ini dalam waktu singkat.

Langkah pertama adalah menambahkan sebuah property baru di view model untuk menampung daftar object ItemPenjualan yang telah tersimpan. Tipe data property tersebut harus berupa ObservableCollection<T> sehingga nantinya view dapat mengetahui bila ada elemen yang berubah (bertambah atau berkurang) di collection tersebut. Sebagai contoh, saya mengubah kode pogram ItemPenjualanViewModel.cs menjadi seperti berikut ini:

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

        public ItemPenjualanViewModel(ItemPenjualan itemPenjualan = null)
        {
            ...

            // Mendapatkan seluruh ItemPenjualan dari database
            this.ListItemPenjualan = new ObservableCollection<ItemPenjualan>();
            using (var db = new LatihanContext())
            {                                
                foreach (ItemPenjualan p in db.DaftarItemPenjualan)
                {
                    this.ListItemPenjualan.Add(p);                    
                }
            }            
        }

        public ObservableCollection<ItemPenjualan> ListItemPenjualan { get; private set; }

        ...
    }
}

Pada kode program di atas, nilai dari property ListItemPenjualan akan di-isi berdasarkan hasil query dari tabel di database pada saat instance view model dibuat.

Berikutnya, saya perlu menambahkan sebuah DataGrid diview sehingga tampilan view menjadi seperti pada gambar berikut ini:

Rancangan view

Rancangan view

DataGrid dapat menghasilkan kolom secara otomatis bila nilai property AutoGenerateColumns-nya bernilai true. Akan tetapi, saya lebih suka mendefinisikan setiap kolom yang ada secara manual. Oleh sebab itu, saya mengubah XAML yang dihasilkan untuk DataGrid di view menjadi seperti berikut ini:

<Window ...>

    <Window.Resources>
        <Style TargetType="{x:Type TextBlock}" x:Key="StyleAngka">            
            <Setter Property="HorizontalAlignment" Value="Right" />
        </Style>
        ...
    </Window.Resources>

    <Grid>        
        ...        
        <DataGrid AutoGenerateColumns="False" AlternatingRowBackGround="LightBlue" ... 
                  SelectionUnit="FullRow" FontSize="15" Language="id-IN" 
                  IsReadOnly="True" ItemsSource="{Binding Path=ListItemPenjualan}">        
            <DataGrid.Columns>
                <DataGridTextColumn Header="Nama Barang" Binding="{Binding NamaBarang}" Width="*"/>
                <DataGridTextColumn Header="Jumlah" Binding="{Binding Jumlah, StringFormat={}{0:#,0}}" 
                                    ElementStyle="{StaticResource StyleAngka}" />                                    
                <DataGridTextColumn Header="Harga" Binding="{Binding Harga, StringFormat={}{0:C}}" Width="*" 
                                    ElementStyle="{StaticResource StyleAngka}"/>
                <DataGridTextColumn Header="Diskon Persen" Binding="{Binding DiskonPersen, StringFormat={}{0:#.#}}" 
                                    ElementStyle="{StaticResource StyleAngka}"/>                
            </DataGrid.Columns>            
        </DataGrid>
        ...                
    </Grid>
</Window>

Sekarang, bila saya menjalankan program, data dari tabel akan ditampilkan pada DataGrid seperti yang terlihat pada gambar berikut ini:

Hasil program

Hasil program

Berikutnya, saya hanya perlu menambahkan sedikit perubahan kecil agar ItemPenjualan yang barusan disimpan juga ikut ditambahkan ke tabel. Pada file ItemPenjualanDataView.cs, saya mengubah method Execute() yang akan dikerjakan saat tombol simpan di-klik menjadi seperti berikut ini:

public void Execute(object parameter)
{
   using (var db = new LatihanContext())
   {  
      db.DaftarItemPenjualan.Add(viewModel.Model);

      // Menambahkan ItemPenjualan ke DataGrid
      viewModel.ListItemPenjualan.Add(viewModel.Model);

      db.SaveChanges();
   }
   MessageBox.Show("Data berhasil disimpan ke database");
}

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