Rx: Opérateur Catch, ou comment ignorer des erreurs
By Michael DELVA on Monday 10 May 2010, 12:31 - C# - Permalink
TweetSuite à mon article introductif sur les Reactive Extensions, je vais enchainer sur le même sujet une série d'articles (6 pour le moment sont prévus), bien plus courts que le dernier en date, et qui se focaliseront sur une manière de faire ou résoudre un problème précis que l'on peut rencontrer en utilisant cette API. Le premier article, ici-même, va s'attarder sur l'une des méthodes d'extension de Rx: Catch. Qui comme son nom le laisse supposer, a à voir avec les exceptions.
Imaginons une application dont le but est de mettre à jour d'autres applications (comme l'Apple Software Update par exemple). Cette application récupère la liste des programmes qu'elle doit mettre à jour et qui sont installés sur le système, puis se connecte à un serveur pour récupérer pour chacune des applications des informations sur les mises à jour à télécharger, avec notamment une liste d'urls pointant vers lesdites mises à jour (plusieurs urls pour une même mise à jour, pour bénéficier de miroirs).
Le comportement que je voudrais que cette application ait, c'est tenter de télécharger chaque mise à jour depuis la première adresse. Si le serveur répond et que le téléchargement peut commencer, pas de soucis on télécharge. Par contre, s'il y a une erreur quelconque (404, timeout...) on tente automatiquement avec la deuxième url, et ainsi de suite. Et on envoie une erreur uniquement dans le cas où le fichier n'a pas pu être téléchargé du tout.
La subtilité ici est de ne pas envoyer une erreur dans le canal OnError de l'IObserver en cas de souci, ce qui est le comportement normal. Pour éviter ça, nous allons utiliser la méthode d'extension Catch, dont voici la description:
Continues an observable sequence that is terminated by an exception with the next observable sequence
Et voici le code un peu modifié que j'ai utilisé, qui montre la procédure en action (les tests d'erreur en moins pour plus de lisibilité):
public Dictionary<AssemblyUpdateVersion, IObservable<DownloadProgress>> DownloadUpdates()
{
var result = new Dictionary<AssemblyUpdateVersion, IObservable<DownloadProgress>>();
var toDownload = from requestedUpdate in requestedUpdates
from updateVersion in requestedUpdate.Versions
select updateVersion;
foreach (AssemblyUpdateVersion versionDownloader in toDownload)
{
if (versionDownloader.RemoteFilePathList.Count() == 0)
{
result.Add(versionDownloader, Observable.Throw<DownloadProgress>(new InvalidOperationException()));
continue;
}
string futureDownloadedFilePath = Path.Combine(DownloadFolder, Path.GetFileName(versionDownloader.RemoteFilePathList.First()));
//versionDownloader.RemoteFilePathList is an IEnumerable<string> with all the download urls for the update
IEnumerable<IObservable<DownloadProgress>> downloadAttempts = from downloadUrl in versionDownloader.RemoteFilePathList
select FileDownloader.DownloadFile(downloadUrl, futureDownloadedFilePath, true);
AssemblyUpdateVersion downloader = versionDownloader;
var downloadObservable = from downloadAttempt in downloadAttempts.Catch()
select downloadAttempt.DownloadProgress;
result.Add(versionDownloader, downloadObservable);
}
return result;
}
Comme vous pouvez le constater, on construit pour chacune des mises à jour à télécharger un IEnumerable<IObservable<DownloadProgress>> (en utilisant la fonction DownloadFile dont je vous ai déjà parlé ici). Et on va convertir cet IEnumerable en un IObservable<DownloadProgress> grâce à la méthode d'extension Catch, qui va opérer comme souhaité: ignorer les erreurs si l'une des adresses de téléchargement est fonctionnelle, ou bien retourner une erreur si aucune ne l'est.
Ici je retourne un Dictionary<AssemblyUpdateVersion, IObservable<DownloadProgress>> car je vais appeler cette fonction depuis un ViewModel, où je vais utiliser la clé du dictionnaire pour le databinding, afin de pouvoir mettre à jour des barres de progression pour chacun des téléchargements, ces derniers ayant lieu simultanément.
Mais ceci se passe dans le prochain article!