Technologie Windows Workflow Foundation 4.0 (dále jen WF) umožňuje na platformě .NET tvorbu aplikací založených na workflow nebo integraci workflow jako součást naší .NET aplikace. Technologií WF je tedy na platformě .NET podporováno modelování procesů, což je jeden z moderních přístupů k vývoji aplikací.
Windows Workflow Foundation existovalo v .NET již od verze 3.0, nutno ale upozornit, že aktuální verze 4, která je součástí .NET Frameworku 4.0, byla kompletně a od základů přepracována a s technologií původní má společný akorát tak začátek názvu. (Zmíněnou starší verzí se věnovat nebudeme a pod označením WF zde bude vždy myšlená současná verze Windows Workflow Foundation 4.0.)
V této sérii budou na jednom větším příkladu vysvětleny principy WF a bude ukázáno jak použít a začlenit Workflow do naší aplikace. Takovým klasickým příkladem pro použití Workflow, se kterým se v praxi můžeme setkat, je např. proces schválení dovolené nadřízeným, a protože čistě náhodu zrovna tento konkrétní scénář jsem i implementovat v jednom našem enterprise systému, použijeme ho i zde.
V souvislosti s tímto konkrétním Workflow budeme uvažovat následující funkcionalitu: Výchozím předpokladem je existence dovolené, kterou pracovník zaplánoval. Při spuštění vlastního Workflow se jako vstupní parametr předá identifikace této dovolené, o jejíž schválení se žádá. Workflow vybere nadřízeného (a případně i jeho zástupce) daného pracovníka, odešle email s žádostí a bude čekat na její rozhodnutí tj. schválení nebo zamítnutí. To může být realizováno .aspx stránkou, na kterou budou směřovat odkazy v generované emailové zprávě. Stránka pak obnoví příslušné workflow, které v dalším kroku provede schválení nebo zamítnutí dovolené (ale jiné workflow by mohlo třeba odeslat další žádost nějakému “vyššímu” šéfovi případně řediteli apod.).
V reálné aplikaci by bylo možné toto workflow hostovat buď přímo jako součást webové aplikace nebo jako samostatný NT service například s vystaveným WCF rozhraním - přes toto rozhraní by se pak provádělo obnovení workflow při rozhodnutí žádosti. V našem příkladu ale toto nebude řešeno, příklad bude pouze předpokládat existenci nějaké infrastruktury, do které bude workflow integrováno.
Při použití technologie WF pro realizaci takovéhoto procesu je ale samotné workflow až skoro to poslední. Vždy je předem potřeba připravit domain specific custom aktivity tj. naimplementovat vlastní jednotlivé stavební bloky, ze kterých se bude celý proces ve workflow teprve skládat. Tím je také oddělená vlastní logika daného procesu od ostatních částí a infrastruktury naší aplikace. Jinými slovy WF je vhodné použít pouze pro proces samotný a nemusí jít až na úroveň základních aktivit (dodávaných přímo jako součást .NET Frameworku). To by bylo jednak velmi pracné a přitom zcela zbytečné.
Pro náš příklad budeme implementovat tyto aktivity:
- Aktivita pro generování žádostí pro vedoucího a zástupce
- Aktivita pro odeslání žádostí (a čekání na rozhodnutí první z nich)
- Aktivita pro schválení dovolené
- Aktivita pro zamítnutí dovolené
Dále tyto aktivity obvykle používají, a také v tomto případě budou používat, nějakou vrstvu business logiky nebo jinak řešeno prováděná business logika není přímo v aktivitách, ale ty jí pouze zaobalují a zpřístupňují pro potřeby workflow.
Prvním krokem pro účely implementaci našeho příkladu bude tedy příprava objektů simulující tuto vrstvu business logiky. Přitom se zde zaměříme pouze na funkcionalitu a interface z pohledu potřeby našeho workflow. Dále je také vhodné, abychom měli volnou vazbu (loose coupling) mezi návrhem workflow a business logikou, proto mezi aktivitami samotnými (jako vstupní a výstupní argumenty) nebudou ve workflow přímo předávaný objekty této vrstvy (to může také umožnit např. podporu pro serializaci celého workflow). Z tohoto důvodu předpokládáme existenci repository tj. komponenty, která umí získat objekt business vrstvy “kdykoliv na vyžádání” pouze na základě jeho identifikátoru. Některé akce také budou zjednodušené nebo pouze naznačené, např. tu nebude žádné volání databázových operací, bez kterého se určitě u reálné aplikace neobejdeme.
Potřebné třídy umístíme do namespace PlanAbsenci.Logic. Bude se jednat o třídy Osoba, PlanAbsence, Repository a PlanAbsenceZadost.
using System;
using System.Linq;
using System.Collections.Generic;
namespace PlanAbsenci.Logic
{
public class Osoba
{
#region member varible and default property initialization
public int IDOsoby { get; internal set; }
public int OsobniCislo { get; internal set; }
public string CeleJmeno { get; internal set; }
public string Email { get; internal set; }
internal int? IDOsobyNadrizeny { get; set; }
internal HashSet<int> IDOsobyZastupci { get; private set; }
#endregion
#region constructors and destructors
internal Osoba()
{
this.IDOsobyZastupci = new HashSet<int>();
}
#endregion
#region action methods
public static Osoba Get(int IDOsoby)
{
return Repository.Osoby[IDOsoby];
}
public IEnumerable<Tuple<Osoba, bool>> GetNadrizenyAZastupci()
{
//Vratí nadřízeného a jeho zástupce (druhá složka Tuple říká, zda se jedná o zástupce)
var nadrizeny = this.Nadrizeny;
if (nadrizeny != null)
{
yield return Tuple.Create(nadrizeny, false);
foreach (var zastupce in nadrizeny.Zastupci)
{
yield return Tuple.Create(zastupce, true);
}
}
}
#endregion
#region property getters/setters
public Osoba Nadrizeny
{
get { return this.IDOsobyNadrizeny == null ? null : Osoba.Get(this.IDOsobyNadrizeny.Value); }
}
public IEnumerable<Osoba> Zastupci
{
get { return from IDOsoby in IDOsobyZastupci select Osoba.Get(IDOsoby); }
}
#endregion
}
}
Statická metoda Osoba.Get() zaobaluje vyžádání konkrétního objektu osoby z repository (pomocná třída Repository) na základě svého jedinečného IDOsoby.
Vlastní osoba pak kromě ID obsahuje osobní číslo, celé jméno, email a dále vazbu na nadřízeného (realizováno vlastností IDOsobyNadrizeny a Nadrizeny) a kolekci osob zastupujících danou osobu (IDOsobyZastupci a Zastupci). Korektně naplněný objekt bude mít vše vyplněné, případně s výjimkou nadřízeného (kořen stromu), pouze kolekce zástupců může být prázdná.
Metoda GetNadrizenyAZastupci() vrací seznam osob – nadřízeného daného pracovníka a všechny zástupce tohoto nadřízeného. U každé osoby je pak ještě příznak, zda se jedná o zástupce nebo nikoliv. Tato metoda bude využívána v aktivitě pro generování žádostí.
using System;
using System.Collections.Generic;
namespace PlanAbsenci.Logic
{
public class PlanAbsence
{
#region member varible and default property initialization
public int IDPlanAbsence { get; internal set; }
public int IDOsoby { get; internal set; }
public DateTime DatumOd { get; internal set; }
public DateTime DatumDo { get; internal set; }
public string OznaceniAbsence { get; internal set; }
public string Popis { get; internal set; }
#endregion
#region constructors and destructors
internal PlanAbsence() { }
#endregion
#region action methods
public static PlanAbsence Get(int IDPlanAbsence)
{
return Repository.PlanAbsence[IDPlanAbsence];
}
public void Schvaleni(int IDOsobyRozhodl)
{
//TODO: Proveď schválení dovolené
}
public void Zamitnuti(int IDOsobyRozhodl)
{
//TODO: Proveď zamítnutí dovolené
}
#endregion
#region property getters/setters
public string Termin
{
get { return string.Format("{0}-{1}", this.DatumOd.ToShortDateString(), this.DatumDo.ToShortDateString()); }
}
#endregion
}
}
Třída PlanAbsence reprezentuje jednotlivé plánované dovolené, o jejichž schvalování se bude žádat. Metoda Get() opět bude umět vrátit konkrétní objekt na základě jeho ID tj. IDPlanAbsence. Další datová pole jsou identifikace pracovníka, který dovolenou zaplánoval (IDOsoby), Datum od a do, označení absence (např. “Dovolená”) a volitelný popis (vše kromě popisu je povinné).
Vlastnost Termin vrací formátovaný časový interval od, do – bude se používat v generované emailové zprávě.
Metody Schvaleni() a Zamitnuti() se budou volat jako výsledek schválení nebo zamítnutí dovolené z Workflow. Pro účely příkladu postačí jakákoliv testovací implementace (např. výpis do konzole). Tyto metody budou dostávat identifikaci osoby (IDOsobyRozhodl), která schválení nebo zamítnutí provedla.
using System;
using System.Collections.Generic;
namespace PlanAbsenci.Logic
{
internal static class Repository
{
#region member varible and default property initialization
public static IDictionary<int, Osoba> Osoby { get; private set; }
public static IDictionary<int, PlanAbsence> PlanAbsence { get; private set; }
#endregion
#region constructors and destructors
static Repository()
{
//TODO: Naplnit data
Osoby = new Dictionary<int, Osoba>();
PlanAbsence = new Dictionary<int, PlanAbsence>();
}
#endregion
}
}
Pomocná třída Repository v našem případě může pro jednoduchost objekty držet pouze např. v jednoduchém Dictionary. V reálu by se pak objekty získávaly buď vždy přímo z databáze nebo z implementované “objektové cache” (může být použitý např. nějaký O-R mapper jako entity framework apod.).
Protože v době od odeslání žádosti do doby jejího rozhodnutí je třeba o žádosti držet data nutná pro opětovnou aktivaci workflow (tyto data nebudou přímo součást generovaných odkazů v emailu žádosti, tam bude uveden pouze identifikátor žádosti), je potřeba business vrstvu ještě rozšířit o podporu pro ukládání jednotlivých dílčích žádostí.
using System;
using System.Collections.Generic;
namespace PlanAbsenci.Logic
{
public sealed class PlanAbsenceZadost
{
#region member varible and default property initialization
public Guid ZadostGuid { get; private set; }
public int IDPlanAbsence { get; private set; }
public string WorkflowType { get; private set; }
public Guid WorkflowInstanceId { get; private set; }
//TODO: Použít perzistentní uložiště
private static Dictionary<Guid, PlanAbsenceZadost> s_Zadosti = new Dictionary<Guid, PlanAbsenceZadost>();
#endregion
#region constructors and destructors
private PlanAbsenceZadost() { }
#endregion
#region action methods
public static PlanAbsenceZadost GetZadost(Guid zadostGuid)
{
PlanAbsenceZadost zadost;
if (!s_Zadosti.TryGetValue(zadostGuid, out zadost))
{
return null;
}
return zadost;
}
public static void InsertZadost(Guid zadostGuid, int IDPlanAbsence, string workflowType, Guid workflowInstanceId)
{
s_Zadosti.Add(zadostGuid, new PlanAbsenceZadost() { ZadostGuid = zadostGuid, IDPlanAbsence = IDPlanAbsence, WorkflowType = workflowType, WorkflowInstanceId = workflowInstanceId });
}
public static void DeleteZadost(Guid zadostGuid)
{
s_Zadosti.Remove(zadostGuid);
}
#endregion
}
}
Identifikátor žádosti bude typu Guid, protože bude potřeba jedinečný identifikátor vygenerovat ještě před uložením žádosti do databáze (nepůjde tedy použít např. identity nad tabulkou v databázi) a také z bezpečnostního hlediska je mnohem lepší mít jako součást url GUID namísto pouze číselného ID. Data pro potřeby obnovení workflow jsou konkrétně WorkflowType (typ workflow) a WorkflowInstanceId.
IDPlanAbsence je identifikace dovolené, ke které žádost patří. Ta pro vlastní potřeby obnovení workflow není sice nutná, ale její začlenění podpoří některé další případné scénáře, například by umožnilo určit, které workflow je nutné zastavit při odstranění dovolené, ke které existuje aktuálně žádost apod.
Vlastní úložiště žádostí by v reálu byla opět databáze z důvodu nutnosti perzistentního uložení, zde je opět pro jednoduchost pouze dictionary.
Statická metoda InsertZadost() slouží pro uložení dat o nové žádosti, metoda DeleteZadost() pro odstranění žádosti v době již po jejím rozhodnutí nebo její zneplatnění po rozhodnutí jiné žádosti o stejné absenci. Metoda GetZadost() umožňuje vrátit data žádosti podle jejího identifikátoru. Pokud byla žádost zneplatněna, vrátí tato metoda hodnotu null.
Tím máme funkce business vrstvy připravené.