Accueil > .Net, C#, WinForm > [C# WinForm] BackgroundWorker, ProgressBar

[C# WinForm] BackgroundWorker, ProgressBar

Depuis hier, j’avais besoin de prendre un peu de recul avec le code.
De faire autre chose.
Donc, j’ai fais du WinForm. Normal, ça faisait longtemps😀

Au programme : traitements longs, BackgroundWorker, ProgressBar et parallélisme. Ah non, ça, je n’ai pas le droit (Framework 3.5 oblige…).
(Et oui, je sais, ce n’est pas neuf, mais ça fait pas de mal d’en reparler ^^)

Ce (long) billet sera organisé par étape pour « formaliser » le sujet, avec un peu plus de rigueur que d’habitude (ça ne peut pas faire de mal).
Donc, on retrouvera l’expression de besoin, la réflexion et la réalisation.
Je ne le faisais pas pour le moment, mais je pense que ça pourrait (m’)être utile.

Donc, action😉

 
Expression du besoin :
Je veux pouvoir renommer en masse les fichiers présents dans un répertoire spécifique.
Le nom doit être composé comme il suit : <pattern><0000>.<ext> Avec :

  • <pattern> : un motif définit par l’utilisateur.
  • <0000> : un nombre incrémenté, mais en conservant l’ordre des fichiers (donc s’il y a plus de 10 fichiers : 01, 02…; plus de 100 : 001, 002; ainsi de suite)..
  • <ext> : l’extension actuelle du fichier (.jpg, .mp3, .docx…).

Je veux que l’utilisateur ne puisse pas lancer à plusieurs reprises le même traitement.
Je veux également que l’application ne soit pas gelée et que l’utilisateur puisse réaliser d’autres actions possibles.
Je veux également que l’utilisateur soit notifié en temps réel de l’avancement des traitements.

 
Réflexion
C’est du renommage en masse, rien de bien compliqué.

Il y a un motif de base, l’ajout d’un nombre incrémentiel et l’extension. Donc, du fichier d’origine, on ne doit conserver que l’extension (qui n’est donc pas filtrée).

L’ordre des fichiers doit être conservé : par défaut par ordre alphabétique (dans l’explorateur Windows). Ce qui veut dire que l’on doit traiter les fichiers de façon séquentielle (boucle For basique) ou alors boucler deux fois pour numéroter puis traiter (requête Linq + parallélisme). Dans l’immédiat, je vais traiter de façon séquentielle (puisque .Net 3.5 en ce moment). Pour le parallélisme, il n’est pas complexe à utiliser et j’ai déjà fais des billets dessus (chercher « parallélisme » dans le moteur en haut à droite du site).

Impossibilité de lancer plusieurs actions en même temps, cela implique de bloquer le bouton qui la lance.

L’application ne doit pas être gelée, donc utilisation du multithreading. C’est donc ici qu’entre en jeu le BackgroundWorker.
L’application doit notifier l’avancement du traitement : on a le choix en la ProgressBar et un message d’information. J’ai pris les deux🙂

Ce n’est pas un « projet », comprendre que c’est une application assez basique.
Donc, je n’ai pas jugé utile de sortir l’artillerie lourde avec des design pattern dans tous les sens, une architecture complexe…
Ceci dit, je n’aime pas le copy/paste (enfin, pas quand je code).
Donc en pré-requis : aucune duplication (DRY), un code simpliste (KISS sur Wikipédia ou KISS sur YouTube, c’est selon !).

Application WinForm basique : un seul csproj, mais avec des répertoires/namespaces pour séparer un peu les responsabilités (entités, UI, langues, fonctionnalités).
Je l’ai dis, l’application sera basique, mais je vais m’en servir pour d’autres petites choses (renommage de fichiers, conversion de format d’images, redimensionnement d’images et recherche de doublons d’images).
Donc, je vais faire une application avec une seule Form principale dotée d’un TabControl dans lequel je vais ajouter un onglet par fonctionnalités qui seront, elles, dans des UserControls.
Après, j’aurais également une petite Form pour la configuration de l’application (répertoire par défaut, choix de langue…).

 
Réalisation
C’est maintenant que l’on va coder, après d’avoir fais les choix ci-dessus.

La première étape consiste à créer une Form principale (MainForm) dotée d’un TabControl et de plusieurs onglets (ce qui permettra également de bien voir que l’application n’est pas figée).
Interface Basique
Note : il est vrai, je ne suis pas ergonome. Mais j’essaie de faire des efforts🙂
 
Ensuite, création d’un UserControl comme ceci :
UserControl pour renommer en masse

Dans le GroupBox : 2 Labels, 2 TextBox (d’on une ReadOnly) et un Button permettant d’ouvrir un FolderBrowserDialog.
En dessous, un Button pour lancer l’action, une ProgessBar et un Label pour le statut.

Le code à proprement parlé, maintenant.
Déjà, les petites choses qui aident :

// Evènement pour le click sur le bouton [...]
protected void UcRenameBrowseClick(object sender, EventArgs e)
{
	UcRenameFolderDialog.ShowNewFolderButton = false;
	UcRenameFolderDialog.RootFolder = System.Environment.SpecialFolder.Desktop;
	if(UcRenameFolderDialog.ShowDialog(this) == DialogResult.OK){
		TboxDirectory.Text = UcRenameFolderDialog.SelectedPath;
	}
}
// Méthode permettant de changer le message du statut
protected void ChangeStatusMessage(String message){
	UcRenameWorkStatus.Text = message;
	UcRenameWorkStatus.Refresh();
}
// Méthode permettant de changer le style et le nombre maximum de la ProgressBar (= le nombre de fichiers à traiter)
protected void ChangeMaxFile(Int32 number){
	UcRenameProgress.Maximum = number;
	UcRenameProgress.Value = 0;
	UcRenameProgress.Refresh();
	UcRenameProgress.Style = ProgressBarStyle.Continuous;
}

Jusqu’ici, rien de bien compliqué.

Voyons un peu comment on utilise le BackgroundWoker, maintenant, lors du click sur le bouton.

protected void UcRenameLaunchClick(object sender, EventArgs e)
{
	// On remet à zéro le statut
	ChangeStatusMessage(String.Empty);
	// On créé un nouveau BackgroundWorker
	BackgroundWorker worker = new BackgroundWorker();
	// On lui attache l'évènement principal qui exécute la tâche
	worker.DoWork+= new DoWorkEventHandler(worker_DoWork);
	// On lui attache l'évènement qui se déclenche à la fin de la tâche
	worker.RunWorkerCompleted+= new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
	// Et on lance l'exécution de la tâche (évènement worker_DoWork)
	worker.RunWorkerAsync();
}

C’est réellement aisé, non ?

L’évènement worker_DoWork, maintenant :

protected void worker_DoWork(object sender, DoWorkEventArgs e)
{
	// Le nombre de fichiers au total
	Int64 fileNumber = 0;
	// On empêche le bouton de pouvoir être utilisé à nouveau
	BaseForm.LaunchDispatcher(() => UcRenameLaunch.Enabled = false);
	// On change le message du statut (Recovering in progess...)
	BaseForm.LaunchDispatcher(() => {
		ChangeStatusMessage(Localisation.Local.UcRenameRecoverFiles);
	});
	// On lance le renommage
	fileNumber = WorkManager.MassRename(TboxDirectory.Text,
	                       TboxNamePattern.Text,
	                       (index) => 
	                       		BaseForm.LaunchDispatcher(() =>  {
									// On incrémente la ProgressBar de 1 fichier
									UcRenameProgress.Increment(1);
									// On affiche le message de statut
									ChangeStatusMessage(
										String.Format(Localisation.Local.UcRenameInProgess, index)); //{0} files renamed.
								}), 
	                       (number) => ChangeMaxFile(number));
	// On change à nouveau le statut
	BaseForm.LaunchDispatcher(() =>  {
		ChangeStatusMessage(String.Format(Localisation.Local.UcRenameFinalMessage, fileNumber));
	});
}

De suite, c’est un peu plus complexe, notamment avec les choix que j’ai fais.

La première chose notable est le BaseForm.LaunchDispatcher().
Le Dispatcher permet de mettre à jour l’IHM (qui est sur le Thread principal) à partir d’un Thread différent (ici, celui du BackgroundWorker) sans pour autant avoir une exception de type : « The calling thread cannot access this object because a different thread owns it. »

Cette méthode est utilisée à plusieurs reprises et sera utilisée dans d’autres composants, donc je l’ai placée au plus haut niveau. Elle est également static car elle ne requiert rien d’autres que les paramètres passés.
Il faut bien comprendre une chose : le Dispatcher prend un Action (pas une Action<T>, donc du void MyMethod()) donc aucun paramètre en entré ni aucun retour.
Cependant, j’ai besoin de passer des paramètres aux méthodes qui vont utiliser le Dispatcher.
Et comme je ne veux pas créer 3000 méthodes pour ça, j’ai le choix entre delegate ou lambda. Autant dire que les lambda, j’en use (et parfois abuse).

Et le code du Dispatcher ?

public static void LaunchDispatcher(Action action){
	Dispatcher.CurrentDispatcher
		.Invoke(DispatcherPriority.Normal,
	    		new MethodInvoker(action));
}

Et oui, pas bien compliqué !

Mais le plus compliqué à comprendre, c’est sans doute la méthode MassRename (quoique…).
J’ai fais le choix de faire une seule et unique itération sur mes fichiers (boucle For), donc je ne sais pas à l’avance combien il y a de fichiers pour le Maximum de la ProgressBar. De même, pour notifier (ProgressBar + statut) à quel fichier j’en suis, il me faut l’index (de la boucle For).

Voici la méthode en question :

public static Int64 MassRename(
							String directoryPath, 
							String pattern, 
							Action<Int32> updateMethod, 
							Action<Int32> updateProgress){
	// J'utilise une variable que j'incrémente car certains fichiers sont exclus
	// Donc le files.Length (plus loin) peut être différent de number
	Int64 number = 0;
	// Vérification de l'existence du répertoire
	if(Directory.Exists(directoryPath)){
		// Récupération des fichiers (tous, il peut être utile de filtrer)
		String[] files = Directory.GetFiles(directoryPath);
		// On trie les fichiers par nom
		Array.Sort(files);
		// Si la méthode est non null, on l'utilise
		if(updateProgress != null)
			updateProgress(files.Length);
		// files.Length : 2004 dans mon cas
		// Donc files.Length.ToString().Length = 4
		// Donc, on format avec 4 zéros 0001, 0002...2004
		// On créé ce formattage une seule et unique fois
		String format = String.Concat("{0:", new String('0', files.Length.ToString().Length), "}");
		// Boucle For en méthode d'extension
		files.For((s, index) => {
		    // Création de l'entité fichier (non pertinent à développer ici)
			FileEntity file = Create(s, index);
			// null = fichier caché, .lnk...
			if(file != null){
				String name = String.Concat(
					String.Format(pattern, 
					              String.Format(format, file.ImageNumber)),
					file.Extension);
				File.Move(file.FullPath, Path.Combine(file.Path, name));
				number++;
				// On utilise la méthode d'update si elle est non null
				if(updateMethod != null)
					updateMethod(index);
			}
		});
	}
	return number;
}

Au final, on obtient donc une application qui ne se fige pas, mais qui ne permet pas non plus de lancer deux fois la même commande.
Elle permet également de notifier (de façon basique) l’état d’avancement.
Donc, mission accomplie !🙂
Application Finale

Bon, maintenant, je vais faire les autres onglets ^^

Catégories :.Net, C#, WinForm
  1. Aucun commentaire pour l’instant.
  1. 02/01/2014 à 10:01
  2. 02/01/2015 à 20:01

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :