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.