[WP7Dev] Utiliser le WebClient avec les Reactive Extensions pour Télécharger en Asynchrone

This article is available in english.

Il y a un framework très intéressant qui s'est glissé dans le SDK pour Windows Phone 7 : Les Reactive Extensions.

C'est en fait un framework assez mal compris, principalement parce qu'il n'est pas simple à maitriser, mais que lorsque vous avez pris la main dessus, il est très très pratique ! J'apprécie particulièrement l'extension MemoizeAll, qui est très utile.

Mais je m'égare.


Un Téléchargement de Chaine de caractère Non-Réactif

Sur le Windows Phone 7, la class WebClient n'a qu'une méthode DownloadStringAsync et a l'évènement DownloadStringCompleted correspondant. Cela veut dire que l'on est forcé d'être asynchrone, pour être cordial avec l'interface graphique et ne pas geler l'application pour l'utilisateur, à cause d'une mauvaise pratique de programmation en étant synchrone sur les appels distants.

Dans un monde sans Reactive Extensions, on aurait quelque chose comme ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void StartDownload()
{
var wc = new WebClient();
wc.DownloadStringCompleted +=
(e, args) => DownloadCompleted(args.Result);

// Démarrer le téléchargement
wc.DownloadStringAsync(new Uri("http://www.data.com/service"));
}

public void DownloadCompleted(string value)
{
myLabel.Text = value;
}

Très simple. Mais rapidement vous allez vous rendre compte que l'exécution de l'évènement DownloadStringCompleted est effectué sur la Thread de l'interface graphique. Cela veut dire que, si pour quelque raison vous avez besoin de faire un long calcul suite à la réception de la chaine, vous allez geler l'interface utilisateur pour la durée du calcul. Et comme Windows Phone 7 est fait pour la fluidité et que vous ne voulez pas être le mauvais élève, vous allez vouloir mettre le calcul dans la queue du ThreadPool.

Mais vous allez aussi avoir à mettre à jour l'interface graphique dans le Dispatcher, donc vous avez besoin de revenir du ThreadPool.

On aura alors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 public void StartDownload()
{
WebClient wc = new WebClient();
wc.DownloadStringCompleted +=
(e, args) => ThreadPool.QueueUserWorkItem(d => DownloadCompleted(args.Result));

// Démarrer le téléchargement
wc.DownloadStringAsync(new Uri("http://www.data.com/service"));
}

public void DownloadCompleted(string value)
{
// Quelques calculs très longs
Thread.Sleep(1000);

Dispatcher.BeginInvoke(() => myLabel.Text = value);
}

C'est un peu plus complexe. Et on remarquera ensuite qu'il faut gérer les exceptions parceque, en fait, c'est le Web. C'est finalement peu fiable.

Donc, ajoutons la gestion des exceptions :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void StartDownload()
{
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (e, args) => {
try {
ThreadPool.QueueUserWorkItem(d => DownloadCompleted(args.Result));
}
catch (WebException e) {
myLabel.Text = "Error !";
}
};

// Démarrer le téléchargement
wc.DownloadStringAsync(new Uri("http://www.data.com/service"));
}

public void DownloadCompleted(string value)
{
// Quelques calculs très longs...
Thread.Sleep(1000);
Dispatcher.BeginInvoke(() => myLabel.Text = value);
}

Cela commence à devenir relativement complexe. Mais maintenant, vous devez attendre le résultat d'un autre appel de WebClient et afficher les deux résultats.

Aïe. Ok, je vais vous épargner celui la.


Le Même Exemple avec les Reactive Extensions

Les Reactive Extensions traitent les évènements asynchrones comme un flux d'évènements. On souscrit à ce flux et on s'en va, et on laisse le Reactive Framework faire la besogne.

Je vais vous épargner les explications sur la dualité entre IObservable et IEnumerable, parce que Erik Meijer l'explique particulierement bien.

Donc, je recommence avec l'exemple simple, et après avoir ajouté les références vers System.Observable et System.Reactive, on peut télécharger une chaîne de caractères :

1
2
3
4
5
6
7
8
9
10
11
12
public void StartDownload()
{
WebClient wc = new WebClient();

var o = Observable.FromEvent<DownloadStringCompletedEventArgs>(wc, "DownloadStringCompleted")

// Quand l'évènement est levé, on sélectionne la chaine et
// on en fait un IObservable<string> à la place
.Select(newString => newString.EventArgs.Result);

// Souscription à l'observable, et on assigne le texte du label
o.Subscribe(s => myLabel.Text = s);


// Démarrage du téléchargement
wc.DownloadStringAsync(new Uri("http://www.data.com/service"));
}

Cet exemple fait la même chose que le premier exemple. Vous remarquerez l'utilisation de Observable.FromEvent qui permet de transformer un évènement en observable d'évènements. Dans cet exemple, le flux d'évènements ne va en contenir en fait qu'un seul, puisque la fin du téléchargement ne s'effectue qu'une seule fois. Chacune de ces occurrences de l'évènement est alors projetée en utilisant Select, vers une chaîne de caractères qui représente le résultat de la requête web.

Cet exemple simple est un peu plus complexe à cause de la plomberie.

Mais maintenant, on veut supporter les changement de contexte de Threads. Les Reactive Extensions supportent le concept de Scheduler, pour observer un IObserable dans un contexte spécifique.

Don, on peut utiliser un Scheduler comme ceci.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public void StartDownload()
{
WebClient wc = new WebClient();

var o = Observable.FromEvent<DownloadStringCompletedEventArgs>(wc, "DownloadStringCompleted")

// On s'assure que l'on est sur le ThreadPool
.ObserveOn(Scheduler.ThreadPool)

// Lorsque l'evenement est levé, on selectionne la chaîne
.Select(newString => ProcessString(newString.EventArgs.Result))

// Maintenant on retourne sur la Thread graphique
.ObserveOn(Scheduler.Dispatcher)

// On souscrit à l'observable et on assigne le text au label
.Subscribe(s => myLabel.Text = s);

wc.DownloadStringAsync(new Uri("http://www.data.com/service"));
}

public string ProcessString(string s)
{
// Un très très long calcul...
return s + "1";
}

Dans cet exemple, on a changé de contexte deux fois pour nos besoins, et maintenant, le code est moins complexe que l'exemple original/

Et maintenant, si l'on veut ajouter la gestion des exceptions :

1
    .Subscribe(s => myLabel.Text = s, e => myLabel.Text = "Erreur ! " + e.Message);

Et vous l'avez :)

Combiner le Résultat de deux Téléchargements

Combiner le résultat de deux opérations asynchrones peut être assez complexe, et vous avez à gérer les exceptions, les rendez-vous et des états complexes. Je n'écrirais pas d'exemple ici, promis, mais je vais vous donner un exemple en utilisant les Reactive Extensions :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

public IObservable<string> StartDownload(string uri)
{
WebClient wc = new WebClient();

var o = Observable.FromEvent<DownloadStringCompletedEventArgs>(wc, "DownloadStringCompleted")

// On s'assure qu'on l'on est pas sur la Thread graphique
.ObserveOn(Scheduler.ThreadPool)

// On transforme l'évènement en chaine de caractères
.Select(newString => ProcessString(newString.EventArgs.Result));

wc.DownloadStringAsync(new Uri(uri));

return o;
}

public string ProcessString(string s)
{
// Un calcul très très long !
return s + "<!-- Processing End -->";
}

public void DisplayMyString()
{
var asyncDownload = StartDownload("http://bing.com");
var asyncDownload2 = StartDownload("http://google.com");

// On prend les deux résultats et on les combine lorsqu'ils sont disponibles
var zipped = asyncDownload.Zip(asyncDownload2, (left, right) => left + " - " + right);

// On revient sur la thread principale
zipped.ObserveOn(Scheduler.Dispatcher)

// On souscrit à la chaine et on l'affiche
.Subscribe(s => myLabel.Text = s);
}

Et vous obtiendrez une intéressante combinaison de Google et Bing :)

Publié jeudi 24 juin 2010 10:32 par jay
Classé sous , , , ,
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires


Les 10 derniers blogs postés

- Les actualités de la semaine sur c2i.fr (14 mai - 20 mai) par Richard Clark le il y a 4 heures et 25 minutes

- Reactive Extensions : Consommer des services avec Rx Partie 3, les pièges à éviter par Léonard Labat le il y a 13 heures et 30 minutes

- SharePoint Blog Site, problème d’archives par Le Blog (Vert) d'Arnaud JUND le 05-20-2012, 13:09

- Soirée ALT.NET Mai - 3 présentations par #Rui le 05-18-2012, 11:59

- [ #SharePoint 2010][ #SQLServer 2012] AlwaysOn pour SharePoint (2/4) : Configuration (2e partie)… par Le blog de Patrick [MVP SharePoint] le 05-18-2012, 11:31

- Team Foundation Server 11: tous les trésors cachés du site d’équipe par Philess le 05-16-2012, 19:01

- [PowerShell 3] Télécharger et installer la documentation en ligne par Blog de SPBrouillet (Pierrick BROUILLET) le 05-16-2012, 17:36

- [#SharePoint 2010][#SQLServer 2012] AlwaysOn pour SharePoint (1/4) : Configuration (1ère partie)… par Le blog de Patrick [MVP SharePoint] le 05-16-2012, 12:10

- Job Day @MIC Brussels - .Net Developers on Mobile applications par Le Blog (Vert) d'Arnaud JUND le 05-15-2012, 20:26

- [SharePoint 2010] – SharePoint 2010, Windows (Server) 8 et des erreurs IIS sont dans une VM… par Blog de SPBrouillet (Pierrick BROUILLET) le 05-14-2012, 12:10