Meilleures pratiques pour implémenter le modèle asynchrone basé sur des événements

Le modèle asynchrone basé sur les événements vous permet d’exposer efficacement un comportement asynchrone dans les classes, en utilisant une sémantique d’événement et de délégué familière. Pour implémenter le modèle asynchrone basé sur les événements, vous devez respecter certaines contraintes liées au comportement. Les sections ci-après décrivent les exigences et les recommandations que vous devez prendre en compte quand vous implémentez une classe qui suit le modèle asynchrone basé sur les événements.

Pour une présentation, consultez Implémentation du modèle asynchrone basé sur des événements.

Garanties de comportement obligatoires

Si vous implémentez le modèle asynchrone basé sur les événements, vous devez fournir une série de garanties pour que votre classe se comporte correctement et que les clients de votre classe puissent s'appuyer sur ce comportement.

Completion

Appelez toujours le gestionnaire d’événements MethodNameCompleted quand l’opération s’est terminée correctement, ou bien en cas d’erreur ou d’annulation. Les applications ne doivent jamais rencontrer de situation dans laquelle elles demeurent inactives sans jamais terminer l'opération en cours. L’exception à cette règle est si l’opération asynchrone elle-même est conçue de manière à ne jamais se terminer.

Événement Completed et classe EventArgs

Pour chaque méthode MethodNameAsync distincte, appliquez les contraintes de conception suivantes :

  • Définissez un événement MethodNameCompleted sur la même classe que la méthode.

  • Définissez une classe EventArgs et le délégué associé pour l’événement MethodNameCompleted qui dérive de la classe AsyncCompletedEventArgs. Le nom de classe par défaut doit être de la forme NomMéthodeCompletedEventArgs.

  • Vérifiez que la classe EventArgs est spécifique des valeurs de retour de la méthode nom_méthode. Quand vous utilisez la classe EventArgs, vous ne devez jamais obliger les développeurs à effectuer un transtypage du résultat.

    L'exemple de code suivant montre une bonne implémentation de cette contrainte de conception, suivie d'une mauvaise.

// Good design
private void Form1_MethodNameCompleted(object sender, xxxCompletedEventArgs e)
{
    DemoType result = e.Result;
}

// Bad design
private void Form1_MethodNameCompleted(object sender, MethodNameCompletedEventArgs e)
{
    DemoType result = (DemoType)(e.Result);
}
  • Ne définissez pas de classe EventArgs pour les méthodes de retour qui retournent void. À la place, utilisez une instance de la classe AsyncCompletedEventArgs.

  • Veillez à toujours déclencher l’événement NomMéthodeCompleted. Cet événement doit être déclenché quand l'opération s'est terminée correctement, ou bien en cas d'erreur ou d'annulation. Les applications ne doivent jamais rencontrer de situation dans laquelle elles demeurent inactives sans jamais terminer l'opération en cours.

  • Veillez à intercepter toutes les exceptions qui se produisent au cours de l'opération asynchrone et à affecter l'exception interceptée à la propriété Error.

  • Si une erreur se produit pendant l'exécution de la tâche, les résultats doivent être inaccessibles. Quand la propriété Error n'a pas pour valeur null, assurez-vous que l'accès à toute propriété dans la structure EventArgs lève une exception. Utilisez la méthode RaiseExceptionIfNecessary pour effectuer cette vérification.

  • Modélisez un délai au terme duquel une erreur se produit. Quand un délai arrive à expiration, déclenchez l’événement NomMéthodeCompleted et affectez une TimeoutException à la propriété Error.

  • Si votre classe prend en charge plusieurs appels simultanés, vérifiez que l’événement NomMéthodeCompleted contient l’objet userSuppliedState adéquat.

  • Vérifiez que l’événement NomMéthodeCompleted est déclenché sur le thread approprié et au moment adéquat du cycle de vie de l’application. Pour plus d'informations, voir la section Thread et contextes.

Exécution d'opérations simultanément

  • Si votre classe prend en charge plusieurs appels simultanés, autorisez le développeur à effectuer le suivi de chaque appel séparément en définissant la surcharge de NomMéthodeAsync qui prend un paramètre d’état objet, ou un ID de tâche, appelé userSuppliedState. Ce paramètre doit toujours être le dernier de la signature de la méthode NomMéthodeAsync.

  • Si votre classe définit la surcharge de NomMéthodeAsync qui prend un paramètre d’état objet, ou un ID de tâche, veillez à effectuer le suivi de la durée de vie de l’opération avec cet ID de tâche, et à le fournir en retour au gestionnaire d’achèvement. Vous pouvez vous aider de classes d'assistance. Pour plus d’informations sur la gestion de l’accès concurrentiel, consultez Guide pratique pour implémenter un composant qui prend en charge le modèle asynchrone basé sur des événements.

  • Si votre classe définit la méthode NomMéthodeAsync sans le paramètre d’état et qu’elle ne prend pas en charge plusieurs appels simultanés, veillez à ce que toute tentative d’appel de NomMéthodeAsync avant que soit achevé l’appel précédent de NomMéthodeAsync lève une InvalidOperationException.

  • D’une façon générale, ne levez pas d’exception si la méthode NomMéthodeAsync sans le paramètre userSuppliedState est appelée plusieurs fois et que plusieurs opérations se trouvent ainsi en attente. Vous pouvez lever une exception quand votre classe ne peut pas explicitement gérer cette situation, mais partez du principe que les développeurs peuvent gérer ces différents rappels impossibles à distinguer.

Accès aux résultats

Rapport de progression

  • Dans la mesure du possible, prenez en charge le rapport de progression. Cela permet aux développeurs de fournir une meilleure expérience utilisateur d'application quand ils utilisent votre classe.

  • Si vous implémentez un événement ProgressChanged ou NomMéthodeProgressChanged, vérifiez qu’aucun événement de ce type n’est déclenché pour une opération asynchrone en particulier après que l’événement NomMéthodeCompleted de cette opération a été déclenché.

  • Si la classe ProgressChangedEventArgs standard est remplie, assurez-vous que la propriété ProgressPercentage peut toujours être interprétée en tant que pourcentage. La valeur de pourcentage n'a pas besoin d'être précise, mais elle doit représenter un pourcentage. Si la mesure du rapport de progression doit être autre chose qu'un pourcentage, dérivez une classe de la classe ProgressChangedEventArgs et laissez ProgressPercentage défini sur 0. Évitez d'utiliser une mesure d'indication autre qu'un pourcentage.

  • Assurez-vous que l'événement ProgressChanged est déclenché sur le thread approprié et au moment adéquat pendant le cycle de vie de l'application. Pour plus d'informations, voir la section Thread et contextes.

Implémentation d'IsBusy

  • N'exposez pas de propriété IsBusy si votre classe prend en charge plusieurs appels simultanés. Par exemple, les proxys de service web XML n'exposent pas de propriété IsBusy, car ils prennent en charge plusieurs appels simultanés de méthodes asynchrones.

  • La propriété IsBusy doit retourner true une fois que la méthode NomMéthodeAsync a été appelée et avant que l’événement NomMéthodeCompleted n’ait été déclenché. Sinon elle doit retourner false. Les composants BackgroundWorker et WebClient sont des exemples de classes qui exposent une propriété IsBusy.

Annulation

  • Dans la mesure du possible, prenez en charge l'annulation. Cela permet aux développeurs de fournir une meilleure expérience utilisateur d'application quand ils utilisent votre classe.

  • Dans le cas de l'annulation, définissez l'indicateur Cancelled dans l'objet AsyncCompletedEventArgs.

  • Assurez-vous que toute tentative d'accéder au résultat lève une exception InvalidOperationException signalant que l'opération a été annulée. Utilisez la méthode AsyncCompletedEventArgs.RaiseExceptionIfNecessary pour effectuer cette vérification.

  • Assurez-vous que les appels d'une méthode d'annulation réussissent toujours et qu'ils ne déclenchent jamais d'exception. En général, un client n'est pas informé qu'il peut ou non annuler une opération à tout moment, ni qu'une annulation précédemment émise a effectivement réussi. Toutefois, l'application est toujours avertie qu'une annulation a réussi, car elle est impliquée dans la gestion de l'état d'achèvement.

  • Déclenchez l’événement NomMéthodeCompleted quand l’opération est annulée.

Erreurs et exceptions

  • Interceptez toute exception qui se produit pendant l'opération asynchrone et définissez la valeur de la propriété AsyncCompletedEventArgs.Error sur cette exception.

Thread et contextes

Pour que votre classe fonctionne correctement, il est primordial que les gestionnaires d’événements du client soient appelés sur le thread ou contexte adéquat en fonction du modèle d’application donné, notamment en ce qui concerne les applications ASP.NET et Windows Forms. Deux classes d'assistance importantes vous permettent de vous assurer que votre classe asynchrone se comporte correctement dans le cadre de n'importe quel modèle d'application : AsyncOperation et AsyncOperationManager.

AsyncOperationManager fournit une méthode, CreateOperation, qui retourne un objet AsyncOperation. Votre méthode NomMéthodeAsync appelle CreateOperation et votre classe utilise l’objet AsyncOperation retourné pour effectuer le suivi de la durée de vie de la tâche asynchrone.

Pour indiquer la progression, les résultats incrémentiels et l'achèvement au client, appelez les méthodes Post et OperationCompleted sur la classe AsyncOperation. AsyncOperation prend en charge le marshaling des appels des gestionnaires d’événements du client en thread ou contexte adéquat.

Notes

Vous pouvez contourner ces règles si vous souhaitez explicitement aller à l'encontre de la stratégie du modèle d'application, tout en tirant parti des autres avantages de l'utilisation du modèle asynchrone basé sur les événements. Par exemple, vous pouvez souhaiter qu'une classe fonctionnant dans Windows Forms soit libre de threads. Vous pouvez créer une classe libre de threads, sous réserve que les développeurs comprennent les restrictions que cela implique. Les applications console ne synchronisent pas l'exécution des appels Post. Cela peut entraîner le déclenchement d'événements ProgressChanged dans le désordre. Si vous souhaitez que l'exécution des appels Post soit sérialisée, implémentez et installez une classe System.Threading.SynchronizationContext.

Pour plus d’informations sur AsyncOperation et AsyncOperationManager dans le cadre d’opérations asynchrones, consultez la page Guide pratique pour implémenter un composant qui prend en charge le modèle asynchrone basé sur les événements.

Consignes

  • Dans l'idéal, chaque appel de méthode doit être indépendant des autres appels. Vous devez éviter d'associer des appels qui partagent des ressources. Si des ressources doivent être partagées par des appels, vous devez prévoir un mécanisme de synchronisation approprié dans votre implémentation.

  • Les conceptions qui obligent le client à implémenter une synchronisation sont déconseillées. Par exemple, dans le cas d'une méthode asynchrone qui reçoit un objet statique global comme paramètre, plusieurs appels simultanés de ce type de méthode peuvent entraîner une altération des données ou des interblocages.

  • Si vous implémentez une méthode avec la surcharge de plusieurs appels (userState dans la signature), votre classe doit gérer une collection d'états utilisateur, ou d'ID de tâche, ainsi que leurs opérations en attente correspondantes. Cette collection doit être protégée avec des régions lock, car les différents appels ajoutent et suppriment des objets userState dans la collection.

  • Pensez à réutiliser les classes CompletedEventArgs si cela est possible et approprié. Dans ce cas, l'affectation des noms n'est pas cohérente avec le nom de la méthode, car un délégué et un type EventArgs donnés ne sont pas liés à une même méthode. Toutefois, évitez absolument que les développeurs soit obligés d'effectuer un transtypage de la valeur récupérée d'une propriété sur la classe EventArgs.

  • Si vous créez une classe qui dérive de Component, n'implémentez pas et n'installez pas votre propre classe SynchronizationContext. Ce sont les modèles d'application, pas les composants, qui contrôlent la classe SynchronizationContext utilisée.

  • Quand vous utilisez un type quelconque de multithreading, vous vous exposez potentiellement à des bogues sérieux et complexes. Consultez les Meilleures pratiques pour le threading managé avant d'implémenter une solution qui utilise le multithreading.

Voir aussi