Windows Workflow Foundation 4, část 4

Tomáš Holan       4. 8. 2011       Workflow Foundation       6038 zobrazení

Poslední částí, která nám chybí pro dokončení našeho příkladu, je spouštění a hostování připraveného workflow resp. jako propojení se zbytkem aplikace.

V případě našeho workflow je třeba zajistit následující:

  • Akce, která provede spuštění workflow nebude čekat na jeho dokončení (ani ve smyslu asynchronní operace) a kontrola se hned vrátí zpět klientovi. Nastartované workflow bude dále vykonávat daný proces zcela autonomně.
  • Dále je přitom ale potřeba s běžícími instancemi komunikovat, konkrétně umět předat data resp. aktivovat bookmark, na kterém workflow čeká.
  • A v neposlední řádě délka běhu workflow může být třeba i několik dnů (jedná se o tzv. long running workflow), proto se u takovéhoto scénáře nabízí požadavek na podporu perzistence běžících workflow. To umožní, aby nebylo nutné dlouho čekající instance stále držet v paměti a také bude možné provádět restart celého procesu, v rámci kterého je workflow hostovane (typicky NT Service).

Pro potřeby tohoto způsobu hostování workflow si připravíme pomocnou třídu. Třídu umístíme do namespace PlanAbsenci.Schvaleni, pojmenujeme jí WorkflowMediator a bude obsahovat veřejné metody StartWorkflow(), SendDataToWorkflow() a TerminateWorkflow().

using System;
using System.Activities;
using System.Collections.Generic;

namespace PlanAbsenci.Schvaleni
{
    internal class WorkflowMediator
    {
        #region member varible and default property initialization
        private readonly object SyncRoot = new object();
        private readonly Dictionary<Guid, WorkflowApplication> Instances = new Dictionary<Guid, WorkflowApplication>();

        private readonly static WorkflowMediator s_Current = new WorkflowMediator();
        #endregion

        #region action methods
        public Guid StartWorkflow(Type workflowType, IDictionary<string, object> startupParameters)
        {
            var workflowDefinition = LoadWorkflowDefinition(workflowType);

            var instance = new WorkflowApplication(workflowDefinition, startupParameters)
            {
                PersistableIdle = e => PersistableIdleAction.Unload,
                Completed = OnWorkflowCompleted,
                Unloaded = OnWorkflowUnloaded
            };
            instance.InstanceStore = GetWorkflowInstanceStore();

            lock (this.SyncRoot)
            {
                this.Instances.Add(instance.Id, instance);
            }

            //Start workflow
            instance.Run();

            return instance.Id;
        }

        public bool SendDataToWorkflow<T>(Type workflowType, Guid instanceId, string bookmarkName, T data)
        {
            var instance = this.LoadInstance(workflowType, instanceId);
            if (instance == null)
            {
                return false;
            }

            return instance.ResumeBookmark(bookmarkName, data) == BookmarkResumptionResult.Success;
        }

        public void TerminateWorkflow(Type workflowType, Guid instanceId, Exception reason)
        {
            var instance = this.LoadInstance(workflowType, instanceId);
            if (instance == null)   //Workflow instance is not running
            {
                return;
            }

            if (reason == null)
            {
                reason = new System.Activities.WorkflowApplicationTerminatedException("Workflow termination requested.", instanceId);
            }
            instance.Terminate(reason);
        }
        #endregion

        #region property getters/setters
        public static WorkflowMediator Current
        {
            get { return s_Current; }
        }
        #endregion

        #region private member functions
        private WorkflowApplication LoadInstance(Type workflowType, Guid instanceId)
        {
            WorkflowApplication instance;
            lock (this.SyncRoot)
            {
                if (this.Instances.TryGetValue(instanceId, out instance))
                {
                    return instance;
                }
            }

            var workflowDefinition = LoadWorkflowDefinition(workflowType);

            instance = new WorkflowApplication(workflowDefinition)
            {
                InstanceStore = GetWorkflowInstanceStore(),
                PersistableIdle = e => PersistableIdleAction.Unload,
                Completed = OnWorkflowCompleted,
                Unloaded = OnWorkflowUnloaded
            };

            try
            {
                instance.Load(instanceId);
            }
            catch (System.Exception)
            {
                //Workflow instance is not running
                return null;
            }

            lock (this.SyncRoot)
            {
                this.Instances.Add(instance.Id, instance);
            }
            return instance;
        }

        private static void OnWorkflowCompleted(WorkflowApplicationCompletedEventArgs e)
        {
            if (e.CompletionState == ActivityInstanceState.Faulted)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Workflow '{0}' terminated", e.InstanceId));

                if (!(e.TerminationException is System.Activities.WorkflowApplicationTerminatedException))
                {
                    var ex = new InvalidOperationException(string.Format("Workflow InstanceId '{0}' terminated, see inner exception for details.", e.InstanceId), e.TerminationException);

                    //TODO: Log exception
                    System.Diagnostics.Debug.WriteLine(ex.ToString());
                }
                return;
            }
            if (e.CompletionState == ActivityInstanceState.Canceled)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Workflow '{0}' canceled", e.InstanceId));
            }
        }

        private void OnWorkflowUnloaded(WorkflowApplicationEventArgs e)
        {
            lock (this.SyncRoot)
            {
                this.Instances.Remove(e.InstanceId);
            }
        }

        private static System.Runtime.DurableInstancing.InstanceStore GetWorkflowInstanceStore()
        {
            //TODO: Setup persistence store
            return null;
            //return new System.Activities.DurableInstancing.SqlWorkflowInstanceStore(System.Configuration.ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
        }

        private static Activity LoadWorkflowDefinition(Type workflowType)
        {
            if (workflowType == null)
            {
                throw new ArgumentNullException("workflowType");
            }
            if (!typeof(Activity).IsAssignableFrom(workflowType))
            {
                throw new ArgumentException("workflowType must be of Activity type.", "workflowType");
            }
            if (workflowType.GetConstructor(Type.EmptyTypes) == null)
            {
                throw new ArgumentException("workflowType must have public default constructor.", "workflowType");
            }

            return (Activity)Activator.CreateInstance(workflowType);
        }
        #endregion
    }
}

Metoda StartWorkflow() umožňuje spustit novou instanci workflow. To které workflow se spouští se předává jako parametr workflowType typu Type. Druhý parametr startupParameters obsahuje dvojice název vstupního argumentu workflow, hodnota. Pomocnou metodou LoadWorkflowDefinition() se okontroluje, že třída určená parametrem workflowType dědí ze třídy Activity a vytvoří se její nová instance pomoci výchozího konstruktoru. Dále se konstruuje objekt WorkflowApplication a jeho metodou Run() se vytvořená instance workflow spustí. Nastartované workflow je také přidáno do dictionary Instances, které udržuje všechny instance, které jsou aktuálně načtené v paměti.

Metoda SendDataToWorkflow() umožňuje provést zaslání notifikace a předání dat do dříve spuštěné instance workflow. Parametry workflowType a instanceId slouží pro obnovení instance workflow a bookmarkName je identifikátor bookmark. Instance workflow se obnovuje pomocnou metodou LoadInstance(). Po jejím obnovení je vlastní zaslání notifikace prováděno metodou ResumeBookmark().

Metoda TerminateWorkflow() umožňuje “násilně” ukončit dříve spuštěné workflow. Po obnovení instance je tak prováděno metodou Terminate(). Metoda se může hodit např. pokud by bylo business logikou prováděno storno celé žádosti o schválení dovolené nebo by byl záznam o dovolené odstraněn a existovala k němu aktuální žádost o schválení.

Pomocná metoda LoadInstance() obnovuje workflow takto: Nejprve se zkouší, zda není instance aktuálně v paměti (v dictionary Instances). Pokud ano, je rovnou vrácena. V opačném případě se vytvoří nový objekt WorkflowApplication (toto je důvod, proč je pro všechny operace s běžícím workflow potřeba i parametr workflowType) a pak se na něm volá metoda Load() s předaným identifikátorem dříve spuštěné instance workflow. Tím dojde k obnovení stavu instance z perzistentního úložiště (pokud by nebyla perzistence používána, neprovede vůbec workflow runtime odstranění běžící instance z paměti). Následně je instance opět přidána do dictionary Instances.

Odstranění instance workflow z dictionary Instances je prováděno v registrované obsluze události Unloaded objektu WorkflowApplication.

Pomocná metoda GetWorkflowInstanceStore() vrací konfiguraci poskytovatele pro perzistenci workflow
(nebo hodnotu null pokud se perzistence nepoužívá). Ve WF je dodávána implementace perzistentního uložiště pro databázi v SQL Server 2005/2008 SqlWorkflowInstanceStore. Postup vytvoření a konfigurace tohoto uložiště je popsáno zde a zde. O povolení perzistence ve WF se dočtete zde.

Dále si připravíme třídu, která bude zastřešovat celý náš proces schválení dovolené. Tomu budou odpovídat metody pro spuštění schválení absence a pro obsloužení rozhodnutí žádosti.

using System;
using System.Collections.Generic;
using PlanAbsenci.Logic;

namespace PlanAbsenci.Schvaleni
{
    public static class SchvaleniAbsence
    {
        #region action methods
        public static void OdeslaniZadosti(PlanAbsence planAbsence)
        {
            if (planAbsence == null)
            {
                throw new ArgumentNullException("planAbsence");
            }

            //Vytvoření a předání požadavku Workflow
            var absence = new AbsenceRequest()
            {
                IDPlanAbsence = planAbsence.IDPlanAbsence,
                IDOsoby = planAbsence.IDOsoby,
            };

            //TODO: Konfigurace, které workfow se má pro schválení žádosti použít
            //Výchozí workflow pro schválení absence
            Type workflowType = typeof(PlanAbsenci.Schvaleni.Workflow.SchvaleniAbsenceVedoucim);

            var parameters = new Dictionary<string, object>()
            { 
                { "Absence", absence }, 
            };

            WorkflowMediator.Current.StartWorkflow(workflowType, parameters);
        }

        public static bool RozhodnutiZadosti(Guid zadostGuid, bool schvaleno, int? IDOsobyRozhodl)
        {
            var zadost = PlanAbsenceZadost.GetZadost(zadostGuid);
            if (zadost == null)
            {
                //Žádost byla již rozhodnutá nebo stornována
                return false;
            }

            //Předání rozhodnutí Workflow             
            string bookmarkName = "ObdrzeniRozhodnutiZadosti_" + zadostGuid.ToString();

            var zadostResponse = new ZadostResponse()
            {
                Schvaleno = schvaleno,
                IDOsobyRozhodl = IDOsobyRozhodl
            };

            return WorkflowMediator.Current.SendDataToWorkflow(Type.GetType(zadost.WorkflowType, true, false), zadost.WorkflowInstanceId, bookmarkName, zadostResponse);
        }
        #endregion
    }
}

Metoda OdeslaniZadosti() bude volána pro konkrétní žádost tj. přímo objekt PlanAbsence business vrstvy. V metodě je naznačené provedení nějaké další logiky pro výběr, které workflow bude voláno, v uvedené implementaci to ale bude naše jediné připravené workflow:
PlanAbsenci.Schvaleni.Workflow.SchvaleniAbsenceVedoucim
Dále je vytvořen objekt s parametrem Absence, který je při startu workflow předán.

Druhá metoda RozhodnutiZadosti() slouží pro odeslání notifikace a předání informací o rozhodnutí žádosti workflow, které se nachází ve fázi čekání (po odeslaní žádostí). Parametry metody jsou informace z url generovaného odkazu emailové zprávy tj. identifikátor (Guid) žádosti a příznak zda je žádost schválena nebo zamítnuta. Posledním parametrem je identifikace osoby, která provedla rozhodnutí žádosti. Ta by se získala jako výsledek Windows autentizace při vstupu na stránku, na kterou je směřován odkaz emailové zprávy. Pokud by  nebylo možné uživatele autentizovat, bude IDOsobyRozhodl null a příslušná aktivita pak použije identifikaci původního příjemce žádosti. Metoda vrací, zda bylo možné notifikaci zaslat, což odpovídá tomu, že žádost nebyla již dříve rozhodnutá nebo stornována.

Tím je veškerá implementace infrastruktury celého našeho procesu schvalování dovolené dokončena.
Pro její použití stačí provádět následující:

  1. Z obsluhy funkce např. v UI klientské aplikaci budeme volat metodu OdeslaniZadosti() třídy SchvaleniAbsence. Do parametru bude předáván příslušný objekt business vrstvy.
  2. Z obsluhy webové stránky, na kterou je směrován odkaz generované emailové zprávy budeme volat metodu RozhodnutiZadosti() třídy SchvaleniAbsence. Do parametru budou předávány údaje z odkazu a identifikace uživatele provádějícího danou akci (pokud je jí možné pro aktuálního uživatele získat).

Věřím, že jsem v tomto seriálu ukázal alespoň základní postupy pro využití technologie WF a jejího začlenění jako součást standardní např. business aplikace.

 

hodnocení článku

0 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback