Mostefai Mohammed Amine

Software and Cloud Architect

Amine
  • Contact
Previous Post
Next Post
Sep 06, 2015 .NET ProgrammingWorfklow Foundation

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 !

Workflowworkflow foundationTutorialService
Share This Post

Related posts

  • WF Cours 4–Services Avancés. Tutoriel 4.1 Persistance–Partie 1 L’objectif de ce tutoriel associé à la gestion des services de workflow, est d’utiliser le service d
  • 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 ...
  • Workflow Foundation Cours 4–Tutoriel 4.2 Suivi L’objectif de ce tutoriel est de mettre en place le suivi des workflows. Pour ce, nous utiliserons l...
Saving the comment

Cancel reply to comment

The captcha value you provided is incorrect.