Blog

Des Tasks à partir d’événements

17 Aug 2016

Quelques-fois vous pouvez être confronté à des API basées sur des callbacks pour l’exécution de tâches asynchrones, et cela ne correspond par vraiment à vos habitudes de programmation moderne basée sur l’asynchronisme TCL (avec des Tasks).

Rappel sur l’objet Task

Commençons par un par un rapide rappel sur l’objet Task qui représente un état d’exécution asynchrone dans les environnements modernes .NET.

Une tâche peut avoir les états suivants :

Tâches manuelles

Grâce au type TaskCompletionSource<T>, la création de tâches déclenchées manuellement s’avère simple, et son état de sortie peut être affecté.

Vous devez tout d’avord créer une instance de source, cette dernière possédant une propriété Task qui peut être attendue :

var source = new TaskCompletionSource<string>();
var task = source.Task;

// ...

await task;

Ensuite, il ne reste plus qu’à mettre à jour le status de la source en conséquence :

// Success
source.SetResult("Success!);

// Cancelled
source.SetCancelled();

// Failed
source.SetException(new Exception("Failed"));

Exemple concret : Xamarin.Android Intents

Cette technique est régulièrement utilisée pour migrer des APIs basées sur des événements en Tasks, mais j’ai récemment utilisé ce méchanisme pour attendre le resultat d’Intents dans une application Xamarin.Android.

Si vous n’êtes pas familier avec les Intents, commencez par lire la documentation Android. Pour simplifier, c’est un message qui peut être propagé au niveau système, et certaines entitées (activities, services, system) peuvent s’inscrire et réagir à ce type de messages.

Voici un exemple de selectionneur de fichier image depuis une Activity Xamarin.Android :

private TaskCompletionSource<Android.Net.Uri> source;

public Task<Android.Net.Uri> PickImageAsync()
{
	if (this.source != null && !this.source.Task.IsFaulted && !this.source.Task.IsCanceled)
		return this.source.Task;

	this.source = new TaskCompletionSource<Android.Net.Uri>();
	var task = this.source.Task;

	try
	{
		var imageIntent = new Intent();
		imageIntent.SetType("image/*");
		imageIntent.SetAction(Intent.ActionGetContent);
		var i = Intent.CreateChooser(imageIntent, "Select a photo");
		StartActivityForResult(i, 0);
	}
	catch (Exception e)
	{
		this.source.SetException(e);
		this.source = null;
	}

	return task;
}

protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
	base.OnActivityResult(requestCode, resultCode, data);

	switch (requestCode)
	{
		case 0:
			if (resultCode == Result.Ok && data?.Data != null)
			{
				this.source.SetResult(data.Data);
			}
			else if (resultCode == Result.Canceled)
			{
				this.source.SetCanceled();
			}
			else
			{
				this.source.SetException(new Exception("Failed to get a file"));
			}

			this.source = null;

			break;
	}
}

La méthode peut ensuite être appellée et attendue simplement :

var file = await this.PickImageAsync();

Ce projet d’exemple est disponible sur GitHub.

Evidemment, vous pouvez également appliquer cette technique avec un BroadcastReceiver et tout cela peut être amélioré en ajoutant l’annulation au travers d’une vérification du CancellationToken, mais la base restera celle présentée.

Je trouve ça agréable d’être capable de respecter les paradigmes propres au C#, même avec des APIs non adaptées à cet usage.