WF Cours 4–Services Avancés. Tutoriel 4.1 Persistance–Partie 2

Ce tutoriel est la suite de la première partie qui consiste à mettre en place un workflow utilisant le service de persistance persistance.

Etape 6 : Préparation du référentiel (Store)

L’objectif de cette étape est de préparer le référentiel qui permettra de persister les workflows sur la BDD SQL Server créée durant les étapes précédentes.

  • Le référentiel utilise une BDD SQL Server. Pour ce, nous avons besoin d’une chaîne de connexion.
  • Déclarez une variable de type « String » appelée « connectionString » comme ceci :
/// <summary>
        /// la chaine de connexion de la base
        /// </summary>
        const string connectionString = "Server=.;Initial Catalog=FormationWF;Integrated Security=SSPI";
  • Utiliser un serveur autre que “.” Si vous avez une installation ou une configuration différente de SQL Server.
  • Au projet « Tutoriel41UI », ajoutez deux réféences sur « System.Runtime.DurableInstancing » et « System.Activities.DurableInstancing »
  • Dans le fichier « MainWindow.xaml.cs » ajoutez deux « using » « System.Activities » et « System.Activities.DurableInstancing »
  • Déclarez une variable privée de type « SqlWorkflowInstanceStore » comme ceci :
/// <summary>
        /// le référentiel SQL Server
        /// </summary>
        private SqlWorkflowInstanceStore _store;
  • Dans la méthode « ConfigurerStore », instanciez le store en utilisant la chaîne de connexion :
// créer le référentiel en utilisant la chaine de connexion
            _store = new SqlWorkflowInstanceStore(connectionString);
  • Nous allons ensuite déclarer les propriétés additionnelles à persister :
// déclarer les propriétés à intégrer avec la persistance
            List<XName> variantProperties = new List<XName>();
            variantProperties.Add(CandidatParticipant.nomns);
            _store.Promote("Candidature", variantProperties, null);
  • Nous allons ensuite configurer le store de façon à ce qu’il soit le store par défaut pour les workflows
// définir le référentiel par défaut
            WorkflowApplication.CreateDefaultInstanceOwner(_store, null, WorkflowIdentityFilter.Any);
  • Le listing complet de la méthode “ConfigurerStore » est comme ceci :
/// <summary>
        /// configure le store SQL Server
        /// </summary>
        private void ConfigurerStore()
        {
            // créer le référentiel en utilisant la chaine de connexion
            _store = new SqlWorkflowInstanceStore(connectionString);
            // déclarer les propriétés à intégrer avec la persistance
            List<XName> variantProperties = new List<XName>();
            variantProperties.Add(CandidatParticipant.nomns);
            _store.Promote("Candidature", variantProperties, null);
            // définir le référentiel par défaut
            WorkflowApplication.CreateDefaultInstanceOwner(_store, null, WorkflowIdentityFilter.Any);
        }
  • Compilez la solution pour vérifier la présence d’erreurs.

Etape 7 : Exécution du workflow

L’objectif de cette étape est créer un workflow pour le dossier d’une nouvelle candidature. Nous allons voir comment dès que le , il est déchargé de la mémoire et persisté sur une base de données.

  • Ouvrez la fenêtre principale « MainWindow » en mode design
  • Affectez un évènement « Click » au bouton « démarrer »
/// <summary>
        /// démarrer une nouvelle candidature
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnDemarrer_Click(object sender, RoutedEventArgs e)
        {
                    }
  • Ajoutez un « using » sur « EmbaucheLibrary » et ajoutez cette bibliothèque aux références si le using n’est pas reonnu.
  • A l’intérieur du bouton de clic, créez une nouvelle instance du workflow
// créer le workflow
            var activity = new EmbaucheWorkflow();
  • Déclarez une variable de type « WorkflowIdentity » comme suit :
/// <summary>
        /// identité du workflow
        /// </summary>
        private WorkflowIdentity _identity = new WorkflowIdentity()
        {
            Name = "Workflow Embauche",
            Version = new Version(1, 0, 0, 0)
        };
  • Ajoutez une méthode « GetIdentity » qui renvoie la variable « _identity ». Nous nous conterons d’une seule version dans ce tutoriel.
 
        /// <summary>
        /// renvoie l'identité du workflow
        /// </summary>
        /// <returns></returns>
        private WorkflowIdentity GetIdentity()
        {
            return _identity;
        }
  • Dans le gestionnaire du click du bouton « demarrer », créer une nouvelle application workflow en utilisant l’identié
// créer une nouvelle application
            var _app = new WorkflowApplication(activity, GetIdentity());
  • Nous allons maintenant ajouter l’Id dela nouvelle application à la liste des workflows.
// empêche de déclencher l'évènement de changement de combobox
            _demarrage = true;
            _liste.Add(_app.Id);
            cbWorkflows.SelectedIndex = _liste.IndexOf(_app.Id);
            _demarrage = false;
  • Nous allons maintenant configurer l’application
// configure l'application
            ConfigurerApplication(_app, txtNom.Text);
  • A la fin, nous devons lancer le workflow
// démarrer le workflow
            _app.Run();
            Console.WriteLine("Workflow {0} démarré", _app.Id);
  • Le listing complet dui gestionnaire de clic devrait être comme ceci :
/// <summary>
        /// démarrer une nouvelle candidature
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnDemarrer_Click(object sender, RoutedEventArgs e)
        {
            // créer le workflow
            var activity = new EmbaucheWorkflow();
            // créer une nouvelle application
            var _app = new WorkflowApplication(activity, GetIdentity());
            // empêche de déclencher l'évènement de changement de combobox
            _demarrage = true;
            _liste.Add(_app.Id);
            cbWorkflows.SelectedIndex = _liste.IndexOf(_app.Id);
            _demarrage = false;
            // configure l'application
            ConfigurerApplication(_app, txtNom.Text);
            // démarrer le workflow
            _app.Run();
            Console.WriteLine("Workflow {0} démarré", _app.Id);
        }
  • Nous allons maintenant passer à la configuration de l’application dans la méthode « ConfigurerApplication »
  • La première chose à faire est d’ associer le store créé précédemment à l’application
// affecter le store
            app.InstanceStore = _store;
  • Nous devons ensuite ajouter l’extension « CandidatParticipant » pour qu’il puisse être intégré dans les traitements
// ajouter l'extension pour pouvoir persister le dossier du candidat
            app.Extensions.Add(new CandidatParticipant() { Nom = NomCandidat });
  • Nous devons créer un évènement qui se déclenche en mode veille. L’évènement active un bouton selon le signet en cours (technique ou oral)
// évènement à déclencher lorsque le workflow est en mode veille
            app.Idle = delegate(WorkflowApplicationIdleEventArgs args)
            {
                var signet = args.Bookmarks.FirstOrDefault();
                if (signet == null)
                    return;
                DesactiverBoutons();
                switch (signet.BookmarkName)
                {
                    case "EvaluationTechnique":
                        ActiverBouton(btnTechnique, true);
                        break;
                    case "EvaluationOrale":
                        ActiverBouton(btnOral, true);
                        break;
                }
            };
  • Nous devons définit l’évènement « PersistIdle » pour indiquer que le worklfow doit être déchargé et persisté en même temps
// évènement se déclenchant avant la persistance
            app.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs args)
            {
                Console.WriteLine("Workflow {0} va être persisté", args.InstanceId);
                return PersistableIdleAction.Unload;
            };
  • Nous ajoutons ensuite l’évènement « Unloaded » qui affiche un message lorsque le workflow est déchargé
// affiche un message lorsque le workflow est déchargé
            app.Unloaded += delegate(WorkflowApplicationEventArgs args)
            {
                Console.WriteLine("Workflow {0} déchargé", args.InstanceId);
            };
  • Nous ajouton ensuite un évènement qui se déclenche lorsque le workflow se termine. L’évènement affiche le statut de la candidature et de terminaison du workflow.
// se déclenche lorsque le workflow se termine
            app.Completed += delegate(WorkflowApplicationCompletedEventArgs args)
            {
                var extensions = args.GetInstanceExtensions<CandidatParticipant>();
                var dossier = extensions.First();
                Console.WriteLine("Workflow {0} terminé avec statut {1}", args.InstanceId, args.CompletionState);
                Console.WriteLine("L'opération d'embauche du dossier {0} a été terminée avec une moyenne de {1}", dossier.Nom, args.Outputs["moyenne"]);
                DesactiverBoutons();
                // supprimer le workflow de la liste
                cbWorkflows.Dispatcher.Invoke(new Action(() => _liste.Remove(args.InstanceId)));
 
            };
  • Le listing complet de « ConfigurerApplication » devrait être comme ceci :
/// <summary>
        /// configure l'application
        /// </summary>
        /// <param name="app"></param>
        /// <param name="NomCandidat"></param>
        private void ConfigurerApplication(WorkflowApplication app, string NomCandidat = null)
        {
            // affecter le store
            app.InstanceStore = _store;
            // ajouter l'extension pour pouvoir persister le dossier du candidat
            app.Extensions.Add(new CandidatParticipant() { Nom = NomCandidat });
            // évènement à déclencher lorsque le workflow est en mode veille
            app.Idle = delegate(WorkflowApplicationIdleEventArgs args)
            {
                var signet = args.Bookmarks.FirstOrDefault();
                if (signet == null)
                    return;
                DesactiverBoutons();
                switch (signet.BookmarkName)
                {
                    case "EvaluationTechnique":
                        ActiverBouton(btnTechnique, true);
                        break;
                    case "EvaluationOrale":
                        ActiverBouton(btnOral, true);
                        break;
                }
            };
 
            // évènement se déclenchant avant la persistance
            app.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs args)
            {
                Console.WriteLine("Workflow {0} va être persisté", args.InstanceId);
                return PersistableIdleAction.Unload;
            };
 
            // affiche un message lorsque le workflow est déchargé
            app.Unloaded += delegate(WorkflowApplicationEventArgs args)
            {
                Console.WriteLine("Workflow {0} déchargé", args.InstanceId);
            };
 
            // se déclenche lorsque le workflow se termine
            app.Completed += delegate(WorkflowApplicationCompletedEventArgs args)
            {
                var extensions = args.GetInstanceExtensions<CandidatParticipant>();
                var dossier = extensions.First();
                Console.WriteLine("Workflow {0} terminé avec statut {1}", args.InstanceId, args.CompletionState);
                Console.WriteLine("L'opération d'embauche du dossier {0} a été terminée avec une moyenne de {1}", dossier.Nom, args.Outputs["moyenne"]);
                DesactiverBoutons();
                // supprimer le workflow de la liste
                cbWorkflows.Dispatcher.Invoke(new Action(() => _liste.Remove(args.InstanceId)));
 
            };
        }
  • Compilez pour vérifer l’absence d’erreurs
  • Exécutez l’application, entrez un nom puis cliquez sur « Nouvelle Candidature »

image

 

  • Remarquez le message indiquant que le workflow a été déchargé et que le bouton « Evaluer Technique » a été activé à cause du signet
  • Allez dans SQL Server Management Studio
  • Affichez le contenu de la table « InstancesTable » de la BDD « FormationWF »

image

  • Remarquez la valeur du champ « Id » qui représente l’id du workflow
  • Remarquez la valeur du champ « BlockingBookmarks » qui indique le signet sur lequel est bloqué le workflow
  • Affichez le contenu de la table « InstancePromotedPropertiesTable »
image

 

  • Remarquez la présence du nom du dossier entré précédemment

Etape 8 : Chargement du Workflow

L’étape précédente a mis en place la persistance. L’objectif de cette étape est de mettre en place le mécanisme inverse permettant de charger un workflow déchargé et persisté.

  • Quittez l’application pour revenir vers Visual Studio
  • Ouvrez « MainWindow.xaml.cs »
  • Nous allons maintenant créer un contexte Enbtity Framework quii nous permettra de nous connecter sur l base de données de persistance.
  • Créez un contexte EntityFramework pointant sur la base de données « FormationWF » et qui inclut une table unique « InstancesTable » .
  • Appelez le contexte « FormationWFEntities »
  • L’option de pluralisation doit être cochée. Le modèle devrait être comme ceci :

image

 

  • Nous allons maintenant changer l’implémentation de la méthode « ChargerListeWorkflows » de façon à ramener cette liste à partir de la BDD
/// <summary>
      /// charge la liste des workflows à partir de la base de données
      /// </summary>
      private void ChargerListeWorkflows()
      {
          using (var ctx = new FormationWFEntities())
          {
              _liste = new ObservableCollection<Guid>(ctx.InstancesTables.Select(et => et.Id).ToList());
          }
          cbWorkflows.ItemsSource = _liste;
      }
  • Nous allons maintenant ajouter la méthode « ChargerApplication » qui permettra de charger un workflow persisté ultérieurement.
  • Ajoutez une méthode privée appelé « ChargerApplication » et dont le type de retour est « WorkflowApplication »
  • La première instruction nous permettra de récupérer l’instance du workflow à partir du référentiel
// récupérer l'instance à partir du store
            var instance = WorkflowApplication.GetInstance((Guid)cbWorkflows.SelectedItem, _store);
  • La deuxième étape est de créer une instance en mémoire du workflow
// créer le workflow en mémoire
var activity = new EmbaucheWorkflow();
  • Ensuite nous créerons une application sur la définition de l’instance et du workflow en mémoire. Ensuite l’application doit être configurée pour qu’elle s’exécute correctement.
// créer l'application
var app = new WorkflowApplication(activity, instance.DefinitionIdentity);
ConfigurerApplication(app);
  • Ensuite, l’étape la plus importante est de charger le workflow
// chager le workflow            
            app.Load(instance);
  • Enfin,on retourne l’application créée.
/// <summary>
        /// change le workflow persisté
        /// </summary>
        /// <returns></returns>
        private WorkflowApplication ChargerApplication()
        {
            // récupérer l'instance à partir du store
            var instance = WorkflowApplication.GetInstance((Guid)cbWorkflows.SelectedItem, _store);
            // créer le workflow en mémoire
            var activity = new EmbaucheWorkflow();
            // créer l'application
            var app = new WorkflowApplication(activity, instance.DefinitionIdentity);
            ConfigurerApplication(app);
            // chager le workflow            
            app.Load(instance);
            return app;
        }
  • La méthode « ChargerApplication » va être utilisée par les deux boutons et la combobox.
  • Un changement de la combobox devrait charger un workflow, afficher les signets bloqués et le relancer, pour ce, implémentez l’évènement « SelectionChanged » comme suit :
/// <summary>
        /// changement de la combo box
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cbWorkflows_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // si démarrage, ne rien faire
            if (_demarrage || (cbWorkflows.SelectedItem == null))
                return;           
            // charge l'application
            var app = ChargerApplication();
            Console.WriteLine("Workflow {0} chargé", app.Id);
            // affiche les signets
            foreach (var signet in app.GetBookmarks())
            {
                Console.WriteLine("Workflow {0}, signet {1} en attente", app.Id, signet.BookmarkName);
            }
            // exécute l'application,
            app.Run();
        }
  • Un clic sur le bouton technique devrait déclencher le signet correspondant. Mais avant, il faut d’abord charger l’application à partir du référentiel.
  • Ajoutez un gestionnaire de clic au bouton d’évaluation technique comme ceci :
private void btnTechnique_Click(object sender, RoutedEventArgs e)
     {
         // charger l'application
         var app = ChargerApplication();
         txtConsole.AppendText(string.Format("La commission technique a donné une note de {0}\n", cbEval.SelectedIndex + 1));
         // déclencher le signet technique
         app.ResumeBookmark("EvaluationTechnique", cbEval.SelectedIndex + 1);
     }
  • De la même façon, affectez un gestionnaire de clic au bouton oral et implémentez-le comme suit :
private void btnOral_Click(object sender, RoutedEventArgs e)
     {
         var app = ChargerApplication();
         txtConsole.AppendText(string.Format("La commission orale a donné une note de {0}\n", cbEval.SelectedIndex + 1));
         app.ResumeBookmark("EvaluationOrale", cbEval.SelectedIndex + 1);
 
     }
 
  • Le listing complet de la classe « MainWindow » devrait être comme ceci :
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    /// <summary>
    /// liste des ids des workflows
    /// </summary>
    ObservableCollection<Guid> _liste;
 
    /// <summary>
    /// la chaine de connexion de la base
    /// </summary>
    const string connectionString = "Server=.;Initial Catalog=FormationWF;Integrated Security=SSPI";
 
    /// <summary>
    /// le référentiel SQL Server
    /// </summary>
    private SqlWorkflowInstanceStore _store;
 
    /// <summary>
    /// indique si on est en démarrage
    /// </summary>
    private bool _demarrage;
 
    /// <summary>
    /// identité du workflow
    /// </summary>
    private WorkflowIdentity _identity = new WorkflowIdentity()
    {
        Name = "Workflow Embauche",
        Version = new Version(1, 0, 0, 0)
    };
 
    /// <summary>
    /// renvoie l'identité du workflow
    /// </summary>
    /// <returns></returns>
    private WorkflowIdentity GetIdentity()
    {
        return _identity;
    }
 
    private void DesactiverBoutons()
    {
        ActiverBouton(btnOral, false);
        ActiverBouton(btnTechnique, false);
    }
 
    private void ActiverBouton(Button bouton, bool valeur)
    {
        bouton.Dispatcher.BeginInvoke(new Action(() => bouton.IsEnabled = valeur));
    }
 
    public MainWindow()
    {
        InitializeComponent();
    }
 
    private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        Console.SetOut(new TextBoxTextWriter(txtConsole));
        DesactiverBoutons();
        // configurer le store
        ConfigurerStore();
        // charger les workflows en cours
        ChargerListeWorkflows();
    }
 
    /// <summary>
    /// charge la liste des workflows à partir de la base de données
    /// </summary>
    private void ChargerListeWorkflows()
    {
        using (var ctx = new FormationWFEntities())
        {
            _liste = new ObservableCollection<Guid>(ctx.InstancesTables.Select(et => et.Id).ToList());
        }
        cbWorkflows.ItemsSource = _liste;
    }
 
    /// <summary>
    /// configure le store SQL Server
    /// </summary>
    private void ConfigurerStore()
    {
        // créer le référentiel en utilisant la chaine de connexion
        _store = new SqlWorkflowInstanceStore(connectionString);
        // déclarer les propriétés à intégrer avec la persistance
        List<XName> variantProperties = new List<XName>();
        variantProperties.Add(CandidatParticipant.nomns);
        _store.Promote("Candidature", variantProperties, null);
        // définir le référentiel par défaut
        WorkflowApplication.CreateDefaultInstanceOwner(_store, null, WorkflowIdentityFilter.Any);
    }
 
 
    /// <summary>
    /// configure l'application
    /// </summary>
    /// <param name="app"></param>
    /// <param name="NomCandidat"></param>
    private void ConfigurerApplication(WorkflowApplication app, string NomCandidat = null)
    {
        // affecter le store
        app.InstanceStore = _store;
        // ajouter l'extension pour pouvoir persister le dossier du candidat
        app.Extensions.Add(new CandidatParticipant() { Nom = NomCandidat });
        // évènement à déclencher lorsque le workflow est en mode veille
        app.Idle = delegate(WorkflowApplicationIdleEventArgs args)
        {
            var signet = args.Bookmarks.FirstOrDefault();
            if (signet == null)
                return;
            DesactiverBoutons();
            switch (signet.BookmarkName)
            {
                case "EvaluationTechnique":
                    ActiverBouton(btnTechnique, true);
                    break;
                case "EvaluationOrale":
                    ActiverBouton(btnOral, true);
                    break;
            }
        };
 
        // évènement se déclenchant avant la persistance
        app.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs args)
        {
            Console.WriteLine("Workflow {0} va être persisté", args.InstanceId);
            return PersistableIdleAction.Unload;
        };
 
        // affiche un message lorsque le workflow est déchargé
        app.Unloaded += delegate(WorkflowApplicationEventArgs args)
        {
            Console.WriteLine("Workflow {0} déchargé", args.InstanceId);
        };
 
        // se déclenche lorsque le workflow se termine
        app.Completed += delegate(WorkflowApplicationCompletedEventArgs args)
        {
            var extensions = args.GetInstanceExtensions<CandidatParticipant>();
            var dossier = extensions.First();
            Console.WriteLine("Workflow {0} terminé avec statut {1}", args.InstanceId, args.CompletionState);
            Console.WriteLine("L'opération d'embauche du dossier {0} a été terminée avec une moyenne de {1}", dossier.Nom, args.Outputs["moyenne"]);
            DesactiverBoutons();
            // supprimer le workflow de la liste
            cbWorkflows.Dispatcher.Invoke(new Action(() => _liste.Remove(args.InstanceId)));
 
        };
    }
 
    /// <summary>
    /// démarrer une nouvelle candidature
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void btnDemarrer_Click(object sender, RoutedEventArgs e)
    {
        // créer le workflow
        var activity = new EmbaucheWorkflow();
        // créer une nouvelle application
        var _app = new WorkflowApplication(activity, GetIdentity());
        // empêche de déclencher l'évènement de changement de combobox
        _demarrage = true;
        _liste.Add(_app.Id);
        cbWorkflows.SelectedIndex = _liste.IndexOf(_app.Id);
        _demarrage = false;
        // configure l'application
        ConfigurerApplication(_app, txtNom.Text);
        // démarrer le workflow
        _app.Run();
        Console.WriteLine("Workflow {0} démarré", _app.Id);
    }
 
    private void btnTechnique_Click(object sender, RoutedEventArgs e)
    {
        // charger l'application
        var app = ChargerApplication();
        txtConsole.AppendText(string.Format("La commission technique a donné une note de {0}\n", cbEval.SelectedIndex + 1));
        // déclencher le signet technique
        app.ResumeBookmark("EvaluationTechnique", cbEval.SelectedIndex + 1);
    }
 
    private void btnOral_Click(object sender, RoutedEventArgs e)
    {
        var app = ChargerApplication();
        txtConsole.AppendText(string.Format("La commission orale a donné une note de {0}\n", cbEval.SelectedIndex + 1));
        app.ResumeBookmark("EvaluationOrale", cbEval.SelectedIndex + 1);
 
    }
 
    /// <summary>
    /// changement de la combo box
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void cbWorkflows_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        // si démarrage, ne rien faire
        if (_demarrage || (cbWorkflows.SelectedItem == null))
            return;           
        // charge l'application
        var app = ChargerApplication();
        Console.WriteLine("Workflow {0} chargé", app.Id);
        // affiche les signets
        foreach (var signet in app.GetBookmarks())
        {
            Console.WriteLine("Workflow {0}, signet {1} en attente", app.Id, signet.BookmarkName);
        }
        // exécute l'application,
        app.Run();
    }
 
    /// <summary>
    /// change le workflow persisté
    /// </summary>
    /// <returns></returns>
    private WorkflowApplication ChargerApplication()
    {
        // récupérer l'instance à partir du store
        var instance = WorkflowApplication.GetInstance((Guid)cbWorkflows.SelectedItem, _store);
        // créer le workflow en mémoire
        var activity = new EmbaucheWorkflow();
        // créer l'application
        var app = new WorkflowApplication(activity, instance.DefinitionIdentity);
        ConfigurerApplication(app);
        // chager le workflow            
        app.Load(instance);
        return app;
    }
}
 
  • Exécutez l’application
  • Vérifiez qu’un workflow reprend même après avoir quitté l’application
  • Vérifiez le comportement avec plusieurs candidats en même temps

image

Téléchargement

Le code du tutoriel peut être obtenu ici.

Enjoy !

Add comment

Loading