Cet article a fait l'objet d'une traduction automatique.

Programmation asynchrone

Modèles pour les applications MVVM asynchrones : Liaison de données

Stephen Cleary

Asynchrone du code à l'aide de l'asynchrone et attendent des mots-clés est de transformer le fonctionnement des programmes sont rédigés, et avec raison. Bien qu'async et attendent peut être utile pour les logiciels de serveur, est l'accent mis sur les applications qui disposent d'une interface utilisateur. Pour de telles applications, ces mots clés peuvent produire une interface utilisateur plus réactive. Cependant, il n'apparaît pas clairement comment utiliser async et attendent avec des profils établis tels que le Model-View-ViewModel (MVVM). Cet article est le premier d'une série courte qui examinera les motifs permettant de combiner async et attendons avec MVVM.

Pour être clair, mon premier article sur async, « Les meilleures pratiques en programmation asynchrone » (msdn.microsoft.com/magazine/jj991977), s'appliquait à toutes les applications qui utilisent async/attendent, client et serveur. Cette nouvelle série s'appuie sur les meilleures pratiques dans cet article et introduit des modèles spécialement pour les applications côté client MVVM. Cependant, ces modèles sont justes motifs et peuvent être pas nécessairement les meilleures solutions pour un scénario spécifique. Si vous trouvez mieux, faites le moi savoir !

À partir de cette écriture, l'async et attendent les mots-clés sont pris en charge sur un grand nombre de plates-formes MVVM : Bureau (Windows Presentation Foundation [WPF] sur Microsoft .NET Framework 4 et supérieur), iOS/Android (Novell), magasin de Windows (Windows 8 et supérieur), Windows Phone (version 7.1 ou supérieure), Silverlight (version 4 ou ultérieure), ainsi que les bibliothèques de classes Portable (CIP) ciblant toute combinaison de ces plates-formes (par exemple, MvvmCross). Le moment est maintenant venu pour « async MVVM » patrons de développer.

Je suppose que vous êtes quelque peu familier avec async et attendent et tout à fait familier avec MVVM. Si ce n'est pas le cas, il y a un certain nombre de matériaux Introduction utiles disponible en ligne. Mon blog (bit.ly/19IkogW) comprend une intro async/attendent que les listes de ressources supplémentaires à la fin et la documentation MSDN sur async est assez bonne (recherche de "programmation asynchrone basée sur les tâches"). Pour plus d'informations sur MVVM, je recommande à peu près n'importe quoi écrit par Josh Smith.

Une Application Simple

Dans cet article, je vais construire une application incroyablement simple, comme Figure 1 montre. Lors du chargement de l'application, il démarre une requête HTTP et compte le nombre d'octets retournés. La demande HTTP peut se terminer avec succès, ou avec une exception, et l'application mettra à jour à l'aide de la liaison de données. L'application est entièrement sensible à tout moment.

The Sample Application
The Sample Application
The Sample Application
Figure 1 l'exemple d'Application

Tout d'abord, cependant, je tiens à mentionner que j'ai suivi le modèle MVVM assez vaguement dans mes propres projets, parfois à l'aide d'un modèle de domaine approprié, mais plus souvent en utilisant un ensemble de services et transferts de données des objets (essentiellement une couche d'accès aux données) au lieu d'un modèle réel. Je suis également plutôt pragmatique lorsqu'il s'agit de la vue ; Je ne pas hésiter à quelques lignes de codebehind si l'alternative est des dizaines de lignes de code dans le soutien des classes et XAML. Alors, quand je parle de MVVM, comprendre que je n'utilise pas n'importe quel particulier définition stricte du terme.

Une des premières choses que vous devez considérer lors de l'introduction async et vous attendent pour le modèle MVVM consiste à déterminer quelles parties de votre solution ont besoin de l'interface utilisateur contexte de thread. Plates-formes Windows sont sérieux au sujet de composants d'interface utilisateur étant accessibles qu'à partir du thread d'interface utilisateur qui les possède. Évidemment, la vue est entièrement liée au contexte de l'interface utilisateur. Je prends également le stand dans mes applications que quoi que ce soit lié à l'affichage via la liaison de données est lié au contexte de l'interface utilisateur. Les versions récentes de WPF sont desserrées cette restriction, ce qui permet un partage de données entre le thread d'interface utilisateur et les threads d'arrière-plan (par exemple, BindingOperations.EnableCollection­synchronisation). Toutefois, la prise en charge pour la liaison de données inter-threads n'est pas garantie sur toutes les plateformes MVVM (WPF, iOS/Android/Windows Phone, Windows Store), dans mes propres projets, je viens de traiter quoi que ce soit lié aux données à l'interface utilisateur comme ayant une affinité de thread d'interface utilisateur.

En conséquence, j'ai toujours traiter mes ViewModels comme s'ils sont liés au contexte de l'interface utilisateur. Dans mes applications, le ViewModel est plus étroitement lié à la vue que le modèle — et la couche ViewModel est essentiellement une API pour l'ensemble de l'application. L'affichage offre littéralement juste la coquille des éléments d'interface utilisateur dans lequel l'application réelle existe. La couche ViewModel est conceptuellement une interface utilisateur testable, complete avec une affinité de thread d'interface utilisateur. Si votre modèle est un domaine réel modèle (pas une couche d'accès aux données) il y a liaison de données entre le modèle et le ViewModel, puis le modèle lui-même a également l'affinité de thread d'interface utilisateur. Une fois que vous avez identifié quelles couches ont une affinité de l'interface utilisateur, vous devriez être capable de dessiner une ligne mentale entre le « code d'interface utilisateur affine » (View et ViewModel et éventuellement le modèle) et le « code d'interface utilisateur indépendant » (probablement le modèle et certainement tous les autres calques, tels que l'accès de services et de données).

En outre, tout le code en dehors de la couche de la vue (c'est-à-dire le ViewModel et modèle couches, services et ainsi de suite) ne devrait pas dépendre sur n'importe quel type lié à une plate-forme d'interface utilisateur spécifique. Toute utilisation directe du répartiteur (Windows Phone/Novell/WPF / Silverlight), CoreDispatcher (magasin de Windows), ou ISynchronizeInvoke (Windows Forms) est une mauvaise idée. (SynchronizationContext est légèrement mieux, mais à peine.) Par exemple, il y a beaucoup de code sur Internet qui fait un travail asynchrone et utilise ensuite le répartiteur pour mettre à jour l'interface utilisateur ; une solution plus portable et allégée doit utiliser attendent pour travail asynchrone et mettre à jour l'interface utilisateur sans utiliser le répartiteur.

ViewModels sont la couche plus intéressante parce qu'ils ont une affinité de l'interface utilisateur, mais ne dépendent d'un contexte spécifique de l'interface utilisateur. Dans cette série, je vais combiner async et MVVM de manière à éviter des types spécifiques de l'interface utilisateur en suivant aussi async conseillées ; ce premier article se concentre sur la liaison de données asynchrone.

Propriétés liées aux données asynchrones

Le terme « propriété asynchrone » est en réalité un oxymoron. Accesseurs get de propriété devrait exécuter immédiatement et récupérer des valeurs actuelles, pas le coup d'envoi des opérations en arrière-plan. C'est probablement l'une des raisons pour lesquelles que le mot-clé async ne peut pas être utilisé sur un accesseur Get de propriété. Si vous trouvez votre conception demandant une propriété asynchrone, examiner d'abord quelques alternatives. En particulier, devrait la propriété être en fait une méthode (ou une commande) ? Si l'accesseur Get de propriété doit lancer une nouvelle opération asynchrone chaque fois qu'il est consulté, qui n'est pas une propriété du tout. Méthodes asynchrones sont simples, et j'aborderai les commandes asynchrones dans un autre article.

Dans cet article, je vais développer une propriété liée aux données asynchrone ; autrement dit, une propriété liée aux données que je mets à jour avec les résultats d'une opération asynchrone. Un scénario courant est lorsqu'un ViewModel doit récupérer des données d'une source externe.

Comme je l'ai expliqué plus tôt, pour mon exemple d'application, je vais définir un service qui compte les octets dans une page Web. Pour illustrer l'aspect de la réactivité d'async/attendent, ce service retardera aussi quelques secondes. J'aborderai plus réalistes services asynchrones dans un article plus tard ; pour l'instant, le « service » est juste la méthode unique dans Figure 2.

La figure 2 MyStaticService.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;
public static class MyStaticService
{
  public static async Task<int> CountBytesInUrlAsync(string url)
  {
    // Artificial delay to show responsiveness.
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
    // Download the actual data and count it.
using (var client = new HttpClient())
    {
      var data = await client.GetByteArrayAsync(url).ConfigureAwait(false);
      return data.Length;
    }
  }
}

Notez que cela est considéré comme un service, donc il est indépendant de l'interface utilisateur. Parce que le service est indépendant de l'interface utilisateur, il utilise ConfigureAwait(false) chaque fois qu'il le fait une attente (tel que mentionné dans mon autre article, « Les meilleures pratiques en programmation asynchrone »).

Nous allons ajouter une simple vue et le ViewModel qui démarre une requête HTTP au démarrage. L'exemple de code utilise les fenêtres WPF avec les vues créant leurs ViewModels sur chantier. C'est juste par souci de simplicité ; les principes async et discuté dans cette série d'articles s'appliquent sur toutes les plateformes MVVM, les cadres et les bibliothèques. La vue pour l'instant consistera en une seule fenêtre principale avec une seule étiquette. Le code XAML pour la vue principale lie juste au membre UrlByteCount :

<Window x:Class="MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>
    <Label Content="{Binding UrlByteCount}"/>
  </Grid>
</Window>

Le codebehind de la fenêtre principale crée le ViewModel :

public partial class MainWindow
{
  public MainWindow()
  {
    DataContext = new BadMainViewModelA();
    InitializeComponent();
  }
}

Erreurs courantes

Vous remarquerez que le type de ViewModel est appelé BadMainViewModelA. C'est parce que je vais d'abord étudier quelques erreurs courantes relatives aux ViewModels. Une erreur courante est de bloquer de façon synchrone sur l'opération, comme suit :

public class BadMainViewModelA
{
  public BadMainViewModelA()
  {
    // BAD CODE!!!
UrlByteCount =
      MyStaticService.CountBytesInUrlAsync("http://www.example.com").Result;
  }
  public int UrlByteCount { get; private set; }
}

Il s'agit d'une violation de la ligne directrice d'async "async all the way", mais les développeurs essaient parfois cela s'ils estiment qu'ils sont hors options. Si vous exécutez ce code, vous verrez que cela fonctionne, dans une certaine mesure. Code qui utilise la tâche < T > ou Task.Wait.Résultat au lieu d'await synchrone bloque sur cette opération.

Il y a quelques problèmes avec blocage synchrone. Le plus évident est le code est maintenant prendre une opération asynchrone et blocage là-dessus ; Ce faisant, il perd tous les avantages de l'asynchronisme. Si vous exécutez le code actuel, vous verrez que l'application ne fait rien pendant quelques secondes, et ensuite la fenêtre de l'interface utilisateur ressorts entièrement formée en vue avec ses résultats déjà peuplées. Le problème est que l'application est insensible, ce qui est inacceptable pour de nombreuses applications modernes. L'exemple de code a un retard délibéré de mettre l'accent sur ce problème de blocage ; dans une application réelle, ce problème pourrait passer inaperçu au cours du développement et apparaissent uniquement dans les scénarios de client « inhabituel » (tels que la perte de connectivité réseau).

Un autre problème avec blocage synchrone est plus subtil : Le code est plus fragile. Mon exemple de service utilise ConfigureAwait(false) correctement, tout comme un service devrait. Cependant, c'est facile d'oublier, surtout si vous (ou vos collègues de travail) n'utilisent pas régulièrement async. Examiner ce qui pourrait arriver au fil du temps que le code de service est maintenu. Un développeur de maintenance peut-être oublier un ConfigureAwait, et à ce moment-là, le blocage du thread interface utilisateur deviendrait un blocage du thread d'interface utilisateur. (Ceci est décrit plus en détail dans mon article précédent sur les meilleures pratiques async).

OK, donc vous devez utiliser « async complètement. » Cependant, de nombreux développeurs procéder à la deuxième approche défectueuse, illustrée en Figure 3.

Figure 3 BadMainViewModelB.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
public sealed class BadMainViewModelB : INotifyPropertyChanged
{
  public BadMainViewModelB()
  {
    Initialize();
  }
  // BAD CODE!!!
private async void Initialize()
  {
    UrlByteCount = await MyStaticService.CountBytesInUrlAsync(
      "http://www.example.com");
  }
  private int _urlByteCount;
  public int UrlByteCount
  {
    get { return _urlByteCount; }
    private set { _urlByteCount = value; OnPropertyChanged(); }
  }
  public event PropertyChangedEventHandler PropertyChanged;
  private void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
  }
}

Encore une fois, si vous exécutez ce code, vous trouverez que cela fonctionne. L'interface utilisateur affiche maintenant immédiatement, avec « 0 » à l'étiquette pendant quelques secondes, puis il est mis à jour avec la valeur correcte. L'interface utilisateur est sensible, et tout semble très bien. Toutefois, le problème dans ce cas est gestion des erreurs. Avec une méthode void async, les erreurs déclenchées par l'opération asynchrone seront brisera l'application par défaut. Il s'agit d'une autre situation qui est facile à manquer au cours du développement et s'affiche uniquement dans des conditions « bizarres » sur des machines clientes. Même changer le code de Figure 3 d'async Sub Async tâche améliore à peine l'application ; toutes les erreurs pourraient être ignorées en mode silencieux, laissant l'utilisateur, vous vous demandez ce qui s'est passé. Aucune méthode de gestion des erreurs est approprié. Et bien qu'il soit possible de traiter cela par l'interception d'exceptions de l'opération asynchrone et mise à jour d'autres propriétés liées aux données, qui seraient traduirait par beaucoup de code fastidieux.

Une meilleure approche

Idéalement, ce que j'ai vraiment envie, c'est un type comme tâche < T > avec les propriétés pour obtenir des résultats ou des détails de l'erreur. Malheureusement, la tâche < T > n'est pas facile de la liaison de données pour deux raisons : Il n'implémente pas INotify­bloque PropertyChanged et sa propriété Result. Toutefois, vous pouvez définir un « observateur de la tâche » de toutes sortes, tels que le type dans Figure 4.

Figure 4 NotifyTaskCompletion.cs

using System;
using System.ComponentModel;
using System.Threading.Tasks;
public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
{
  public NotifyTaskCompletion(Task<TResult> task)
  {
    Task = task;
    if (!task.IsCompleted)
    {
      var _ = WatchTaskAsync(task);
    }
  }
  private async Task WatchTaskAsync(Task task)
  {
    try
    {
      await task;
    }
    catch
    {
    }
    var propertyChanged = PropertyChanged;
    if (propertyChanged == null)
        return;
    propertyChanged(this, new PropertyChangedEventArgs("Status"));
    propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
    propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
    if (task.IsCanceled)
    {
      propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
    }
    else if (task.IsFaulted)
    {
      propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
      propertyChanged(this, new PropertyChangedEventArgs("Exception"));
      propertyChanged(this,
        new PropertyChangedEventArgs("InnerException"));
      propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
    }
    else
    {
      propertyChanged(this,
        new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
      propertyChanged(this, new PropertyChangedEventArgs("Result"));
    }
  }
  public Task<TResult> Task { get; private set; }
  public TResult Result { get { return (Task.Status == TaskStatus.RanToCompletion) ?
Task.Result : default(TResult); } }
  public TaskStatus Status { get { return Task.Status; } }
  public bool IsCompleted { get { return Task.IsCompleted; } }
  public bool IsNotCompleted { get { return !Task.IsCompleted; } }
  public bool IsSuccessfullyCompleted { get { return Task.Status ==
    TaskStatus.RanToCompletion; } }
  public bool IsCanceled { get { return Task.IsCanceled; } }
  public bool IsFaulted { get { return Task.IsFaulted; } }
  public AggregateException Exception { get { return Task.Exception; } }
  public Exception InnerException { get { return (Exception == null) ?
null : Exception.InnerException; } }
  public string ErrorMessage { get { return (InnerException == null) ?
null : InnerException.Message; } }
  public event PropertyChangedEventHandler PropertyChanged;
}

Parcourons la méthode de base NotifyTaskCompletion < T >.WatchTaskAsync. Cette méthode prend une tâche qui représente l'opération asynchrone et (de façon asynchrone) attend qu'il se termine. Notez que l'attente n'utilise pas de ConfigureAwait(false) ; Je veux revenir au contexte de l'interface utilisateur avant de déclencher les notifications de PropertyChanged. Cette méthode viole une directive de codage commune ici : Il a une clause catch générale vide. Dans ce cas, cependant, c'est exactement ce que je veux. Je ne veux pas propager exceptions directement vers la boucle principale de l'interface utilisateur ; Je veux capturer toutes les exceptions et définir des propriétés pour que la gestion des erreurs est faite via la liaison de données. Une fois la tâche terminée, le type soulève PropertyChanged notifications pour toutes les propriétés appropriées.

Un ViewModel mis à jour à l'aide de NotifyTaskCompletion < T > ressemblerait à ceci :

public class MainViewModel
{
  public MainViewModel()
  {
    UrlByteCount = new NotifyTaskCompletion<int>(
      MyStaticService.CountBytesInUrlAsync("http://www.example.com"));
  }
  public NotifyTaskCompletion<int> UrlByteCount { get; private set; }
}

Ce ViewModel débutera l'opération immédiatement et puis créer un « observateur » pour la tâche qui en résulte lié aux données. Le code de liaison de données de vue doit être actualisée permettant de lier explicitement le résultat de l'opération, comme ceci :

<Window x:Class="MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>
    <Label Content="{Binding UrlByteCount.Result}"/>
  </Grid>
</Window>

Notez que le contenu de l'étiquette est lié aux données NotifyTask­achèvement < T >.Résultat, pas de tâche < T >.Résultat. NotifyTaskCompletion < T >.Il en résulte l'environnement la liaison de données : Il ne bloque pas, et il en avisera la liaison lorsque la tâche se termine. Si vous exécutez le code maintenant, vous trouverez qu'il se comporte à l'instar de l'exemple précédent : L'interface utilisateur est réactif et charge immédiatement (affiche la valeur par défaut « 0 ») et puis met à jour en quelques secondes avec les résultats réels.

L'avantage de NotifyTaskCompletion < T > est qu'il dispose de nombreuses autres propriétés aussi bien, alors vous pouvez utiliser la liaison de données pour afficher les indicateurs occupés ou les détails de l'erreur. Il n'est pas difficile à utiliser certaines de ces propriétés de commodité pour créer un indicateur occupé et les détails de l'erreur complètement l'affichage, comme le code de liaison de données mis à jour en Figure 5.

Figure 5 MainWindow.xaml

<Window x:Class="MainWindow"
        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Window.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
  </Window.Resources>
  <Grid>
    <!-- Busy indicator -->
    <Label Content="Loading..." Visibility="{Binding UrlByteCount.IsNotCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}"/>
    <!-- Results -->
    <Label Content="{Binding UrlByteCount.Result}" Visibility="{Binding
      UrlByteCount.IsSuccessfullyCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}"/>
    <!-- Error details -->
    <Label Content="{Binding UrlByteCount.ErrorMessage}" Background="Red"
      Visibility="{Binding UrlByteCount.IsFaulted,
      Converter={StaticResource BooleanToVisibilityConverter}}"/>
  </Grid>
</Window>

Avec cette dernière mise à jour, ce qui modifie uniquement la vue, l'application affiche « Loading... » pendant quelques secondes (tout en restant sensibles) et puis met à jour ou l'autre des résultats de l'opération ou un message d'erreur s'affichée sur un fond rouge.

NotifyTaskCompletion < T > gère un seul cas d'utilisation : Lorsque vous avez une opération asynchrone et voulez données lier les résultats. Il s'agit d'un scénario courant lorsque vous effectuez des recherches de données ou le chargement lors du démarrage. Toutefois, cela n'aide pas beaucoup quand vous avez une commande réelle qui est asynchrone, par exemple, « enregistrer l'enregistrement en cours. » (Je considérerai les commandes asynchrones dans mon prochain article).

À première vue, il semble que c'est beaucoup plus de travail pour construire une interface utilisateur asynchrone, et c'est vrai dans une certaine mesure. Utilisation conforme de l'asynchrone et attendent les mots-clés encourage vivement à vous aider à concevoir un UX mieux. Lorsque vous passez à une interface utilisateur asynchrone, vous trouvez que vous pouvez ne plus bloquer l'interface utilisateur lorsqu'une opération asynchrone est en cours. Vous devez penser à ce que l'interface utilisateur devrait ressembler au cours du processus de chargement et délibérément design pour cet État. Il s'agit plus de travail, mais c'est un travail qui doit être fait pour des applications plus modernes. Et c'est une des raisons que plates-formes plus récents tels que la Banque de Windows prennent en charge API asynchrones uniquement : pour encourager les développeurs à concevoir un UX plus réactif.

Synthèse

Lorsqu'une base de code est convertie de synchrone en asynchrone, généralement l'accès de service ou des données composantes changent tout d'abord et async pousse de là vers l'interface utilisateur. Une fois que vous avez fait quelques fois, traduisant une méthode de synchrone asynchrone devient assez simple. J'attends (et j'espère) que cette traduction sera automatisée par outillage futur. Toutefois, lorsque async frappe l'interface utilisateur, c'est-à-dire lorsque real changements sont nécessaires.

Lorsque l'interface utilisateur devient asynchrone, vous devez résoudre les situations où vos applications ne répondent pas en améliorant leur conception de l'interface utilisateur. Le résultat final est une application plus réactive, plus moderne. « Rapide et fluide, » si vous voulez.

Cet article introduit un type simple qui peut se résumer comme une tâche < T > pour la liaison de données. La prochaine fois, je vais regarder les commandes asynchrones et explorer un concept qui est essentiellement un « ICommand pour async ». Puis, dans le dernier article de la série, je vais emballer en considérant les services asynchrones. Gardez à l'esprit la communauté se développe encore ces modèles ; n'hésitez pas à les ajuster à vos besoins particuliers.

Stephen Cleary est un mari, le père et le programmeur vivant dans le nord du Michigan. Il a travaillé avec multithreading et asynchrone de programmation pendant 16 ans et a utilisé le soutien async dans Microsoft .NET Framework depuis la première version CTP. Sa page d'accueil, y compris son blog, est à stephencleary.com.

Merci aux experts techniques Microsoft suivants d'avoir relu cet article : James McCaffrey et Stephen Toub