Partager via


Silverlight en ligne

Silverlight dans un monde connecté occasionnellement

Mark Bloodworth

Télécharger l'exemple de code

Les gens vivent dans un monde en ligne, ou c'est tout au moins le cas de certains d'entre nous, à certains moments. À l'avenir, il est possible qu'une connectivité généralisée et continue soit disponible, avec une bande passante plus importante que nécessaire, mais cela n'est pas le cas aujourd'hui. En réalité, nous sommes connectés occasionnellement et avec suffisamment de bande passante. Nous savons rarement quel est l'état de la connexion à un moment donné.

Il est nécessaire de tenir compte de nombreuses options en matière d'architecture lors de la conception d'applications susceptibles d'offrir aux utilisateurs la meilleure expérience possible dans cette réalité.

Qu'ils soient riches ou pauvres, les clients intelligents ont un point commun, à savoir qu'ils sont déployés sur une machine locale. En raison de leur nature, il est donc possible d'exécuter ces applications sans être connecté à un réseau. En revanche, une application traditionnelle basée sur le navigateur doit être connectée à un serveur Web distant pour être exécutée.

Il existe entre ces deux extrêmes un éventail d'options sans cesse plus important. Toutes ces options proposent des capacités diverses d'exécution d'une application hors connexion, ainsi que différents degrés de flexibilité et d'interactivité en matière de conception d'interface utilisateur. De plus, elles imposent différents niveaux de restriction de sécurité. Nous aborderons ici la dernière version des applications à connexion occasionnelle qui offrent une expérience utilisateur très interactive et peuvent être exécutées à l'intérieur ou à l'extérieur d'un navigateur. Nous présenterons des exemples de code illustrant la détection de la connectivité réseau, ainsi que des traitements en arrière-plan qui chargent et téléchargent des données lors d'une connexion en ligne.

Contexte

Imaginons l'évolution d'une application type présentant un intérêt dans le cadre de cette discussion. Notre exemple a commencé sous la forme d'une application simple client lourd qui était exécutée sous les systèmes d'exploitation Windows uniquement. La solution initiale permettait certes aux utilisateurs de travailler hors connexion, mais ses limites devenaient de plus en plus évidentes :

  • Une exigence a été ajoutée afin de permettre la prise en charge de plusieurs systèmes d'exploitation. Seul un nombre limité d'utilisateurs potentiels pouvait être pris en charge dans le cadre de la première version.
  • Les problèmes de déploiement entraînaient des incohérences dans les versions installées par la base des utilisateurs.

En raison des demandes accrues de prise en charge d'une solution plus légère capable de fonctionner sur plusieurs systèmes d'exploitation et de réduire les problèmes de déploiement, l'application a été réécrite comme simple application HTML de client léger. Cela a cependant créé d'autres problèmes :

  • Ses fonctionnalités d'interface utilisateur étaient limitées et créaient ainsi une expérience loin d'être intuitive.
  • Elle exigeait de longs tests de compatibilité des navigateurs.
  • Les performances sur de nombreuses infrastructures de réseau étaient médiocres. Par exemple, de grandes quantités de données de référence devaient être téléchargées chaque fois qu'un utilisateur avait besoin de remplir un formulaire, ainsi que de longs scripts permettant de gérer la logique impliquée dans la validation.
  • Les utilisateurs ne pouvaient pas utiliser l'application hors connexion.

Il était évident que cette version n'était pas à la hauteur non plus.

Bien qu'elle soit difficile à atteindre, la solution idéale dans ce cas est une application RIA (Rich Internet Application, application Internet riche) avec une interface utilisateur intuitive et souple. Lorsqu'elle est en ligne, l'application doit laisser les utilisateurs gérer des quantités importantes de données et effectuer des téléchargements de données asynchrones ainsi que la validation des données, et ce sans verrouiller l'interface utilisateur. Elle doit prendre en charge le travail hors connexion et l'accès à une banque de données sur le client. Elle doit s'intégrer aux périphériques matériels sur le client, par exemple les appareils photos. En dernier lieu, la solution idéale devrait être lancée à partir du menu Démarrer ou d'une icône d'application, et exister au-delà des limites d'un navigateur Web.

Silverlight est capable de répondre à ces exigences. Silverlight 3 a introduit le concept d'une expérience hors navigateur, concept qui a été étendu dans le cadre de Silverlight 4. De plus, Silverlight 4 a lancé la capacité d'interaction avec des dossiers spécifiques tels que « Mes images », au même titre qu'avec des périphériques matériels comme les webcams (les applications qui utilisent cette fonctionnalité améliorée demandent un niveau de confiance élevé ainsi que l'approbation de l'utilisateur avant l'installation de l'application). Pour plus d'informations sur les applications approuvées, consultez l'article suivant : msdn.microsoft.com/library/ee721083(v=VS.95)). Au cours de cet article, nous nous concentrerons sur les problèmes rencontrés lors de la conception d'une application prenant en charge tout travail en ligne et hors connexion.

La figure 1 illustre le cas d'une ébauche d'architecture intéressante.

image: Candidate High-Level Architecture
Figure 1 Architecture de haut niveau intéressante

Dans ce cas, les scénarios utilisateur classiques sont notamment les suivants :

  • Un employé mobile avec un ordinateur portable. L'ordinateur portable peut avoir une carte 3G ou être connecté à un réseau sans fil dans un bureau ou une zone d'accès sans fil à Internet.
  • Un utilisateur se servant d'un ordinateur de bureau dans un environnement où la connectivité est limitée, par exemple des bureaux situés dans un bâtiment ancien ou préfabriqué.

Détection de l'état du réseau

Une application censée fonctionner dans un environnement connecté occasionnellement doit être capable de vérifier l'état en cours de la connexion réseau. Silverlight 3 a introduit cette fonctionnalité avec la méthode NetworkInterface.GetIsNetworkInterfaceAvailable. Ces applications peuvent également utiliser l'événement NetworkChange.NetworkAddressChangedEvent, qui est généré lorsque l'adresse IP d'une interface réseau est modifiée.

Par conséquent, la première étape permettant d'intégrer dans une application la capacité de traiter une connexion volatile consiste à gérer l'événement NetworkChange.NetworkAddressChangedEvent. Pour gérer cet événement, la classe App est la plus appropriée. Elle joue le rôle de point d'entrée d'une application Silverlight. Par défaut, cette classe sera implémentée par App.xaml.cs (pour ceux qui écrivent en C#) ou par App.xaml.vb (pour ceux qui écrivent en VB.NET). À partir de maintenant, nous utiliserons des exemples en C#. Le gestionnaire d'événements Application_StartUp semble être l'endroit judicieux pour l'abonnement :

private void Application_Startup(object sender, StartupEventArgs e)
{
  NetworkChange.NetworkAddressChanged += new
    NetworkAddressChangedEventHandler(NetworkChange_ 
    NetworkAddressChanged);
  this.RootVisual = newMainPage();
}

Nous avons besoin de l'instruction using suivante :

using System.Net.NetworkInformation;

Le gestionnaire d'événements NetworkChange_NetworkAddressChanged contient l'essentiel de la détection réseau. Voici un exemple d'implémentation :

void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
  this.isConnected = (NetworkInterface.GetIsNetworkAvailable());
  ConnectionStatusChangedHandler handler = 
    this.ConnectionStatusChangedEvent;
  if (handler != null)
  {
    handler(this.isConnected);
  }
}

Le premier appel consiste à vérifier s'il existe une connexion réseau à l'aide de la méthode GetIsNetworkAvailable. Dans cet exemple, le résultat est stocké dans un champ exposé via une propriété. Un événement est déclenché en vue de son utilisation par d'autres parties de l'application :

private bool isConnected = (NetworkInterface.GetIsNetworkAvailable());

public event ConnectionStatusChangedHandlerConnectionStatusChangedEvent;

public bool IsConnected
{
  get
  { 
    return isConnected;
  }
}

L'exemple suivant contient la structure de base de la détection et de la gestion de la connectivité réseau actuelle. Cependant, bien que la méthode GetIsNetworkAvailable renvoie true dès que l'ordinateur est connecté à un réseau (autre qu'une interface de bouclage ou de tunnel), il est possible que le réseau soit connecté mais inutile. Cela peut être le cas lorsque l'ordinateur est connecté à un routeur, mais que ce dernier a perdu sa connexion Internet, ou lorsqu'un ordinateur est connecté à un point d'accès Wi-Fi public pour lequel l'utilisateur doit être connecté via un navigateur.

Le fait de savoir qu'il existe une connexion réseau valide ne représente qu'une partie d'une solution solide. Il peut être impossible d'atteindre les services Web utilisés par une application Silverlight pour différentes raisons et il est tout aussi important qu'une application connectée occasionnellement soit capable de gérer cette éventualité.

Différentes approches permettent de vérifier qu'un service Web est disponible. En ce qui concerne les services Web internes, c'est-à-dire les services Web contrôlés par le développeur de l'application Silverlight, il est parfois souhaitable d'ajouter une méthode no-op simple susceptible d'être utilisée périodiquement pour déterminer la disponibilité. Lorsque cela n'est pas possible, dans le cas de services Web tiers par exemple, ou que cela n'est pas souhaitable, le délai d'expiration doit être configuré et géré de façon appropriée. Silverlight 3 utilise un sous-ensemble de la configuration client Windows Communication Foundation ; il est généré automatiquement lors de l'utilisation de l'outil Ajouter une référence de service.

Enregistrement des données

Outre sa capacité à réagir face aux changements de l'environnement réseau, une application doit également pouvoir gérer les données entrées lorsque l'application est hors connexion. Microsoft Sync Framework (msdn.microsoft.com/sync) est une plateforme complète qui propose une prise en charge étendue des types de données, des banques de données, des protocoles et des topologies. Lors de la rédaction de cet article, cette plateforme n'était pas disponible pour Silverlight, mais elle le sera à l'avenir. Pour plus d'informations, regardez les sessions proposées par MIX10 sur live.visitmix.com/MIX10/Sessions/SVC10 ou lisez l'article du blog concernant ce point sur blogs.msdn.com/sync/archive/2009/12/14/offline-capable-applications-using-silverlight-and-sync-framework.aspx. Il est évident que l'utilisation de la plateforme Microsoft Sync Framework constituera la meilleure option lorsqu'elle sera disponible pour Silverlight. En attendant, une solution simple s'impose pour combler le vide.

Une file d'attente observable

Idéalement, les éléments de l'interface utilisateur ne doivent pas se préoccuper du lieu de stockage des données, c'est-à-dire localement ou dans le nuage, si ce n'est lorsqu'il est approprié de signaler à l'utilisateur que l'application est actuellement hors connexion ou en ligne. L'utilisation d'une file d'attente est une bonne façon de créer cette séparation entre l'interface utilisateur et le code du stockage des données. Le composant qui traite la file d'attente doit être capable de réagir aux nouvelles données mises en file d'attente. Tous ces facteurs mènent à une file d'attente observable. La figure 2 présente un exemple d'implémentation d'une file d'attente observable.

Figure 2 Une file d'attente observable

public delegate void ItemAddedEventHandler();

public class ObservableQueue<T>
{
  private readonly Queue<T> queue = new Queue<T>();

  public event ItemAddedEventHandler ItemAddedEvent;

  public void Enqueue(T item)
  {
    this.queue.Enqueue(item);
    ItemAddedEventHandler handler = this.ItemAddedEvent;
    if (handler != null)
    {
      handler();
    }
  }

  public T Peek()
  {
    return this.queue.Peek();
  }

  public T Dequeue()
  {
    return this.queue.Dequeue();
  }

  public ArrayToArray()
  {
    return this.queue.ToArray();
  }

  public int Count
  {
    get
    {
      return this.queue.Count;
    }
  }
}

Cette classe simple encapsule une file d'attente standard et génère un événement lors de l'ajout des données. Il s'agit d'une classe générique qui ne fait aucune supposition sur le format ou le type des données qui seront ajoutées. Une file d'attente observable est utile uniquement lorsque quelque chose l'observe. Dans ce cas, le quelque chose en question est une classe nommée QueueProcessor. Avant d'examiner le code de QueueProcessor, il reste un point à mentionner : le traitement en arrière-plan. Lorsque QueueProcessor est averti que de nouvelles données ont été ajoutées à la file d'attente, il doit traiter les données sur un thread en arrière-plan pour que l'interface utilisateur reste réactive. Pour atteindre cet objectif conceptuel, la classe BackgroundWorker, qui est définie dans l'espace de noms System.ComponentModel, est idéale.

BackgroundWorker

BackgroundWorker représente un moyen pratique d'exécuter des opérations sur un thread en arrière-plan. Ce composant expose deux événements, ProgressChanged et RunWorkerCompleted, qui permettent d'informer une application de la progression d'une tâche BackgroundWorker. L'événement DoWork est déclenché lors de l'appel de la méthode BackgroundWorker.RunAsync. L'exemple suivant définit un composant BackgroundWorker :

private void SetUpBackgroundWorker()
{
  backgroundWorker = new BackgroundWorker();
  backgroundWorker.WorkerSupportsCancellation = true;
  backgroundWorker.WorkerReportsProgress = true;
  backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
  backgroundWorker.ProgressChanged += new
    ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
  backgroundWorker.RunWorkerCompleted += new
    RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
}

Notez que le code de ce gestionnaire d'événements pour l'événement DoWork doit être vérifié périodiquement afin de voir si une annulation est en attente. Pour plus d'informations, consultez la documentation MSDN sur msdn.microsoft.com/library/system.componentmodel.backgroundworker.dowork%28VS.95%29.

IWorker

Pour respecter la nature générique de la file d'attente ObservableQueue, il serait approprié de séparer la définition du travail à faire de la création et de la configuration du BackgroundWorker. Une interface simple, Iworker, définit le gestionnaire d'événements de DoWork. L'objet expéditeur de la signature du gestionnaire d'événements sera le BackgroundWorker, qui permet aux classes implémentant l'interface IWorker de signaler la progression et de vérifier les annulations en attente. Voici la définition de l'interface IWorker :

public interface IWorker
{
  void Execute(object sender, DoWorkEventArgs e);
}

Il est facile de se laisser aller lors de la conception et de faire des séparations alors qu'elles ne sont pas du tout nécessaires. C'est l'expérience pratique qui est à l'origine de la création de l'interface IWorker. La file d'attente ObservableQueue, telle qu'elle est présentée dans cet article, était sans aucun doute censée faire partie d'une solution pour une application connectée occasionnellement. Cependant, il s'est avéré que d'autres tâches, telles que l'importation de photos d'un appareil photo numérique, étaient également implémentées plus facilement avec une file d'attente ObservableQueue. Par exemple, lorsque les chemins d'accès aux photos sont placés sur une file d'attente de ce type, l'implémentation de l'interface IWorker peut traiter les images en arrière-plan. La nature générique de la file d'attente ObservableQueue et la création de l'interface IWorker ont rendu ces scénarios possibles tout en gardant le problème initial présent à l'esprit.

Traitement de la file d'attente

QueueProcessor est la classe qui relie l'implémentation de la file d'attente ObservableQueue et de l'interface IWorker pour qu'elles agissent de façon utile. Le traitement de la file d'attente revient à définir un composant BackgroundWorker, ce qui inclut la définition de la méthode Execute de l'interface IWorker comme gestionnaire d'événements de l'événement BackgroundWorker.DoWork et l'abonnement à l'événement ItemAddedEvent. La figure 3 présente un exemple d'implémentation de QueueProcessor.

Figure 3 Implémentation de QueueProcessor

public class QueueProcessor<T>
{
  private BackgroundWorker backgroundWorker;
  private readonly IWorker worker;

  public QueueProcessor(ObservableQueue<T>queueToMonitor, IWorker worker)
  {
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
       ConnectionStatusChangedEventHandler(QueueProcessor_
         ConnectionStatusChangedEvent);
    queueToMonitor.ItemAddedEvent += new
      ItemAddedEventHandler(PendingData_ItemAddedEvent);
    this.worker = worker;
    SetUpBackgroundWorker();
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy) 
      && (((SampleCode.App)Application.Current).PendingData.Count>0))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void PendingData_ItemAddedEvent()
  {
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void SetUpBackgroundWorker()
  {
    backgroundWorker = new BackgroundWorker();
    backgroundWorker.WorkerSupportsCancellation = true;
    backgroundWorker.WorkerReportsProgress = true;
    backgroundWorker.DoWork += new DoWorkEventHandler(this.worker.Execute);
    backgroundWorker.ProgressChanged += new
      ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
    backgroundWorker.RunWorkerCompleted += new
      RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
  }

  private void backgroundWorker_RunWorkerCompleted(object sender,
    RunWorkerCompletedEventArgs e)
  {
    if (e.Cancelled)
    {
      // Handle cancellation
    }
      else if (e.Error != null)
      {
        // Handle error
      }
    else
    {
      // Handle completion if necessary
    }
  }

  private void backgroundWorker_ProgressChanged(object sender, 

    ProgressChangedEventArgs e)
  {
     // Raise event to notify observers
  }

  private void QueueProcessor_ConnectionStatusChangedEvent(bool isConnected)
  {
    if (isConnected)
    {
      if (!backgroundWorker.IsBusy)
      {
        backgroundWorker.RunWorkerAsync();
      }
    }
    else
    {
      backgroundWorker.CancelAsync();
    }
  }
}

L'exemple de code de la figure 3 laisse l'utilisateur pratiquer à titre d'exercice l'implémentation de certaines des fonctionnalités de BackgroundWorker, telles que le traitement des erreurs. La méthode RunAsync est appelée uniquement si l'application est actuellement connectée et si BackgroundWorker n'est pas déjà occupé à traiter la file d'attente.

UploadWorker

Le constructeur de QueueProcessor requiert une interface IWorker, ce qui signifie bien évidemment qu'une implémentation concrète est nécessaire pour le téléchargement des données dans le nuage. UploadWorker semble être un nom judicieux pour une classe de ce type. La figure 4 représente un exemple d'implémentation.

Figure 4 Téléchargement de données dans le nuage

public class UploadWorker :  IWorker
{
  public override void Execute(object sender, DoWorkEventArgs e)
  {
    ObservableQueue<DataItem>pendingData = 
      ((SampleCode.App)Application.Current).PendingData;
    while (pendingData.Count>0)
    {
      DataItem item = pendingData.Peek();
      if (SaveItem(item))
      {
        pendingData.Dequeue();
      }
    }
  }

  private bool SaveItem(DataItem item)
  {
    bool result = true;
    // Upload item to webservice
    return result;
  }
}

Dans la figure 4, la méthode Execute télécharge les éléments mis en file d'attente. S'ils ne peuvent pas être téléchargés, ils restent dans la file d'attente. Notez que si l'accès au BackgroundWorker est obligatoire, par exemple pour signaler la progression ou vérifier la présence d'annulations en attente, l'objet expéditeur est le BackgroundWorker. Si un résultat est attribué à la propriété Result de « e », DoWorkEventArgs, il sera disponible dans le gestionnaire d'événements RunWorkerCompleted.

Stockage isolé

Vous pouvez mettre des données dans une file d'attente et soumettre uniquement celles-ci à un service Web pendant que vous êtes connecté (afin de les stocker dans le nuage), à condition que l'application ne soit jamais fermée. Si l'application est fermée alors qu'il reste des données en attente dans la file d'attente, une stratégie est nécessaire pour stocker ces données jusqu'au prochain chargement de l'application. Dans ce cas de figure, Silverlight propose le stockage isolé.

Le stockage isolé est un système de fichiers virtuel disponible pour les applications Silverlight qui permet le stockage local des données. Il peut stocker une quantité limitée de données (la limite par défaut est de 1 Mo), mais une application peut demander de l'espace supplémentaire à l'utilisateur. Dans le cadre de notre exemple, la sérialisation de la file d'attente dans le stockage isolé enregistrera l'état de la file d'attente entre les sessions de l'application. Une classe simple nommée QueueStore fera l'affaire, comme illustré à la figure 5.

Figure 5 Sérialisation d'une file d'attente dans le stockage isolé

public class QueueStore
{
  private const string KEY = "PendingQueue";
  private IsolatedStorageSettings appSettings = 
    IsolatedStorageSettings.ApplicationSettings;

  public void SaveQueue(ObservableQueue<DataItem> queue)
  {
    appSettings.Remove(KEY);
    appSettings.Add(KEY, queue.ToArray());
    appSettings.Save();
  }

  public ObservableQueue<DataItem>LoadQueue()
  {
    ObservableQueue<DataItem> result = new ObservableQueue<DataItem>();
    ArraysavedArray = null;

    if (appSettings.TryGetValue<Array>(KEY, out savedArray))
    {
      foreach (var item in savedArray)
      {
        result.Enqueue(item as DataItem);
      }
    }

  return result;
  }
}

À condition que les éléments de la file d'attente soient sérialisables, QueueStore vous permettra d'enregistrer et de charger les files d'attente. L'appel de la méthode Save dans la méthode Application_Exit de App.xaml.cs et celui de la méthode Load dans la méthode Application_Startup de App.xaml.cs permet à l'application d'enregistrer l'état entre les sessions.

Les éléments ajoutés à la file d'attente étaient de type DataItem. Cette classe simple représente les données. Dans le cas d'un modèle de données légèrement plus riche, DataItem peut contenir un graphique d'objet simple. Dans des scénarios plus complexes, DataItem peut être une classe de base dont les autres classes hériteront.

Extraction de données

Pour qu'une application soit utilisable lorsqu'elle est uniquement connectée à certains moments, elle doit disposer d'une méthode de mise en cache locale des données. Pour cela, il convient d'abord de tenir compte de la taille de l'ensemble des données de travail requis par une application. Dans le cas d'une application simple, le cache local peut contenir toutes les données requises par l'application. Par exemple, une application de lecteur simple qui utilise des flux RSS peut avoir besoin de mettre en cache uniquement les flux et les préférences utilisateur. Dans le cas d'autres applications, l'ensemble de travail peut être trop important ou il peut simplement être trop difficile de prévoir les données nécessaires à l'utilisateur, ce qui augmente réellement la taille de l'ensemble des données de travail.

Il est plus aisé de commencer par une application simple. Les données nécessaires lors de la session de l'application, par exemple les préférences utilisateur, peuvent être téléchargées au démarrage de l'application et conservées en mémoire. Si ces données sont modifiées par l'utilisateur, les stratégies de téléchargement dont nous avons déjà discuté peuvent être appliquées. Cependant, cette approche suppose que l'application soit connectée au démarrage, ce qui n'est pas toujours le cas. À nouveau, le stockage isolé est la bonne réponse et les exemples présentés précédemment prendront en charge ce scénario si vous ajoutez un appel permettant de télécharger la version serveur des données lorsque c'est nécessaire. Gardez présent à l'esprit que l'utilisateur peut avoir une application installée sur un ordinateur situé au bureau et sur un autre se trouvant à son domicile. Le moment approprié pourrait donc être lorsque l'utilisateur reprend l'application après une pause importante.

Un autre scénario peut impliquer une application simple qui affiche des données relativement statiques, par exemple des actualités. Une stratégie similaire peut être appliquée : téléchargez les données dès que possible, conservez-les en mémoire et dans le stockage isolé lors de l'arrêt de l'application. Rechargez-les ensuite au démarrage depuis le stockage isolé. Lorsqu'une connexion est disponible, les données mises en cache peuvent être invalidées et actualisées. Lorsque la connexion est disponible pendant plus de quelques minutes, l'application doit actualiser périodiquement les données telles que des actualités. Comme nous l'avons vu précédemment, l'interface utilisateur ne doit pas avoir conscience de ce travail en arrière-plan.

En cas de téléchargement d'actualités, le point de départ est une classe simple NewsItem :

public class NewsItem
{
  public string Headline;
  public string Body;

  public override stringToString()
  {
    return Headline;
  }
}

Cette classe est simplifiée à l'excès dans le cadre de cet exemple et ToString est remplacé afin de faciliter la liaison dans l'interface utilisateur. Pour stocker les actualités téléchargées, il est nécessaire d'utiliser une classe simple Repository destinée à télécharger les actualités en arrière-plan, comme illustré à la figure 6.

Figure 6 Stockage des actualités téléchargées dans une classe Repository

public class Repository
{
  private ObservableCollection<NewsItem> news = 
    new ObservableCollection<NewsItem>();
  private DispatcherTimer timer = new DispatcherTimer();
  private const int TIMER_INTERVAL = 1;

public Repository()
{
  ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += 
    new ConnectionStatusChangedHandler(Repository_
    ConnectionStatusChangedEvent);
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
    StartTimer();
  }
}

private void Repository_ConnectionStatusChangedEvent(bool isConnected)
{
  if (isConnected)
  {
    StartTimer();
  }
  else
  {
    StopTimer();
  }
}

private void StopTimer()
{
  this.timer.Stop();
}

private void StartTimer()
{
  this.timer.Interval = TimeSpan.FromMinutes(1);
  this.timer.Tick += new EventHandler(timer_Tick);
  this.timer.Start();
}

voidtimer_Tick(object sender, EventArgs e)
{
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
  }
}

private void RetrieveNews()
{
  // Get latest news from server
  List<NewsItem> list = GetNewsFromServer();
  if (list.Count>0)
  {
    lock (this.news)
    {
      foreach (NewsItem item in list)
      {
        this.news.Add(item);
      }
    }
  }
}

private List<NewsItem>GetNewsFromServer()
{
  // Simulate retrieval from server
  List<NewsItem> list = new List<NewsItem>();
  for (int i = 0; i <5; i++)
  {
    NewsItemnewsItem = new NewsItem()
    { Headline = "Something happened at " + 
        DateTime.Now.ToLongTimeString(),
        Body = "On " + DateTime.Now.ToLongDateString() + 
        " something happened.  We'll know more later." };
      list.Add(newsItem);
    }
    return list;
  }

  public ObservableCollection<NewsItem> News
  {
    get
    {
      return this.news;
    }
    set
    {
      this.news = value;
    }
  }
}

Dans la figure 6, l'extraction des actualités est simulée par souci de concision. La classe Repository s'abonne à l'événement ConnectionStatusChangedEvent. De plus, lorsqu'elle est connectée, elle utilise une minuterie DispatcherTimer pour extraire les actualités selon les intervalles spécifiés. Une minuterie DispatcherTimer est utilisée avec une collection ObservableCollection afin de permettre la liaison simple des données. Cette minuterie est intégrée à la file d'attente Dispatcher, elle est donc exécutée sur le thread de l'interface utilisateur. La mise à jour d'une collection ObservableCollection provoque la génération d'un événement afin qu'un contrôle lié de l'interface utilisateur soit automatiquement mis à jour, ce qui est idéal dans le cas du téléchargement d'actualités. Une minuterie System.Threading.Timer est disponible dans Silverlight, mais elle n'est pas exécutée sur le thread de l'interface utilisateur. Dans ce cas, toute opération qui accède aux objets du thread de l'interface utilisateur doit être appelée à l'aide de Dispatcher.BeginInvoke.

L'utilisation de Repository requiert uniquement une propriété dans App.xaml.cs. Étant donné que la classe Repository s'abonne à l'événement ConnectionStatusChangedEvent dans son constructeur, Application_StartupeventinApp.xaml.cs est le meilleur endroit pour l'instancier.

Il y a peu de chances que des données telles que les préférences utilisateur soient modifiées par une personne autre que l'utilisateur pendant que l'application est hors connexion, bien que cela soit tout à fait possible dans le cas d'applications utilisées sur plusieurs périphériques par le même utilisateur. Les données telles que les articles ne sont pas amenées à changer non plus. Cela signifie que les données mises en cache ont des chances d'être valides et que les problèmes de synchronisation sont peu probables lors de la reconnexion. Cependant, dans le cas de données volatiles, une approche différente peut être nécessaire. Par exemple, il peut être intéressant d'informer l'utilisateur de la dernière extraction des données pour qu'il puisse réagir de façon appropriée. Si l'application doit prendre des décisions en fonction des données volatiles, des règles d'expiration sont nécessaires, ainsi que la notification de l'utilisateur que les données obligatoires ne sont pas disponibles en raison de la mise hors connexion de l'application.

Si les données peuvent être modifiées, la comparaison du moment et de l'emplacement de la modification permettra la résolution des conflits dans la plupart des cas. Par exemple, une approche optimiste suppose que la version la plus récente est la plus valide et gagne par conséquent en cas de conflit, mais vous pouvez aussi décider de résoudre un conflit en fonction de l'emplacement de mise à jour des données. Il est essentiel de connaître la topologie de l'application et les scénarios d'utilisation pour adopter la bonne approche. Lorsque les versions conflictuelles ne peuvent pas ou ne doivent pas être résolues, un journal signalant ces versions doit être stocké et les utilisateurs appropriés avertis pour qu'ils puissent se faire une opinion.

Vous devrez également réfléchir à l'emplacement de la logique de synchronisation. La conclusion la plus simple est qu'elle doit résider sur le serveur pour qu'elle puisse arbitrer les versions conflictuelles. Dans ce cas, UploadWorker a besoin d'une modification mineure lui permettant de mettre à jour DataItem en fonction de la dernière version du serveur. Comme indiqué précédemment, la plateforme Microsoft Sync Framework gérera un grand nombre de ces problèmes en dernier recours, ce qui donne toute liberté au développeur pour se concentrer sur le domaine de l'application.

Assemblage des parties

Avec toutes les parties mentionnées, il est facile de créer une application Silverlight simple qui explore ces fonctionnalités dans un environnement connecté occasionnellement. La figure 7 présente une capture d'écran de ce type d'application.

image: Sample Application for Demonstrating Network Status and Queues
Figure 7 Exemple d'application illustrant les états du réseau et les files d'attente

Dans l'exemple de la figure 7, l'application Silverlight est exécutée hors navigateur. Étant donnée la nature des applications connectées occasionnellement, nombre d'entre elles seront probablement exécutées hors navigateur car elles sont facilement accessibles pour l'utilisateur à partir du menu démarrer et du bureau et elles peuvent être exécutées quelle que soit la connexion réseau. En revanche, une application Silverlight exécutée dans le contexte d'un navigateur requiert une connexion réseau pour que le serveur Web sur lequel elle réside puisse servir la page Web et l'application Silverlight.

Bien qu'elle ne soit certainement pas digne d'une récompense pour sa conception, l'interface utilisateur simple illustrée à la figure 7 vous permet de pratiquer l'exemple de code. Outre l'affichage de l'état actuel du réseau, il contient des champs permettant la saisie des données et une zone de liste liée à la propriété News de Repository. L'exemple XAML permettant de créer l'écran est illustré à la figure 8. Le code-behind est illustré à la figure 9.

Figure 8 XAML de l'exemple d'interface utilisateur pour l'application

<UserControl x:Class="SampleCode.MainPage" 
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:dataInput="clr-namespace:System.Windows.Controls;
    assembly=System.Windows.Controls.Data.Input" 
    Width="400" Height="300">
      <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="12" />
          <ColumnDefinition Width="120" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="12" />
          <RowDefinition Height="64" />
          <RowDefinition Height="44" />
          <RowDefinition Height="34" />
          <RowDefinition Height="34" />
          <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Ellipse Grid.Row="1" Grid.Column="1" Height="30" HorizontalAlignment="Left" 
          Name="StatusEllipse" Stroke="Black" StrokeThickness="1" 
          VerticalAlignment="Top" Width="35" />
        <Button Grid.Row="4" Grid.Column="1" Content="Send Data" Height="23" 
          HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top" 
          Width="75" Click="button1_Click" />
        <TextBox Grid.Row="2" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="VehicleTextBox" VerticalAlignment="Top" Width="210" />
        <TextBox Grid.Row="3" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="TextTextBox" VerticalAlignment="Top" Width="210" />
        <dataInput:Label Grid.Row="2" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="VehicleLabel" VerticalAlignment="Top" 
          Width="120" Content="Title" />
        <dataInput:Label Grid.Row="3" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="TextLabel" VerticalAlignment="Top" 
          Width="120" Content="Detail" />
        <ListBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Height="100" 
          HorizontalAlignment="Left" Name="NewsListBox" 
          VerticalAlignment="Top" Width="376" />
      </Grid>
</UserControl>

Figure 9 Code-behind de l'exemple d'interface utilisateur pour l'application

public delegate void DataSavedHandler(DataItem data);

public partial class MainPage : UserControl
{
  private SolidColorBrush STATUS_GREEN = new SolidColorBrush(Colors.Green);
  private SolidColorBrush STATUS_RED = new SolidColorBrush(Colors.Red);

  public event DataSavedHandlerDataSavedEvent;

  public MainPage()
  {
    InitializeComponent();
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
      ConnectionStatusChangedHandler(MainPage_ConnectionStatusChangedEvent);
    IndicateStatus(((NetworkStatus.App)Application.Current).IsConnected);
    BindNews();
  }

  private void MainPage_ConnectionStatusChangedEvent(bool isConnected)
  {
    IndicateStatus(isConnected);
  }

  private void IndicateStatus(bool isConnected)
  {
    if (isConnected)
    {
      StatusEllipse.Fill = STATUS_GREEN;
    }
    else
    {
      StatusEllipse.Fill = STATUS_RED;
    }
  }

  private void BindNews()
  {
    NewsListBox.ItemsSource = 
      ((SampleCode.App)Application.Current).Repository.News;
  }

  private void button1_Click(object sender, RoutedEventArgs e)
  {
    DataItem dataItem = new DataItem
    {
      Title = this.TitleTextBox.Text,
      Detail = this.DetailTextBox.Text
    };
    DataSavedHandler handler = this.DataSavedEvent;
    if (handler != null)
    {
      handler(dataItem);
    }

    this.TitleTextBox.Text = string.Empty;
    this.DetailTextBox.Text = string.Empty;
  }
}

La classe de la figure 9 génère un événement pour indiquer que les données ont été enregistrées. Un observateur (dans le cas présent, App.xaml.cs) s'abonne à cet événement et met les données dans la file d'attente ObservableQueue.

Une nouvelle catégorie d'application

La prise en charge par Silverlight des applications qui sont connectées uniquement à certains moments permet une nouvelle catégorie d'application. Ces applications sont à l'origine de nouvelles réflexions pour les développeurs qui doivent s'interroger sur le comportement de l'application lorsqu'elle est connectée et lorsqu'elle ne l'est pas. Cet article présente ces considérations et propose des stratégies et un exemple de code permettant de les traiter. Il est évident qu'étant donnée l'étendue des scénarios qui existent dans un monde connecté occasionnellement, les exemples ne sont là qu'à titre de point de départ.

Mark Bloodworth est architecte au sein de l'équipe de développement des plateformes chez Microsoft. Il travaille en collaboration avec des entreprises sur des projets novateurs. Avant de rejoindre Microsoft, il était architecte de solutions senior chez BBC Worldwide, où il dirigeait une équipe chargée de l'architecture et de l'analyse des systèmes. Il a consacré la plus grande partie de sa carrière à l'utilisation des technologies Microsoft, et plus particulièrement de la plateforme Microsoft .NET Framework, avec une pincée de Java pour équilibrer les choses. Il tient un blog sur remark.wordpress.com.

Dave Brown travaille depuis plus de neuf ans pour Microsoft, où il a commencé à exercer au sein du service de conseils pour les technologies Internet. Il travaille actuellement dans l'équipe de développement des plateformes au Royaume-Uni comme architecte pour le Microsoft Technology Centre. Ce travail l'amène à partager son temps entre l'analyse métier des scénarios clients, la conception de l'architecture des solutions et la gestion ainsi que le développement du code pour les solutions de validation technique. Son blog est disponible sur drdave.co.uk/blog.

Je remercie l'expert technique suivant : Ashish Shetty