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.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

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

Logo WordPress.com

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

Gambar Twitter

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

Foto Facebook

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

Foto Google+

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

Connecting to %s

%d blogger menyukai ini: