Création un DialogWorker - PARTIE I - Les commandes

Publié le par Jérémy JANISZEWSKI

INTRODUCTION

Il arrive parfois que nous ayons besoin de traiter des informations qui soient longues (comme la copie de fichiers ou de téléchargement de données). Lorsque de telles choses arrivent, nous avons une boite de dialogue qui s'affiche avec une barre de progression. Ici, ce que nous allons proposer, c'est de créer ce genre de boîte mais au lieu de n'exécuter qu'une tâche à la fois, elle pourra en exécuter autant que nous le souhaitons.

CONCEPTION

Il existe 3 types de commandes pour la gestion d'une opération :

  • Les commandes séquentielles comme la création d'un fichier ZIP.
  • Les commandes parallèles comme la génération d'une interface.
  • Les commandes asynchrones comme l'envoie d'emails.

Cette première partie s'occupera de créer les classes correspondants à ces commandes.

CREATION DE LA CLASSE DE BASE

La classe de base pour la gestion des commandes sera abstraite. En premier lieu, nos commandes devront exposées deux évènements :

  • Un évènement pour savoir si la commande est terminée.
  • Un évènement pour rapporter la progression de la commande.
 /// <summary> /// Represents a command for the working dialog. /// </summary> abstract public class Command : IDisposable {  #region Events /// <summary> /// Event raised when the command has finished his job. /// </summary> public event EventHandler<CommandEventArgs> EndWorking; /// <summary> /// Event raised when the command report a progress. /// </summary> public event ReportProgressCommandHandler ReportProgressCommand;  #endregion Events 

Ensuite, il nous faudra diverses variables :

  • Un Boolean pour savoir si la commande s'est terminée avec succès.
  • Un ProgressBar se rafraichissant à chaque fois que la progression a changée.
  • Un TextBlock affichant le pourcentage de la tache actuellement en cours de traitement.
  • Un TextBlock affichant la tache actuellement en cours de traitement.
  • Un String permettant d'afficher un message.
  • Un String permettant d'afficher un message d'erreur.
  • Un Long permettant de connaître le nombre d'objets à traiter
  • Un Long permettant de connaitre le nombre d'objets déjà traités.
  • Un Int32 permettant de connaître le pourcentage actuelle de la progression.
  • Un Int32 permettant de connaître le dernier pourcentage de la progression. (Permettant ainsi de rapporter la progression que si celle-ci a changée)
  • Un Dispatcher permettant de rafraichir l'UI si le traitement se doit être long, ou si nous devons effectuer un traitement sur le thread principal.
  #region Fields /// <summary> /// Does the work successfully completed ? /// </summary> protected bool success; /// <summary> /// Represents the progress bar to animate for the current task. /// </summary> protected ProgressBar currentTaskProgressBar; /// <summary> /// Represents the current progress of the task. /// </summary> protected TextBlock currentTaskPercentageCompleted; /// <summary> /// Represents the name of the task. /// </summary> protected TextBlock currentTaskName; /// <summary> /// Message returned by the command. /// </summary> protected string message; /// <summary> /// Error message returned by the command. /// </summary> protected string errorMessage; /// <summary> /// Represents the current created object. /// </summary> protected long currentObject; /// <summary> /// Represents the total number of objects to create. /// </summary> protected long totalObjectToCreate; /// <summary> /// Represents the current percentage of the task. /// </summary> protected int currentTaskPercentage; /// <summary> /// Represents the last percentage of the task. /// </summary> protected int lastTaskPercentage; /// <summary> /// Dispatcher to use for refreshing the ui. /// </summary> protected Dispatcher timer;  #endregion Fields 

Au niveau de la propriété, nous devons lui exposer un membre Name qui permettra de récupérer le nom donnée à la commande.

  #region Property /// <summary> /// Gets the name of the command. /// </summary> public string Name { get; private set; }  #endregion Property 

Au niveau du constructeur, nous allons le déclarer comme étant protected et initialiser certains champs :

  #region Constructor /// <summary> /// Instantiates a new command. /// </summary> protected Command(string name) { ExceptionHelper.RaiseNullOrEmptyException(name); Name = name; success = true; currentObject = 0; currentTaskPercentage = 0; lastTaskPercentage = 0; totalObjectToCreate = 0; timer = Dispatcher.CurrentDispatcher; }  #endregion Constructor 

Maintenant, au niveau des méthodes, il va nous falloir :

  • une méthode permettant de lancer l'évènement EndWorking.
  • une méthode (surchargeable) permettant de définir "titre" de la tache à exécuter.
  • une méthode permettant de préparer l'UI.
  • une méthode pour exécuter la commande (abstraite).
  • une méthode (surchargeable) permettant de définir le mode de progression de la barre.
  • une méthode (surchargeable) permettant de libérer les ressources allouées à la commande.
  • des méthodes permettant d'obtenir ou définir les valeurs des propriétés.
  • une méthode (surchargeable) permettant de reporter la progression de la tache.
  • une méthode permettant de lancer l'évènement ReportProgressCommand.
   #region Methods /// <summary> /// Occurs when the command the command has finished his job. /// </summary> /// <param name="message">Message returned by the command.</param> /// <param name="errorMessage">Error message returned by the command.</param> /// <param name="success">Flag to know if the command has /// successfully done his job.</param> protected void OnEndWorking(string message, string errorMessage, bool success) { if (EndWorking != null) EndWorking(this, new CommandEventArgs(message, errorMessage, success)); } /// <summary> /// Sets the name of the task before the work starts doing his job. /// </summary> virtual protected string SetTaskNameBeforeDoingWork() { return string.Empty; } /// <summary> /// Prepare the ui. /// </summary> /// <param name="currentTaskProgressBar">Progress bar to update.</param> /// <param name="currentTaskPercentageCompleted">Text block for percentage representating.</param> /// <param name="currentTaskName">Text block for task's name representating.</param> internal void Prepare(ref ProgressBar currentTaskProgressBar, ref TextBlock currentTaskPercentageCompleted, ref TextBlock currentTaskName) { this.currentTaskProgressBar = currentTaskProgressBar; this.currentTaskPercentageCompleted = currentTaskPercentageCompleted; this.currentTaskName = currentTaskName; this.currentTaskProgressBar.Dispatcher.Invoke(new Action(() => { this.currentTaskProgressBar.IsIndeterminate = IsIndeterminate(); this.currentTaskProgressBar.Value = 0; }), null); this.currentTaskName.Dispatcher.Invoke(new Action(() => { this.currentTaskName.Text = SetTaskNameBeforeDoingWork(); }), null); } /// <summary> /// Starts the command. /// </summary> abstract public void Start(); /// <summary> /// Gets if the progress is in indeterminate mode. /// </summary> /// <returns>Returns true if indeterminate, false otherwise.</returns> virtual protected bool IsIndeterminate() { return false; } /// <summary> /// Releases all resources used by the command. /// </summary> virtual public void Dispose() { } /// <summary> /// Gets the value of the specified property. /// This property should not be indexed. /// </summary> /// <param name="property">Property to get the value</param> /// <returns>The value of the object.</returns> internal object GetValue(PropertyInfo property) { return property.GetValue(this, null); } /// <summary> /// Gets the value of the property. /// This property should not be indexed. /// </summary> /// <param name="propertyName">Name of the property to get /// the value.</param> /// <returns>The value of the property.</returns> internal object GetValue(string propertyName) { PropertyInfo p = GetType().GetProperty(propertyName); return p.GetValue(this, null); } /// <summary> /// Sets the value of the specified property. /// This property should not be indexed. /// </summary> /// <param name="info">Property to set.</param> /// <param name="value">Value of the property.</param> internal void SetValue(PropertyInfo info, object value) { info.SetValue(this, value, null); } /// <summary> /// Sets the value of the specified property. /// This property should not be indexed. /// </summary> /// <param name="propertyName">Name of the property /// to set the value.</param> /// <param name="value">The value of the property.</param> internal void SetValue(string propertyName, object value) { GetType().GetProperty(propertyName).SetValue(this, value, null); } /// <summary> /// Reports the progression of the task. /// </summary> virtual protected void ReportProgress() { } /// <summary> /// Occurs when a report needs to be performed.  /// </summary> /// <param name="progress">Progression of the command.</param> protected void OnReportProgressCommand(int progress) { if (ReportProgressCommand != null) ReportProgressCommand(this, progress); }  #endregion Methods } 

  Nous avons une bonne base pour la création de commandes séquentielles, parallèles et asynchrone. 

CREATION DE LA CLASSE DE COMMANDE SEQUENTIELLE

  La commande séquentielle n'est pas multithread. Ainsi, prévoyez de l'utiliser si votre tache n'est pas multithread ou asynchrone. Nous pourrions utiliser cette commande, pour, par exemple, compresser des fichiers ou écrire des données volumineuses dans un fichier.

Cette commande repose sur l'utilisation d'un background worker. Le début de la classe est donc ainsi :

 /// <summary> /// <para> /// Represents a sequential command. /// A background worker do the job, but don't use this class /// if you plan to generate UI. /// If you want to generate UI and more complex objects, use ParallelCommand. /// This class is marked as abstract. /// </para> /// </summary> abstract public class SequentialCommand : Command { #region Fields /// <summary> /// Represents a synchronus operation. /// </summary> protected BackgroundWorker worker;  #endregion Fields  #region Constructor /// <summary> /// Instantiates a new command. /// </summary> protected SequentialCommand(string name) : base(name) { worker = new BackgroundWorker(); worker.DoWork += DoWork; worker.WorkerSupportsCancellation = false; worker.ProgressChanged += ProgressChanged; worker.RunWorkerCompleted += WorkCompleted; worker.WorkerReportsProgress = true; }  #endregion Constructor 

La méthode DoWork sera marquée comme virtual et protected.

  #region Methods /// <summary> /// Occurs when the command does his job. /// </summary> /// <param name="sender">Object who invokes this method.</param> /// <param name="e">Event associated to this method.</param> virtual protected void DoWork(object sender, DoWorkEventArgs e) { } 

La méthode WorkCompleted sera elle aussi marquée comme virtual et protected car nous pourrions modifier son comportement. De base, cette méthode va appeler la méthode OnEndWorking.

 /// <summary> /// Occurs when the command has finished his job. /// </summary> /// <param name="sender">Object which invokes this method.</param> /// <param name="e">Event associated to this method.</param> virtual protected void WorkCompleted(object sender, RunWorkerCompletedEventArgs e) { OnEndWorking(message, errorMessage, success); } 

La méthode ProgressChanged sera marquée comme private, car il n'y aura pas lieu de la changer. Cette méthode va mettre à jour une portion de l'UI.

 /// <summary> /// Occurs when the progression has changed. /// </summary> /// <param name="sender">Object which invokes this method.</param> /// <param name="e">Event associated to this method.</param> private void ProgressChanged(object sender, ProgressChangedEventArgs e) { currentTaskPercentageCompleted.Dispatcher.Invoke(new Action(() => { currentTaskPercentageCompleted.Text = e.ProgressPercentage.ToString(); }), null); currentTaskProgressBar.Dispatcher.Invoke(new Action(() => { currentTaskProgressBar.Value = e.ProgressPercentage; }), null); } 

Maintenant, nous allons surcharger notre méthode de progression. Nous allons aussi la marquer comme sealed. Ici, nous devrons tenir compte se savoir si la barre de progression est en mode indéterminée ou non. Si elle est effectivement en mode indéterminée, lors de l'appel à cette méthode, nous devons lui spécifier que 100% de la tache a été exécutée. Sinon, nous utilisons le rapport nombre_d_objets_a_traiter / nombre_d_objets_total * 100 pour connaitre le pourcentage de progression.

 /// <summary> /// Report the progress on the progress bar. /// </summary> sealed override protected void ReportProgress() { if (!IsIndeterminate()) currentTaskPercentage = (int)(((float)currentObject / (float)totalObjectToCreate) * 100f); else currentTaskPercentage = 100; if (lastTaskPercentage != currentTaskPercentage) { lastTaskPercentage = currentTaskPercentage; worker.ReportProgress(currentTaskPercentage); } OnReportProgressCommand(currentTaskPercentage); } 

La méthode Start sera qualifiée comme sealed. Elle mettra en route le background worker, et la méthode Dispose disposera du worker.

  /// <summary> /// Starts the job. /// </summary> sealed override public void Start() { worker.RunWorkerAsync(); } /// <summary> /// Dispose the command. /// </summary> override public void Dispose() { worker.Dispose(); worker = null; }  #endregion Methods } 

CREATION DE LA CLASSE DE COMMANDE PARALLELE

Une commande parallèle est utile pour exécuter de gros traitement de données. Nous pourrions imaginer ce scénario (d'ailleurs utilisé pour la version 2 de Nétiquette)  :

  • Générer une interface graphique

En effet, si nous avons 15 éléments à générer et que dans ces 15 éléments, nous ayons à générer X autres éléments, il peut être très utile de séparer la génération de l'UI en dédiant sur plusieurs threads spécifiques les 15 éléments.

En cela, une commande parallele est constitué d'objets Task,et d'un objet StringBuilder pour permettre de récupérer toutes les erreurs contenues dans chacune des tâches. 

Le début de la classe est donc définie ainsi :

 /// <summary> /// Represents a parallel command. /// If you use an asynchronous method, /// use AsynchronousCommand. /// This class is abstract. /// </summary> abstract public class ParallelCommand : Command {  #region Fields /// <summary> /// Represents a list of task to execute. /// </summary> protected List<Task> tasks; /// <summary> /// Contains the error occured on each task. /// </summary> private StringBuilder builder;  #endregion Fields 

Ensuite, le constructeur va instancier la liste des tâches et le string builder.

  #region Constructor /// <summary> /// Instantiates a new parallel command. /// </summary> /// <param name="name">Name of the command.</param> protected ParallelCommand(string name) : base(name) { tasks = new List<Task>(); builder = new StringBuilder(); }  #endregion Constructor 

Ensuite, au niveau des méthodes, nous allons sceller la méthode Start et celle-ci permettra de créer les tâches à exécuter et d'attendre que toutes les tâches se soient termninées (avec succès ou non) pour continuer l'exécution du programme.

  #region Methods /// <summary> /// Execute all tasks. /// </summary> sealed override public void Start() { CreateTasks(); WaitTasksCompleted(); } 

La méthode CreateTasks sera marquée comme virtual et protected, alors que la méthode WaitTasksCompleted sera marquée comme private.

 /// <summary> /// Prepare all tasks needed before doing work. /// </summary> virtual protected void CreateTasks() { } /// <summary> /// Occurs when all tasks has completed. /// </summary> private void WaitTasksCompleted() { if (tasks.Count == 0) { OnStop(null); return; } Task.Factory.ContinueWhenAll(tasks.ToArray(), new Action<Task[]>(OnStop)); } 

Il nous faudra aussi une méthode permettant d'ajouter nos tâches. En effet, il ne faut pas oublier qu'en l'état que la liste des tâches ne sert pas à grand chose si nous ne pouvons lui ajouter une tâche.

Une telle méthode se définie ainsi :

  /// <summary> /// Add a task into the command. /// </summary> /// <param name="action">Action to execute for the task.</param> protected void AddTask(Action action) { tasks.Add(Task.Factory.StartNew(action, TaskCreationOptions.AttachedToParent)); } 

Ici, nous utilisons la factory intégré à la classe Task et nous démarrons un nouveau thread pour la tâche à exécuter et nous demandons que la tâche soit associée à un parent dans la hiérarchie des tâches.

Maintenant la méthode OnStop va créer le message d'erreur (si besoin est) et envoyer l'instruction OnEndWorking.

 /// <summary> /// Create the error message and send the EndWorking command. /// </summary> /// <param name="tasks">Array of tasks.</param> private void OnStop(Task[] tasks) { if (tasks == null) message = GetSuccessMessage(); else { builder.Clear(); foreach (Task task in tasks) { if (task.Exception != null) { builder.AppendFormat("Exception\n{0}\n{1}\n", task.Exception.Message, task.Exception.InnerException); } errorMessage = builder.ToString(); } if (!string.IsNullOrEmpty(errorMessage)) { message = GetFailedMessage(); success = false; } else message = GetSuccessMessage(); } timer.Invoke(new Action(() => { OnEndWorking(message, errorMessage, success); })); } 

Il convient ici de noter que si message d'erreurs il y'a dans l'un ou l'autre tâche, il y'aura forcément echec de la commande parallèle, d'où la mise à false de la variable success.

Ensuite, il nous faut créer nos méthodes pour afficher un message (de succès ou d'échec). Ces méthodes seront qualifiées comme virtual et protected.

 /// <summary> /// Gets the failed message. /// </summary> /// <returns>Message to set when one tasks has failed.</returns> virtual protected string GetFailedMessage() { return string.Empty; } /// <summary> /// Gets the success message. /// </summary> /// <returns>Message to set when all tasks have successfully completed.</returns> virtual protected string GetSuccessMessage() { return string.Empty; } 

  Enfin il faut surcharger nos méthodes de rapport de progression et de libération de nos objets. La méthode de rapport de progression est similaire à celle de la classe SequentialCommand, à ceci près que nous n'utilisons pas le background worker.

Quand à la méthode Dispose, nous ne faisons que disposer toutes les tâches présentes dans la liste des tâches.

 /// <summary> /// Reports the progression of the command. /// </summary> sealed protected override void ReportProgress() { if (!IsIndeterminate()) currentTaskPercentage = (int)(((float)currentObject / (float)totalObjectToCreate) * 100f); else currentTaskPercentage = 100; if (currentTaskPercentage.IsInRange(1, 100)) { if (lastTaskPercentage != currentTaskPercentage) { lastTaskPercentage = currentTaskPercentage; timer.Invoke(new Action(() => { currentTaskProgressBar.Value = currentTaskPercentage; currentTaskPercentageCompleted.Text = currentTaskPercentage.ToString(); })); OnReportProgressCommand(currentTaskPercentage); } } } /// <summary> /// Dispose all tasks. /// </summary> override public void Dispose() { if (tasks != null) { for (int i = 0; i < tasks.Count; ++i) { tasks[i].Dispose(); tasks[i] = null; } tasks.Clear(); tasks = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }  #endregion Methods } 

La classe de gestion de commandes parallèle est terminée. Il ne nous reste plus qu'à voir la classe de commande asynchrone.

 

CREATION DE LA CLASSE DE COMMANDE ASYNCHRONE

La classe de commande asynchrone peut être utilisée pour toute classe utilisant des instructions asynchrones (comme la classe SmtpClient).  

Au niveau des champs, il lui suffira seulement d'un booléen pour savoir si la tâche s'est terminée.

 /// <summary> ///Represents the management of an asynchronous command. /// </summary> abstract public class AsynchronousCommand : Command {  #region Fields /// <summary> /// Flag to know if the asynchronous operation is completed. /// </summary> protected bool asyncCompleted;  #endregion Fields 

Le constructeur est donc extrêmement simple :

  #region Constructor /// <summary> /// Instantiates the asynchronous command. /// </summary> /// <param name="name">Name of the command.</param> protected AsynchronousCommand(string name) : base(name) { }  #endregion Constructor 

Maintenant au niveau des méthodes, il nous faudra sceller la méthode Start. Cette méthode va appeler une méthode DoWork, qui sera marquée comme virtual et protected. Notre méthode Start appelera après celle-là, une méthode Wait qui attendra la terminaison de l'opération asynchrone.

La méthode DoWork contiendra le code necessaire à l'exécution de la méthode asynchrone et la méthode Wait "bloquera" l'application tant que l'opération asynchrone n'est pas terminée.

 /// <summary> /// Performs the job. /// </summary> virtual protected void DoWork() { } /// <summary> /// Wait the completion of the job. /// </summary> private void Wait() { while (!asyncCompleted) ApplicationHelper.DoEvents(); OnEndWorking(message, errorMessage, success); } 

Ensuite il nous faudra une méthode qu'il faudra (dans la plupart des cas) considérer comme la méthode à appeler à droite de l'évènement asynchrone. Cette méthode sera marquée comme virtual et protected. Ensuite nous surchargerons notre méthode de rapport de progression.

  /// <summary> /// Occurs when the asynchronous command is completed. Normally used /// as an anonymous method for an AsynchronousCompleted event. /// </summary> /// <param name="sender">Object which invokes this method.</param> /// <param name="e">Event associated to this method.</param> virtual protected void OnTaskCompleted( object sender, AsyncCompletedEventArgs e) { } /// <summary> /// Report the progression of the command. /// </summary> sealed override protected void ReportProgress() { if (!IsIndeterminate()) currentTaskPercentage = (int)(((float)currentObject / (float)totalObjectToCreate) * 100f); else currentTaskPercentage = 100; if (currentTaskPercentage.IsInRange(1, 100)) { if (lastTaskPercentage != currentTaskPercentage) { lastTaskPercentage = currentTaskPercentage; timer.Invoke(new Action(() => { currentTaskProgressBar.Value = currentTaskPercentage; currentTaskPercentageCompleted.Text = currentTaskPercentage.ToString(); })); OnReportProgressCommand(currentTaskPercentage); } } }  #endregion Methods } 

 HELPERS ETC...

 Durant la rédaction de cette première partie de l'article, nous avons utilisés quelques classes ou routines inconnues. La première d'entre elles est l'évènement ReportProgressCommand. Son délégué est définie ainsi :

 public delegate void ReportProgressCommandHandler(object sender,int progress); 

La suivante est la classe CommandEventArgs. Cette classe ne fait qu'exposer (en lecture seule) les variables errormessage, message et success.

 sealed public class CommandEventArgs : EventArgs {  #region Properties /// <summary> /// Gets the message. /// </summary> public string Message { get; private set; } /// <summary> /// Gets the error message. /// </summary> public string ErrorMessage { get; private set; } /// <summary> /// Gets if the command has successfully completed his job. /// </summary> public bool Success { get; private set; }  #endregion Properties #region Constructor /// <summary> /// Instantiates a new event args for the command. /// </summary> /// <param name="message">Message returned by the command.</param> /// <param name="errorMessage">Error message returned by the command.</param> /// <param name="success">Flag returned by the command.</param> public CommandEventArgs(string message, string errorMessage, bool success) { this.Message = message; this.ErrorMessage = errorMessage; this.Success = success; }  #endregion Constructor } 

Quant à la méthode DoEvents, je l'ai reprise d'un site qui proposé sa vision du DoEvents cher aux développeurs Winforms.

 static public void DoEvents() { Application.Current.Dispatcher.Invoke( DispatcherPriority.Background, new ThreadStart(delegate { })); } 

CONCLUSION

Cette première partie est terminée. Nous avons créer une classe de base Command qui peut être enrichie en la faisant hériter.

3 types de commandes sont ainsi nées : 

  • Une commande séquentielle
  • Une commande parallèle
  • Une commande asynchrone

La deuxième partie de cette article permettra de lier diverses commandes entre elles. Parce que quoi qu'il arrive, même si les commandes seront exécutées une par une par le DialogWorker, il se pourrait que nous ayons besoin de transférer les données crées par une commande A et les envoyées dans une commande B.

Bien sûr les commandes iront toujours en descendant, il ne sera pas faisable d'exécuter les commandes A, B, C, D, E et de demander à E d'envoyer ses résultats à A.

 

Stay tuned,  

@ bientôt sur ce blog

Publié dans WPF

Pour être informé des derniers articles, inscrivez vous :
Commenter cet article