Druhým krokem bude příprava a implementace potřebných custom aktivit.
Nejčastější typ custom aktivity je CodeActivity tj. aktivita, která je celá implementována v C# kódu jako třída a celé její činnosti odpovídá jedna metoda. Druhým typem aktivity je pak XAML aktivita, která je složená z aktivit jiných (XAML nebo code, vlastních nebo standardních), vytváří se pomoci activity designeru a interně je popsána jazykem XAML.
Ve WF také není žádný rozdíl mezi XAML aktivitou a vlastním workflow tj. workflow je top-level XAML aktivita, kterou pouze nepoužíváme v jiné aktivitě, ale spouštíme jí jako instanci celého workflow přes WF runtime. Aktivity (XAML i code) a tedy i workflow samotné dále mohou mít vstupní a výstupní argumenty, umožňující “komunikaci” aktivity s okolním světem.
Pro celý proces schvalování absencí si vyhradíme namespace PlanAbsenci.Schvaleni (sem budeme umísťovat typy týkající se našeho workflow a k němu potřebné infrastruktury). Vlastní aktivity pak umístíme do namespace PlanAbsenci.Schvaleni.Activities.
Do projektu musíme také přidat tyto potřebné reference:
System.Activities.dll
System.Activities.DurableInstancing.dll
System.Runtime.Serialization.dll
System.Runtime.DurableInstancing.dll
Začneme aktivitou pro generování dílčích žádostí, jejím vstupem (což bude i vstup celého workflow) bude typ AbsenceRequest a jejím výstupem kolekce typů ZadostRequest. Implementace těchto typů vypadá následovně:
using System;
namespace PlanAbsenci.Schvaleni
{
[Serializable]
public sealed class AbsenceRequest
{
#region member varible and default property initialization
public int IDPlanAbsence { get; internal set; }
public int IDOsoby { get; internal set; }
#endregion
#region constructors and destructors
internal AbsenceRequest() { }
#endregion
}
[Serializable]
public sealed class ZadostRequest
{
#region member varible and default property initialization
public Guid Guid { get; private set; }
public int IDPlanAbsence { get; internal set; }
public int IDOsoby { get; internal set; }
public int IDOsobyPrijemce { get; internal set; }
public bool Zastupce { get; internal set; }
#endregion
#region constructors and destructors
public ZadostRequest()
{
this.Guid = Guid.NewGuid();
}
#endregion
}
}
Typ AbsenceRequest bude dvojice IDPlanAbsence identifikující danou dovolenou a IDOsoby identifikující pracovníka, který jí zaplánovat.
Typ ZadostRequest bude reprezentovat jednotlivou dílčí žádost tj. žádost týkající se konkrétní dovolené a určenou pro jednoho konkrétního příjemce. Kromě jedinečného GUID bude obsahovat IDPlanAbsence, IDOsoby, IDOsobyPrijemce (identifikace pracovníka, kterému je žádost určená) a příznak Zastupce (který určuje, zda je příjemce pouze zástupce nebo přímo vedoucí).
using System;
using System.Linq;
using System.Activities;
using System.Collections.Generic;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class GetZadostiVedouci : CodeActivity<IEnumerable<ZadostRequest>>
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<AbsenceRequest> Absence { get; set; }
#endregion
#region private member functions
protected override IEnumerable<ZadostRequest> Execute(CodeActivityContext context)
{
var absence = context.GetValue(this.Absence);
var osoba = Osoba.Get(absence.IDOsoby);
var list = (from i in osoba.GetNadrizenyAZastupci()
select new ZadostRequest()
{
IDPlanAbsence = absence.IDPlanAbsence,
IDOsoby = absence.IDOsoby,
IDOsobyPrijemce = i.Item1.IDOsoby,
Zastupce = i.Item2
}).ToList();
if (list.Count == 0)
{
throw new InvalidOperationException(string.Format("Osoba '{0}' nemá přiřazeného žádného nadřízeného, žádost nelze odeslat.", osoba.CeleJmeno));
}
return list;
}
#endregion
}
}
CodeActivity dědí ze základní třídy CodeActivity případně CodeActivity<T>. V případě CodeActivity<T> má metoda Execute() návratovou hodnotu typu T, která je pak automaticky převedena a dostupná jako výstupní argument aktivity s názvem Result. Ostatní argumenty se zavádějí jako vlastnosti typu InArgument<T> nebo OutArgument<T>. Atribut RequiredArgument u argumentu říká, že se jedná o povinný argument. Vlastní činnost aktivity se implementuje v metodě Execute(). Na hodnotu vstupního argumentu se lze odkázat pomoci volání context.GetValue() s parametrem typu InArgument<T> (metoda vrátí přímo hodnotu typu T).
Aktivita GetZadostiVedouci má tedy jeden povinný vstupní argument Absence typu AbsenceRequest a jeden výstupní argument s výchozím názvem Result typu IEnumerable<ZadostRequest>. V metodě Execute() nejprve získáme příslušný objekt Osoba, na kterém následně voláme metodu GetNadrizenyAZastupci(). Na základě obdržených výsledků konstruujeme výstupní seznam žádostí pro vedoucího a případně jeho zástupce.
using System;
using System.Activities;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class OdeslaniZadosti : CodeActivity
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<ZadostRequest> Zadost { get; set; }
#endregion
#region property getters/setters
private Activity RootActivity
{
get
{
var prop = typeof(Activity).GetProperty("RootActivity", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
return (Activity)prop.GetValue(this, null);
}
}
#endregion
#region private member functions
protected override void Execute(CodeActivityContext context)
{
var zadost = context.GetValue(this.Zadost);
Type workflowType = this.RootActivity.GetType();
//Uložení žádosti do databáze
PlanAbsenceZadost.InsertZadost(zadost.Guid, zadost.IDPlanAbsence, workflowType.FullName + ", " + workflowType.Assembly.GetName().Name, context.WorkflowInstanceId);
//Odeslaní emailu s žádostí
MailGenerator.SendZadost(zadost);
}
#endregion
}
}
Pomocná aktivita Odeslanizadosti bude sloužit pro odeslání emailu odpovídající jedné dílčí žádosti. Aktivita má jeden povinný vstupní argument Zadost typu ZadostRequest a nemá žádný výstupní argument (proto dědí ze základní třídy CodeActivity a metoda Execute() je návratového typu void).
V metodě Execute() se provádí uložení potřebných údajů o žádosti do databáze (v naší business vrstvě je to pouze simulováno). Jedním z těchto údajů je název typu workflow, v rámci kterého aktuální aktivita běží. Ten zde získáváme vlastností RootActivity, která pomoci reflekce čte hodnotu stejnojmenné internal vlastnosti základní třídy Activity. Název typu je formátován do tvaru typ, assembly. Druhým údajem je WorkflowInstanceId, které je dostupné na objektu context.
Dále je prováděno volání vlastního generování a odeslání emailu s žádostí, které umístíme do samostatné pomocné statické třídy MailGenerator.
using System;
using System.Collections.Generic;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni
{
internal static class MailGenerator
{
#region constants
//TODO: Url přesunout do konfigurace
private const string cRozhodnutiZadostiPageUrl = "http://localhost/PlanAbsenci.Web/rozhodnuti-zadosti-o-schvaleni-absence.aspx";
#endregion
#region action methods
public static void SendZadost(ZadostRequest zadost)
{
if (zadost == null)
{
throw new ArgumentNullException("zadost");
}
var planAbsence = PlanAbsence.Get(zadost.IDPlanAbsence);
var osoba = Osoba.Get(zadost.IDOsoby);
var prijemce = Osoba.Get(zadost.IDOsobyPrijemce);
new MailDefinition()
{
TemplateName = "MailTemplates\SchvaleniAbsenceZadost.htm",
Recipients = new System.Net.Mail.MailAddress(prijemce.Email, prijemce.CeleJmeno).ToString(),
Replacements = new Dictionary<string, string>()
{
{ "Osoba", string.Format("{0} ({1})", osoba.CeleJmeno, osoba.OsobniCislo) },
{ "Termin", planAbsence.Termin },
{ "OznaceniAbsence", planAbsence.OznaceniAbsence },
{ "Popis", planAbsence.Popis },
{ "UrlSchvaleni", GenerateLinkUrl(zadost, true) },
{ "UrlZamitnuti", GenerateLinkUrl(zadost, false) }
}
}.Send();
}
public static void SendSchvaleniNeboZamitnutiAbsence(PlanAbsence planAbsence, int IDOsobyRozhodl, bool schvaleno)
{
if (planAbsence == null)
{
throw new ArgumentNullException("planAbsence");
}
var osoba = Osoba.Get(planAbsence.IDOsoby);
var rozhodl = Osoba.Get(IDOsobyRozhodl);
new MailDefinition()
{
TemplateName = schvaleno ? "MailTemplates\SchvaleniAbsence.htm" : "MailTemplates\ZamitnutiAbsence.htm",
Recipients = new System.Net.Mail.MailAddress(osoba.Email, osoba.CeleJmeno).ToString(),
Replacements = new Dictionary<string, string>()
{
{ "Osoba", string.Format("{0} ({1})", osoba.CeleJmeno, osoba.OsobniCislo) },
{ "Rozhodl", string.Format("{0} ({1})", rozhodl.CeleJmeno, rozhodl.OsobniCislo) },
{ "Termin", planAbsence.Termin },
{ "OznaceniAbsence", planAbsence.OznaceniAbsence },
{ "Popis", planAbsence.Popis }
}
}.Send();
}
#endregion
#region private member functions
private static string GenerateLinkUrl(ZadostRequest zadost, bool schvaleno)
{
string query = string.Format("?Guid={0}&Schvaleno={1}", zadost.Guid, schvaleno ? 1 : 0);
return cRozhodnutiZadostiPageUrl + query;
}
#endregion
}
}
V této třídě si kromě metody SendZadost() rovnou připravíme ještě i druhou metodu SendSchvaleniNeboZamitnutiAbsence(), kterou využijeme později a která bude odesílat pracovníkovi email s informací o provedení schválení nebo zamítnutí jeho absence. Obě tyto metody vytváří pro odeslání emailu instanci obecné třídy MailDefinition, jejíž definice může vypadat takto:
using System;
using System.IO;
using System.Net.Mail;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace PlanAbsenci
{
internal class MailDefinition
{
#region member varible and default property initialization
public string TemplateName { get; set; }
public string Recipients { get; set; }
public IDictionary<string, string> Replacements { get; set; }
#endregion
#region action methods
public void Send()
{
if (string.IsNullOrEmpty(TemplateName))
{
throw new InvalidOperationException("Template name is not specified.");
}
if (string.IsNullOrEmpty(Recipients))
{
throw new InvalidOperationException("Recipients is not specified.");
}
string path = this.TemplateName;
if (!Path.IsPathRooted(path))
{
path = Path.Combine(System.IO.Path.GetDirectoryName((System.Reflection.Assembly.GetEntryAssembly() ?? System.Reflection.Assembly.GetExecutingAssembly()).Location), path);
}
string body = File.ReadAllText(path);
var smtpcfg = (System.Net.Configuration.SmtpSection)System.Configuration.ConfigurationManager.GetSection("system.net/mailSettings/smtp");
if (smtpcfg == null || smtpcfg.Network == null || string.IsNullOrEmpty(smtpcfg.From))
{
throw new InvalidOperationException("From address is not specified in configuration.");
}
string from = smtpcfg.From;
body = ApplyReplacements(body, this.Replacements);
string subject = ExtractSubjectFromHtmlBody(body);
using (var message = new MailMessage() { Body = body, IsBodyHtml = true, From = new MailAddress(from) })
{
foreach (var adr in ParseRecipients(this.Recipients))
{
message.To.Add(adr);
}
if (!string.IsNullOrEmpty(subject))
{
message.Subject = subject;
}
var client = new SmtpClient();
client.Send(message);
}
}
#endregion
#region private member functions
private static string ApplyReplacements(string str, IDictionary<string, string> replacements)
{
if (replacements != null)
{
foreach (var item in replacements)
{
if (!string.IsNullOrEmpty(item.Key))
{
string name = item.Key.StartsWith("<%", StringComparison.Ordinal) ? item.Key : "<%" + item.Key + "%>";
string value = System.Web.HttpUtility.HtmlEncode(item.Value) ?? "";
str = Regex.Replace(str, System.Web.HttpUtility.HtmlEncode(name), value, RegexOptions.IgnoreCase);
str = Regex.Replace(str, name, value, RegexOptions.IgnoreCase);
}
}
}
return str;
}
private static string ExtractSubjectFromHtmlBody(string body)
{
if (body == null)
{
return null;
}
int index = -1;
int index2 = -1;
int index3 = -1;
var sb = new System.Text.StringBuilder();
foreach (string line in ReadBodyLines(body))
{
if (index == -1)
{
index = line.IndexOf("<head>", StringComparison.OrdinalIgnoreCase);
if (index == -1)
{
continue;
}
}
if (index2 == -1)
{
index2 = line.IndexOf("<title>", index, StringComparison.OrdinalIgnoreCase);
if (index2 == -1)
{
index2 = line.IndexOf("</head>", index, StringComparison.OrdinalIgnoreCase);
if (index2 != -1)
{
break;
}
continue;
}
}
index3 = line.IndexOf("</title>", index2, StringComparison.OrdinalIgnoreCase);
if (index3 != -1)
{
if (index2 != 0) //line with "<title>"
{
sb.Append(line.Substring(index2 + "<title>".Length, index3 - index2 - "<title>".Length));
break;
}
sb.Append(line.Substring(0, index3));
break;
}
if (index2 != 0) //line with "<title>"
{
sb.Append(line.Substring(index2 + "<title>".Length));
index2 = 0;
continue;
}
sb.Append(line);
}
string subject = System.Web.HttpUtility.HtmlDecode(sb.ToString().Trim());
return subject.Length == 0 ? null : subject;
}
private static IEnumerable<string> ReadBodyLines(string body)
{
using (var reader = new StringReader(body))
{
while (true)
{
string line = reader.ReadLine();
if (line == null)
{
yield break;
}
yield return line;
}
}
}
private static IEnumerable<MailAddress> ParseRecipients(string addresses)
{
if (string.IsNullOrEmpty(addresses))
{
yield break;
}
for (int i = 0; i < addresses.Length; i++)
{
yield return ParseMailAddress(addresses, ref i);
}
}
private static MailAddress ParseMailAddress(string addresses, ref int i)
{
int index = addresses.IndexOfAny(new[] { '"', ',' }, i);
if (index != -1 && addresses[index] == '"')
{
index = addresses.IndexOf('"', index + 1);
if (index == -1)
{
throw new FormatException("Invalid mail address format");
}
index = addresses.IndexOf(',', index + 1);
}
if (index == -1)
{
index = addresses.Length;
}
var adr = new MailAddress(addresses.Substring(i, index - i).Trim(' ', '\t'));
i = index;
return adr;
}
#endregion
}
}
Třída obsahuje relativní cestu k souboru HTML šablony emailové zprávy, v našem případě jsou to konkrétně tyto:
- “MailTemplates\SchvaleniAbsenceZadost.htm”
- “MailTemplates\SchvaleniAbsence.htm”
- “MailTemplates\ZamitnutiAbsence.htm”
Dále adresu příjemce, která je pomoci objektu System.Net.Mail.MailAddress vyplňována včetně jména pracovníka jako alias, a replacementy tj. hodnoty, které se mají v příslušné šabloně nahradit. Jako subject emailové zprávy je použit Title uvedený přímo v HTML šabloně.
Email s žádostí obsahuje odkaz pro schválení a odkaz pro zamítnutí žádosti. Odkazy jsou generované v metodě GenerateLinkUrl(). URL stránky, na kterou odkazy směrují, by bylo v reálné aplikaci uloženo v nějaké konfiguraci. Parametry query stringu jsou pouze identifikace žádosti a příznak zda se jedná o schválení či zamítnutí.
Další pomocná aktivita ObdrzeniRozhodnutiZadosti bude trochu odlišná, než ostatní code aktivity. Tato aktivita bude totiž realizovat čekání na rozhodnutí žádosti tj. na to, že uživatel aktivoval odkaz z generovaného a odeslaného emailu, resp. na to, že se do workflow zasílají nějaká data zvenku. Pro výstup této aktivity nejprve doplníme typ ZadostResponse.
using System;
namespace PlanAbsenci.Schvaleni
{
[Serializable]
public class ZadostResponse
{
#region member varible and default property initialization
public bool Schvaleno { get; internal set; }
public int? IDOsobyRozhodl { get; internal set; }
#endregion
#region constructors and destructors
internal ZadostResponse() { }
#endregion
}
}
Typ obsahuje identifikaci pracovníka, který žádost rozhodl (pokud jí bylo možné získat), a příznak zda je provedeno schválení či zamítnutí žádosti.
using System;
using System.Activities;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class ObdrzeniRozhodnutiZadosti : NativeActivity<ZadostResponse>
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<Guid> ZadostGuid { get; set; }
#endregion
#region property getters/setters
protected override bool CanInduceIdle
{
get { return true; }
}
#endregion
#region private member functions
protected override void Execute(NativeActivityContext context)
{
string bookmarkName = "ObdrzeniRozhodnutiZadosti_" + this.ZadostGuid.Get(context).ToString();
context.CreateBookmark(bookmarkName, new BookmarkCallback(OnComplete));
}
private void OnComplete(NativeActivityContext context, Bookmark bookmark, object state)
{
ZadostResponse item = (ZadostResponse)state;
context.SetValue(this.Result, item);
}
#endregion
}
}
Aktivita ObdrzeniRozhodnutiZadosti nedědí ze základní třídy CodeActivity resp. CodeActivity<T>, ale ze třídy NativeActivity resp. NativeActivity<T>, která umožňuje přístup i k funkcím workflow runtime. T je stejně jako u obyčejné code aktivity datový typ výstupního argumentu Result. V našem případě má aktivita jeden povinný vstupní argument ZadostGuid typu Guid a jeden výstupní argument Result typu ZadostResponse.
Pro realizaci čekání na externí událost se ve WF používají tzv. bookmarks. Řešení má dva nutné kroky:
- V metodě Execute() (která má i u generické základní třídy návratový typ void) se provede vytvoření bookmarku. To se provádí voláním context.CreateBookmark() s parametry: zvolený název bookmarku a delegát pro registraci callback funkce. Název bookmarku je nutné zvolit jedinečně pro danou instanci workflow tj. pro typ aktivity i každou její instanci v rámci jednoho workflow. V našem případě je proto v tomto názvu obsažen GUID žádosti. Vytvořením bookmarku se zahájí pasivní čekání na notifikaci tykající se daného bookmarku (vlastní zaslání notifikace budeme implementovat později).
- Dokud aktivita neobdrží notifikaci pro vytvořený bookmark, je z hlediska svého stavu registrována jako že pořád běží. Teprve při obdržení notifikace je vyvolán callback registrovaný při vytváření bookmarku, v našem případě tedy metoda OnComplete(). V této metodě nastavujeme hodnotu výstupního argumentu Result na obdržená data (hodnota parametru state). Nastavení výstupního argumentu se provádí voláním metody context.SetValue(). Po ukončení běhu callback metody je automaticky ukončen i běh samotné aktivity a řízení je předáno aktivitě další, případně je ukončeno celé workflow apod.
Hodnota vlastnosti CanInduceIdle říká, zda je možné v době čekání na notifikaci považovat instanci celého workflow za nečinnou tj. případně jí persistentně uložit a úplně odstranit z paměti (pokud to workflow podporuje). V takovém případě musí být workflow z persistentního úložiště opět obnoveno před zasláním notifikace.
Při rozhodnutí jedné žádosti (při obdržení notifikace v aktivitě ObdrzeniRozhodnutiZadosti) bude potřeba provést odstranění/zneplatnění ostatních žádostí. K tomu bude sloužit pomocná aktivita StornoZadosti.
using System;
using System.Activities;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class StornoZadosti : CodeActivity
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<ZadostRequest> Zadost { get; set; }
#endregion
#region private member functions
protected override void Execute(CodeActivityContext context)
{
var zadost = context.GetValue(this.Zadost);
//Provedení storna dílčí žádosti
PlanAbsenceZadost.DeleteZadost(zadost.Guid);
}
#endregion
}
}
Aktivita má pouze jeden povinný vstupní argument Zadost typu ZadostRequest a nemá žádný výstupní argument. V metodě Execute() je pouze provedeno volání odstranění žádosti s daným GUID (metodou PlanAbsenceZadost.DeleteZadost() business vrstvy).
Zbývají nám poslední dvě code aktivity, a sice aktivita pro schválení a aktivita pro zamítnutí dovolené.
using System;
using System.Activities;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class AbsenceSchvaleni : CodeActivity
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<ZadostRequest> Zadost { get; set; }
[RequiredArgument]
public InArgument<ZadostResponse> ZadostResponse { get; set; }
#endregion
#region private member functions
protected override void Execute(CodeActivityContext context)
{
var zadost = context.GetValue(this.Zadost);
var zadostResponse = context.GetValue(this.ZadostResponse);
//Pokud se neprovedla autentizace při rozhodnutí žádosti, použijeme jako identitu příjemce žádosti
int IDOsobyRozhodl = zadostResponse.IDOsobyRozhodl ?? zadost.IDOsobyPrijemce;
//Schválení dovolené
var planAbsence = PlanAbsence.Get(zadost.IDPlanAbsence);
planAbsence.Schvaleni(IDOsobyRozhodl);
MailGenerator.SendSchvaleniNeboZamitnutiAbsence(planAbsence, IDOsobyRozhodl, true);
}
#endregion
}
}
Aktivita pro schválení dovolené má dva povinné vstupní argumenty Zadost typu ZadostRequest a ZadostResponse typu ZadostResponse a nemá žádný výstupní argument. Aktivita si vyžádá příslušnou dovolenou z repository a provede volání metody pro její schválení. Dále je prováděno volání odeslání emailové zprávy o provedení schválení dovolené pomoci dříve připravené metody MailGenerator.SendSchvaleniNeboZamitnutiAbsence().
Implementace aktivity pro zamítnutí dovolené je velmi obdobná, pouze se volá jiná metoda business vrstvy:
using System;
using System.Activities;
using PlanAbsenci.Logic;
namespace PlanAbsenci.Schvaleni.Activities
{
public sealed class AbsenceZamitnuti : CodeActivity
{
#region member varible and default property initialization
[RequiredArgument]
public InArgument<ZadostRequest> Zadost { get; set; }
[RequiredArgument]
public InArgument<ZadostResponse> ZadostResponse { get; set; }
#endregion
#region private member functions
protected override void Execute(CodeActivityContext context)
{
var zadost = context.GetValue(this.Zadost);
var zadostResponse = context.GetValue(this.ZadostResponse);
//Pokud se neprovedla autentizace při rozhodnutí žádosti, použijeme jako identitu příjemce žádosti
int IDOsobyRozhodl = zadostResponse.IDOsobyRozhodl ?? zadost.IDOsobyPrijemce;
//Zamítnutí dovolené
var planAbsence = PlanAbsence.Get(zadost.IDPlanAbsence);
planAbsence.Zamitnuti(IDOsobyRozhodl);
MailGenerator.SendSchvaleniNeboZamitnutiAbsence(planAbsence, IDOsobyRozhodl, false);
}
#endregion
}
}
Tím máme implementovány všechny potřebné code aktivity.