Mostefai Mohammed Amine's Blog

Say That I Was Here !

Tutoriel 5.1 - Création d’un service de recrutement–Partie 2

Dans la partie précédente, nous avons créé les services et les workflows nécessaires à notre opération de recrutement. Durant cette partie, nous ferons le reste : les applications clientes. Etape 7 : Création de l’application candidat L’objectif de cette étape est de créer l’application « Candidat » qui permettra à un postulant de soumettre une candidature. Cette application sera également serveur puisqu’elle hébergera un service de notification. Ouvrez Visual Studio dans une nouvelle fenêtre en mode administrateur Créez une nouvelle application WPF et appelez-la « CandidatApp » Ouvrez « MainWindow » en mode design Supprimez la grille Glissez un « DockPanel » sur la fenêtre principale avec les propriétés « HorizontalAlignment » et « VerticalAlignment » à « Stretch », supprimez les propriétés « Height » et « Width ». « LastChildFill » doit être «True » Glissez un « Label » sur le « StackPanel » avec la propriété « Content » à « Nom : » et « Margin » à 3 et « DockPanel.Dock » à « Top » Glissez un « TextBox » avec la propriété « Name » à « txtNom » et « Margin » à 3 et « DockPanel.Dock » à « Top » Glissez un deuxième « Label » en dessous de la « TextBox » avec la propriété « Content » à « Date de naissance : » et « Margin » à 3 et « DockPanel.Dock » à « Top » Glissez un « DatePicker » en dessous du deuxième « Label » avec les propriétés « Name » à « dpDate », « Margin » à 3 et « DockPanel.Dock » à « Top » Ajoutez un bouton en dessous du « Picker » avec « Name », « Margin », « Content » à « btnSoumettre », 3 et « Soumettre » Glissez un « TextBox » en dessous du bouton  avec la propriété « Name » à « txtConsole », « Margin » à 3, « AcceptReturn » à « True » Le code XAML de la fenêtre principale doit être comme ceci : <Window x:Class="CandidatApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" LastChildFill="True" > <Label Content="Nom :" Margin="3" DockPanel.Dock="Top"/> <TextBox Name="txtNom" Margin="3" DockPanel.Dock="Top"/> <Label Content="Date de Naissance :" Margin="3" DockPanel.Dock="Top"/> <DatePicker Name="dpDate" Margin="3" DockPanel.Dock="Top"/> <Button Content="Soumettre" DockPanel.Dock="Top" Margin="3" Name="btnSoumettre"/> <TextBox Name="txtConsole" Margin="3" AcceptsReturn="True" TextWrapping="Wrap" VerticalAlignment="Stretch"/> </DockPanel></Window> Lancez la boîte de dialogue d’ajout de nouvel élément (au projet CandidatApp) à partir de l’explorateur de solutions Dans la zone de recherche, tapez « WCF » Sélectionnez « Service WCF » Dans la zone « Nom », entrez « NotificationService » Remarquez que VS crée une interface appelée « INotificationService » et son implémentation « NotificationService Ouvrez le fichier « INotificationService.cs » Supprimez la déclaration de méthode « DoWork » Ajoutez une méthode de type « void » appelée « Notifier » et qui a deux paramètres « Nom » et « Acceptation » de type « String » et « Boolean » Décorez la méthode par l’attribut « OperationContract » pour la rendre comme service Le listing de l’interface INotificationService devrait être comme ceci : [ServiceContract] public interface INotificationService { [OperationContract] void Notifier(string Nom, bool Acceptation); } Ouvrez le fichiez « NotificationService.cs » Supprimez le code de « DoWork » Implémentez la méthode « Notifier » comme ceci : public void Notifier(string Nom, bool Acceptation) { string etat; if (Acceptation) etat = "accepté"; else etat = "rejeté"; Console.WriteLine("Le candidat {0} a été {1} ", Nom, etat);  } Compilez l’application. Ajoutez au projet la classe « TextBoxTextWriter » créé lors des tutoriaux précédents pour rediriger les sorties de la console sur la « TextBox » txtConsole Ouvrez « MainWindow.xaml.cs » Ajoutez un using sur l’esapce de nom de la classe « TextBoxTextWriter » Ajoutez un using sur « System.ServiceModel » Ajoutez un évènement de chargement « Loaded » sur le « DockPanel » Redirigez la sortie de la console// rediriger la sortie de la console Console.SetOut(new TextBoxTextWriter(txtConsole)); Créez ensuite le hôte qui permettra d’herbeger le service WCF comme ceci : // créer le hote wcf var host = new ServiceHost(typeof(NotificationService)); // lancer le hote wcf host.Open(); Console.WriteLine("service de notification démarré"); Nous allons enfin affectez une valeur par défaut à la date de naissance qui est de 20 ans avant aujourd’hui// date par défaut dpDate.SelectedDate = DateTime.Now.AddYears(-20); Le listing complet de la méthode devrait être comme ceci : private void DockPanel_Loaded(object sender, RoutedEventArgs e) { // rediriger la sortie de la console Console.SetOut(new TextBoxTextWriter(txtConsole)); // créer le hote wcf var host = new ServiceHost(typeof(NotificationService)); // lancer le hote wcf host.Open(); Console.WriteLine("service de notification démarré"); // date par défaut dpDate.SelectedDate = DateTime.Now.AddYears(-20); } Ouvrez le fichier « App.Config » Changez la propriété « baseAddress » de l’hôte à http://localhost:7766/Notification Le listing de « App.Config » devrait être comme ceci : <?xml version="1.0" encoding="utf-8" ?><configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="CandidatApp.NotificationService"> <endpoint address="" binding="basicHttpBinding" contract="CandidatApp.INotificationService"> <identity> <dns value="localhost" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:7766/Notification" /> </baseAddresses> </host> </service> </services> </system.serviceModel></configuration> Lancez l’application pour tester le service Appelez la méthode « Notifier » à partir du testeur « WCF » Remarquez que les messages de notifications sortent sur la console Etape 8 : Création du proxy vers le workflow Dans l’étape précédente, nous avons créé le serveur de notification qui permettra au workflow de notifier le candidat. Dans cette étape, nous allons nous connecter au service du workflow pour pouvoir soumettre le dossier du candidat. Revenez sur la fenêtre VS qui contient le projet « Tutoriel51 » Lancez l’application « WorkflowConsoleHost » Revenez sur le projet « CandidatApp » Dans l’explorateur de solutions, cliquez sur le bouton droit sur les références puis sur « ajouter une référence de service » Dans la zone « Adresse », entrez http://localhost:9988/EmbaucheSimple Cliquez sur « Go » Dans l’espace de noms, entrez « FormationWF » Dans les services, sélectionnez « EmbaucheSimpleService » puis « IEmbaucheService » Cliquez sur « OK » Ouvrez « MainWndow » en mode design Affectez un évènement « Click » au bouton Créons les informations à transmettre à partir du formulaire // créer les informations var info = new FormationWF.CandidatInfo() { Nom = txtNom.Text, DateNaissance = dpDate.SelectedDate.Value }; Créons ensuite le proxy qui nous permettra de nous connecter au serveur// créer le proxy var client = new FormationWF.EmbaucheServiceClient(); Nous allons ensuite créer les données à transmettre au serveur // données à transmettre var data = new FormationWF.RecevoirCandidature(); data.pInfo = new FormationWF.CandidatInfo() { Nom = txtNom.Text, DateNaissance = dpDate.SelectedDate.Value }; Ensuite nous allons appeler le service et récupérer le résultat // appel du service var res = client.RecevoirCandidature(data); if (res.HasValue && !res.Value) Console.WriteLine("Candidature automatiquement rejetée"); Le listing complet du gestionnaire de clic est comme suit : private void btnSoumettre_Click(object sender, RoutedEventArgs e) { // créer les informations var info = new FormationWF.CandidatInfo() { Nom = txtNom.Text, DateNaissance = dpDate.SelectedDate.Value }; // créer le proxy var client = new FormationWF.EmbaucheServiceClient(); // données à transmettre var data = new FormationWF.RecevoirCandidature(); data.pInfo = new FormationWF.CandidatInfo() { Nom = txtNom.Text, DateNaissance = dpDate.SelectedDate.Value }; // appel du service var res = client.RecevoirCandidature(data); // afficher résultat si rejet auto if (res.HasValue && !res.Value) Console.WriteLine("Candidature automatiquement rejetée"); } Exécutez l’application puis remarquez comment des candidatures de moins de 30 ans sont automatiquement rejetées Etape 9 : Création de l’application de l’évaluateur L’objectif de cette étape est de créer une application qui va être utilisée par l’évaluateur pour évaluer les candidatures. Ouvrez Visual Studio dans une nouvelle fenêtre Créez une nouvelle application WPF et appelez-la « EvaluateurApp » Ouvrez « MainWindow » en mode design Supprimez la grille Glissez un « StackPanel » sur la fenêtre principale avec les propriétés « HorizontalAlignment » et « VerticalAlignment » à « Stretch », supprimez les propriétés « Height » et « Width ». Glissez un « Label » sur le « StackPanel » avec la propriété « Content » à « Nom : » et « Margin » à 3 Glissez un « TextBox » avec la propriété « Name » à « txtNom » et « Margin » à 3 Glissez un deuxième « Label » en dessous de la « TextBox » avec la propriété « Content » à « Note : » et « Margin » à 3 Glissez une « ComboBox » en dessous du deuxième « Label » avec les propriétés « Name » à « cbNote », « Margin » à 3. La combobox contient 5 notes de 1 à 5 et la propriété « SelectedIndex » à 0 Ajoutez un bouton en dessous du « ComboBox » avec « Name », « Margin », « Content » à « btnEvaluer », 3 et « Evaluer » Le XAML de la fenêtre devrait être comme ceci : <Window x:Class="EvaluateurApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel> <Label Content="Nom :" Margin="3"/> <TextBox TextWrapping="Wrap" Name="txtNom" Margin="3"/> <Label Content="Note :" Margin="3"/> <ComboBox Name="cbNote" Margin="3" SelectedIndex="0"> <ListBoxItem Content="1"/> <ListBoxItem Content="2"/> <ListBoxItem Content="3"/> <ListBoxItem Content="4"/> <ListBoxItem Content="5"/> </ComboBox> <Button Content="Evaluer" Name="btnEvaluer"/> </StackPanel>  </Grid></Window> Dans l’explorateur de solutions, cliquez sur le bouton droit sur les références puis sur « ajouter une référence de service » Dans la zone « Adresse », entrez http://localhost:9988/EmbaucheSimple Cliquez sur « Go » Dans l’espace de noms, entrez « FormationWF » Dans les services, sélectionnez « EmbaucheSimpleService » puis « IEmbaucheService » Cliquez sur « OK » Ajoutez un gestionnaire de clic au bouton « Evaluer » qui permet d’appeler la méthode « Evaluer » du service private void btnEvaluer_Click(object sender, RoutedEventArgs e) { // créer le proxy var proxy = new FormationWF.EmbaucheServiceClient(); // appeler le service proxy.Evaluer(txtNom.Text, cbNote.SelectedIndex + 1); } Compilez l’application pour vérifier d’éventuelles erreurs. Etape 10 : Finalisation du workflow L’objectif de cette étape est de finaliser le workflow de façon à ce qu’après l’évaluation, il envoie une notification au candidat. Revenez à la solution « Tutoriel51 » Si l’application console est en exécution, quittez-la Glissez une activité « Send » au workflow Connectez le dernier « Assign » avec l’activité « Send » Connectez le nouveau « Send » avec le côté « False » de l’activité de décision Dans la zone « OperationName », entrez « Notifier » Cliquez sur le bouton « Contenu » Ajoutez un paramètre appelé « Nom » de type « String » et dont la valeur est « info .Nom » Ajoutez un deuxième paramètre appelée « Acceptation », de type « Boolean » et dont la valeur est « Resultat »   Cliquez sur « OK » Dans la propriété « EndPoint.AddressUri » de l’activité « Send », entrez http://localhost:7766/Notification. C’est l’adresse du service de notification. Dans la propriété « EndPoint.Binding », de l’activité « Send », sélectionnez « basicHttpBinding » Dans la propriété « ServiceContractName » entrez « INotificationService » Le workflow devrait être comme ceci : Etape 11 : Mise en place de la corrélation Le workflow dans l’étape précédente est fonctionnel mais il reste un problème : lorsque l'âge du candidat dépasse 30 ans, l’évaluateur doit lui donner une note. Le problème est qu’un instant donné, plusieurs instances peuvent s’exécuter en parallèle. Lorsqu’un évaluateur donne une note, on doit affecter cette note au bon candidat. La solution pour trouver la bonne instance du workflow est la corrélation. Nous établirons une corrélation sur le nom du candidat. Ouvrez le workflow « EmbaucheSimpleService.xamlx » en mode design Remarquez que notre workflow contient deux activités « Receive ». Pour assurer une cohérence d’exécution, nous relierons les deux « Receive » par une corrélation. Le premier « Receive » doit initialiser une corrélation tandis que le deuxième doit s’appliquer à cette corrélation. Cliquez sur l’activité organigramme (FlowChart) parente Examinez les variables Remarquez la présence d’une variable appelée « __handle » de type « CorrlationHandle » Si cette variable n’existe pas, créez-la Changez la portée de la variable (Scope) pour qu’elle soit sur tout l’organigramme Renommez cette variable en « dossier » Double-cliquez sur la première séquence du workflow Cliquez sur la propriété « CorrelationInitializers » de l’activité « Receive » de la séquence Dans la zone « Add initializer », entrez « dossier » Dans la liste déroulante au dessus de la grille « XPath Queries », sélectionnez « Query Correlation Initializer » Dans la grille, dans la colonne « Query », sélectionnez la propriété « Nom » du paramètre « pInfo » Remarquez que VS génère automatiquement le chemin « XPath » relatif à la propriété Laissez la colonne « Key » à « Key1 » Cliquez sur « OK » Nous allons maintenant corréler la deuxième activité « Receive » avec la première Dans la deuxième activité « Receive » (Recevoir Evaluation), cliquez sur le bouton de la propriété « CorrelateOn » Dans la zone « Correlates With », entrez « dossier » Dans la « XPath Queries », sélectionnez le paramètre « Nom ». Remarquez que le chemin « XPath » est automatiquement généré. Ce que nous avons fait c’est que nous utilierons le paramètre « Nom » fourni par l’évaluateur pour trouver la bonne instance du workflow. Cliquez sur « OK » Le workflow est maintenant prêt. Compilez pour vérifier l’absence d’erreurs. Etape 12 : Exécution et Tests L’objectif de cette étape est de tester nos services. Nous validerons les trois cas de figures : Si le candidat a moins de 30 ans, sa candidature est automatiquement rejetée. Il reçoit immédiatement une notification. Si le candidat a plus de 30 ans, l’évaluateur doit l’évaluer. Lorsque l’évaluateur lui attribue une note supérieure ou égale à 3, il est accepté, sinon il est rejeté. Procédure : Lancez les trois applications Dans l’application « CandidatApp », entrez « Kamel » dans le nom et « 01/05/1986 » dans la date de naissance Cliquez sur « Soumettre » Remarquez que « Kamel » a été automatiquement rejeté Créez deux candidatures pour « Yazid » et pour « Racha » nés respectivement le « 25/03/1971 » et « 26/08/1978 » Remarquez que les candidatures n’ont pas été automatiquement rejetées Allez sur l’application « EvaluateurApp » Dans la zone « Nom » entrez « Racha » et sélectionnez « 4 » dans la note puis cliquez sur « Evaluer » Revenez à l’application « CandidatApp », remarquez qu’une notification indiquant que « Racha » a été acceptée. La corrélation a fait qu’on trouve le bon « Workflow » malgré que « Racha » a un dossier ultérieur à celui de « Yazid » Revenez à l’application « EvaluateurApp » Dans la zone « Nom » entrez « Yazid », dans la zone « Note » entrez « 2 » puis cliquez sur le bouton « Evaluer » Revenez sur l’application « CandidatApp » Remarquez une notification stipulant que « Yazid » a été rejetée       Le code source complet des applications de ce tutoriel est accessible ici. Enjoy !

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 »   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 » 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 »   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 :   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émoirevar 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'applicationvar 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 Téléchargement Le code du tutoriel peut être obtenu ici. Enjoy !