Silverlight en línea

Silverlight en un mundo conectado ocasionalmente

Mark Bloodworth

Descargar el ejemplo de código

Las personas viven en un mundo en línea, o al menos algunos lo hacemos, algunas veces. En algún momento en el futuro, es posible que haya una conectividad dominante y siempre activa con más ancho de banda del necesario, pero no actualmente. En realidad, estamos conectados de manera ocasional y con el ancho de banda suficiente. En pocas ocasiones sabemos el estado en que estamos en un momento determinado.

Para diseñar aplicaciones que puedan entregar la mejor experiencia de usuario en esta realidad, existen diversas opciones de arquitectura a considerar.

Los clientes inteligentes, tanto ricos como pobres, comparten un atributo común en cuanto a que están implementados en un equipo local. Como tal, es intrínsicamente posible ejecutar esas aplicaciones sin estar conectados a la red. Por otro lado, una aplicación tradicional basada en explorador debe conectarse a su servidor web remoto para su ejecución.

Entre estos dos extremos existe una gama siempre creciente de opciones. Todas ofrecen distintas capacidades para ejecutar una aplicación sin conexión, junto con diversos grados de flexibilidad e interactividad para el diseño de la interfaz de usuario (UI) e imponen niveles diferentes de restricciones de seguridad. Analizaremos la encarnación más reciente de aplicaciones conectadas ocasionalmente, las cuales vienen con una experiencia de usuario altamente interactiva y se pueden ejecutar dentro o fuera de un explorador. Presentaremos ejemplos de código que tratan con la detección de conectividad de la red, junto con subprocesos de trabajo en segundo plano que cargan y descargan datos al estar en línea.

Contexto

Imaginemos la evolución de una aplicación típica pertinente para tratar este asunto. Nuestro ejemplo comenzó como una simple aplicación cliente pesada que sólo se ejecutaba en sistemas operativos de Windows. Aunque permitía que los usuarios trabajaran sin conexión, las limitaciones de la solución inicial fueron cada vez más evidentes:

  • Había un requisito adicional para ser compatible con varios sistemas operativos. Sólo un subconjunto de los usuarios potenciales podían ser compatibles en esta primera versión.
  • Problemas de implementación causaron discrepancias en las versiones instaladas por la base de usuarios.

Con una presión cada vez mayor para ser compatible con una solución más liviana que trabajara en varios sistemas operativos y con los problemas de implementación minimizados, la aplicación se volvió a escribir como una simple aplicación cliente ligera en HTML. Sin embargo, esto condujo a otro tipo de problemas:

  • Sus capacidades de UI eran limitadas, resultando en una experiencia poco intuitiva.
  • Necesitaba largas pruebas de compatibilidad de explorador.
  • El desempeño sobre muchas infraestructuras de redes de usuarios era insuficiente. Por ejemplo, se debían descargar grandes cantidades de datos de referencia cada vez que un usuario necesitaba rellenar un formulario, junto con extensos scripts para proveer la lógica involucrada en la validación.
  • Los usuarios no podían usar la aplicación sin conexión.

Claramente, esa versión tampoco prosperó.

En este caso la solución ideal, aunque esquiva, es una aplicación de Internet enriquecida (RIA) con una UI intuitiva y flexible. La aplicación necesita dejar a los usuarios administrar grandes cantidades de datos y realizar cargas asincrónicas de datos y validación de datos al estar en línea, sin bloquear la UI. Debería ser compatible con el trabajo sin conexión y obtener acceso a un almacén de datos en el cliente. Debería integrarse con dispositivos de hardware en el cliente, tales como cámaras. Finalmente, esta solución ideal debería iniciarse desde el menú Inicio o desde un icono de aplicación, y existir fuera de los confines de un explorador web.

Silverlight puede cumplir con estos requisitos. Silverlight 3 introdujo el concepto de una experiencia fuera del explorador, y esto se ha extendido a Silverlight 4. Además, Silverlight 4 introdujo la capacidad de interactuar con carpetas específicas, tales como “Mis imágenes,” y con dispositivos de hardware tales como cámaras web (las aplicaciones que usan esta funcionalidad mejorada informarán al usuario que la aplicación requiere de confianza elevada y requieren el consentimiento del usuario antes de instalar la aplicación. Para obtener más información sobre aplicaciones de confianza, consulte este artículo: msdn.microsoft.com/library/ee721083(v=VS.95)). En este artículo, nos centraremos en problemas comunes que se encuentran al diseñar una aplicación que es compatible tanto con el trabajo en línea como sin conexión.

La figura 1 muestra una arquitectura improvisada que sirve como un buen candidato.

image: Candidate High-Level Architecture
Figura 1 Arquitectura candidata de alto nivel

Los escenarios típicos del usuario para este caso incluyen:

  • Un trabajador móvil con un equipo portátil. El equipo portátil puede tener una tarjeta 3G o puede conectarse a una red inalámbrica en una oficina o en una zona activa con Internet.
  • Un usuario con un equipo de escritorio en un entorno con conectividad limitada, como un edificio de oficinas más antiguo o prefabricado.

Detección del estado de la red

Una aplicación de la que se espera que opere en un entorno conectado ocasionalmente debe ser capaz de comprobar el estado actual de la conexión de red. Silverlight 3 presenta esta capacidad con el método NetworkInterface. GetIsNetworkInterfaceAvailable. Tales aplicaciones también pueden hacer uso de NetworkChange.NetworkAddressChangedEvent, que se activa cuando cambia la dirección IP de una interfaz de red.

Así, el primer paso para equipar una aplicación para tratar con una conexión volátil es controlar el NetworkChange.NetworkAddressChangedEvent. El lugar obvio para controlar este evento es en la clase App que actúa como un punto de entrada a la aplicación Silverlight. Esta clase será implementada de manera predeterminada por App.xaml.cs (para aquellos que escriben en C#) o por App.xaml.vb (para aquellos que escriben en VB.NET). De aquí en adelante, usaremos ejemplos en C#. El controlador de eventos Application_StartUp parece ser el lugar razonable para suscribirse:

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

Necesitamos la siguiente instrucción de uso:

using System.Net.NetworkInformation;

El controlador de eventos NetworkChange_NetworkAddressChanged contiene lo sustancial de la detección de red. Vea una implementación de muestra:

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

La primera llamada es para GetIsNetworkAvailable para ver si hay una conexión de red. En este ejemplo, el resultado se almacena en un campo que se expone por medio de una propiedad, y se activa un evento para otras partes de la aplicación a usar:

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

public event ConnectionStatusChangedHandlerConnectionStatusChangedEvent;

public bool IsConnected
{
  get
  { 
    return isConnected;
  }
}

Este ejemplo tiene el esqueleto para detectar y controlar la conectividad de red actual. Sin embargo, aunque GetIsNetworkAvailable devuelva un valor “true” cuando el equipo se conecta a una red (sin ser de bucle invertido o interfaz de túnel), la red puede estar conectada pero no ser útil. Este puede ser el caso cuando el equipo está conectado a un enrutador pero este ha perdido su conexión a Internet, o cuando el equipo está conectado a un punto de acceso público Wi-Fi que requiere que el usuario inicie sesión por medio de un explorador.

Saber que existe una conexión de red válida sólo es una parte de una solución sólida. Los servicios web que una aplicación Silverlight consume pueden estar inaccesibles por muchos motivos, y es igualmente importante que una aplicación conectada ocasionalmente pueda tratar esta eventualidad.

Existen varios enfoques para comprobar que un servicio web está disponible. Para servicios web de origen, esto es, servicios web que están bajo el control del desarrollador de la aplicación Silverlight, puede ser deseable agregar un simple método que no tiene ningún efecto y que se puede usar periódicamente para determinar la disponibilidad. Donde esto no es posible o no es deseable, por ejemplo, en el caso de servicios web de terceros, el tiempo de espera debería ser configurado y controlado de manera apropiada. Silverlight 3 usa un subconjunto de la configuración de cliente de Windows Communication Foundation, se genera automáticamente cuando se usa la herramienta Agregar referencia de servicio.

Almacenamiento de datos

Junto con ser capaz de reaccionar a cambios en el entorno de la red, una aplicación también necesita tratar con datos que se ingresan cuando la aplicación está sin conexión. Microsoft Sync Framework (msdn.microsoft.com/sync) es una completa plataforma con compatibilidad amplia para tipos de datos, almacenes de datos, protocolos y topologías. En el momento de escribir este artículo, no está disponible para Silverlight, aunque si lo estará. Vea la sesión de MIX10 en live.visitmix.com/MIX10/Sessions/SVC10 o lea la publicación en el blog: blogs.msdn.com/sync/archive/2009/12/14/offline-capable-applications-using-silverlight-and-sync-framework.aspx para obtener más información. Claramente, usar Microsoft Sync Framework será la mejor opción cuando esté disponible para Silverlight. Mientras tanto, se necesita una solución simple para salvar la brecha.

Una cola observable

Idealmente, los elementos de UI no necesitan preocuparse si los datos están siendo almacenados localmente o en la nube, sino sólo cuando es apropiado dar una señal al usuario de que la aplicación está sin conexión o en línea. Usar una cola es una buena manera de crear esta separación entre la UI y el código de almacenamiento de datos. El componente que está procesando la cola necesita ser capaz de reaccionar a los nuevos datos que están entrando en la cola. Todos esos factores llevan hacia una cola observable. La figura 2 muestra una implementación de muestra de una cola observable.

Figura 2 Una cola 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;
    }
  }
}

Esta sencilla clase ajusta una cola estándar y genera un evento cuando se agregan datos. Es una clase genérica que no hace suposiciones sobre el formato o el tipo de datos que serán agregados. Una cola observable es útil sólo cuando existe algo que la observa. En este caso, ese algo es una clase llamada QueueProcessor. Antes de mirar el código para el QueueProcessor, se debe considerar una cosa más: el procesamiento en segundo plano. Cuando se notifica al QueueProcessor que se han agregado nuevos datos a la cola, debería procesar los datos en un subproceso en segundo plano para que la UI se mantenga con capacidad de respuesta. Para alcanzar este objetivo de diseño es ideal la clase BackgroundWorker, que se define en el espacio de nombre System.ComponentModel.

BackgroundWorker

BackgroundWorker es una manera conveniente de ejecutar operaciones en un subproceso en segundo plano. Expone dos eventos, ProgressChanged y RunWorkerCompleted, que proporcionan los medios para informar a una aplicación sobre los progresos de una tarea de BackgroundWorker. El evento DoWork se activa al llamar el método BackgroundWorker.RunAsync. Aquí hay un ejemplo de muestra que establece un 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);
}

Observe que el código en el controlador de eventos para el evento DoWork debería comprobar periódicamente si está pendiente una cancelación. Consulte la documentación de MSDN en msdn.microsoft.com/library/system.componentmodel.backgroundworker.dowork%28VS.95%29 para obtener más detalles.

IWorker

De acuerdo con la naturaleza genérica de ObservableQueue, sería bueno separar la definición del trabajo que se va a realizar de la creación y la configuración del BackgroundWorker. IWorker, una interfaz sencilla, define el controlador de eventos para DoWork. El objeto remitente en la firma del controlador de eventos será el BackgroundWorker, el que habilita a las clases implementando la interfaz IWorker para informar el progreso y comprobar una cancelación pendiente. Aquí está la definición de IWorker:

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

Es fácil dejarse tentar en el diseño y hacer separaciones donde no son necesarias. La idea para crear la interfaz IWorker vino de la experiencia práctica. La ObservableQueue, como se presenta en este artículo, fue pensada para ser parte de una solución para una aplicación conectada ocasionalmente. Sin embargo, resultó ser que otras tareas, tales como importar fotografías desde una cámara digital, se implementaban de una manera más fácil con una ObservableQueue. Por ejemplo, cuando las rutas a las fotografías se colocan en una ObservableQueue, una implementación de IWorker puede procesar las imágenes en segundo plano. Hacer la ObservableQueue genérica y crear la interfaz IWorker habilitó tales escenarios, a la vez que seguía abordando el problema original.

Procesamiento de la cola

QueueProcessor es la clase que vincula la ObservableQueue y la implementación IWorker y las reúne para hacer algo útil. Procesar la cola es materia de configuración de un BackgroundWorker, que incluye el establecimiento del método Execute del IWorker como el controlador de eventos para el evento BackgroundWorker.DoWork y suscribirse al ItemAddedEvent. La figura 3 muestra una implementación de muestra de QueueProcessor.

Figura 3 Implementación 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();
    }
  }
}

El código de ejemplo en la figura 3 deja la implementación de algunas de las capacidades de BackgroundWorker, como el control de errores, como ejercicios para el lector. El método RunAsync se invoca sólo si la aplicación está actualmente conectada y el BackgroundWorker no está ocupado aún en el procesamiento de la cola.

UploadWorker

El constructor del QueueProcessor necesita un IWorker, lo que por supuesto significa que se necesita de una implementación completa para cargar datos a la nube. UploadWorker parece ser un nombre adecuado para tal clase. En la figura 4 se muestra una implementación de muestra.

Figura 4 Carga de datos a la nube

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;
  }
}

En la figura 4, el método Execute carga elementos en la cola. Si no pueden ser cargados, permanecen en la cola. Observe que si se requiere obtener acceso a BackgroundWorker, por ejemplo, para informar el progreso o para comprobar una cancelación pendiente, el objeto remitente es el BackgroundWorker. Si a la propiedad Result se asigna un resultado de e, el DoWorkEventArgs, estará disponible en el controlador de eventos RunWorkerCompleted.

Almacenamiento aislado

Colocar datos en una cola y enviar sólo esos datos a un servicio web mientras está conectado (para que puedan ser almacenados en la nube) está bien, siempre que la aplicación nunca se cierre. En la eventualidad en que la aplicación se cierre mientras permanecen datos pendientes en la cola, se necesita de una estrategia para almacenar los datos hasta la próxima vez que la aplicación se cargue. Silverlight proporciona Almacenamiento aislado para esta situación.

Almacenamiento aislado es un sistema de archivos virtual disponible para las aplicaciones Silverlight que permite el almacenamiento local de datos. Puede almacenar una cantidad de datos limitada (el límite predeterminado es 1 MB), pero una aplicación puede pedir más espacio desde el usuario. En nuestro ejemplo, serializar la cola para Almacenamiento aislado guardará el estado de la cola entre sesiones de la aplicación. Una clase sencilla llamada QueueStore hará el truco, como se muestra en la figura 5.

Figura 5 Serializar una cola para Almacenamiento aislado

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;
  }
}

Siempre que los elementos de la cola sean serializables, QueueStore habilitará guardar y cargar colas. Llamar el método Save en el método Application_Exit de App.xaml.cs y el método Load en el método Application_Startup de App.xaml.cs permite que la aplicación guarde el estado entre sesiones.

Los elementos agregados a la cola han sido del tipo DataItem. Esta es una clase sencilla que representa los datos En el caso de un modelo de datos ligeramente más enriquecido, DataItem podría mantener un simple gráfico de objetos. En escenarios más complejos, DataItem podría ser la clase base de la que otras clases heredan.

Recuperación de datos

Para que una aplicación sea usable cuando está conectada sólo a veces, debe tener alguna manera de almacenar datos en caché de manera local. La primera consideración para esto es el tamaño del conjunto de datos de trabajo que necesita una aplicación. En una aplicación simple, puede ser posible que una caché local contenga todos los datos que la aplicación necesita. Por ejemplo, una simple aplicación de lectura que consume fuentes RSS puede tener que almacenar en caché sólo las fuentes y las preferencias del usuario. Para otras aplicaciones, el conjunto de trabajo puede ser muy grande o simplemente puede ser difícil predecir cuáles son los datos que necesita el usuario, lo que aumenta de manera eficiente el tamaño del conjunto de datos de trabajo.

Una aplicación sencilla es un lugar más fácil para comenzar. Los datos que se necesitan a través de toda la sesión de la aplicación, tales como las preferencias del usuario, se pueden descargar al inicio de la aplicación y mantenerse en la memoria. Si el usuario cambiara estos datos, se pueden aplicar las estrategias de carga analizadas anteriormente. Pero este enfoque supone que la aplicación será conectada en el inicio, el que puede no ser el caso. Nuevamente la respuesta es Almacenamiento aislado, y los ejemplos presentados anteriormente llevarán a este escenario, agregando una llamada para descargar la versión de los datos del servidor cuando sea apropiado. Tenga en mente que el usuario puede tener la aplicación instalada tanto en el equipo de trabajo y en su equipo doméstico, así el momento apropiado puede ser cuando un usuario vuelva a la aplicación después de una pausa significativa.

Otro escenario puede involucrar a una aplicación sencilla que está mostrando datos relativamente estáticos, como noticias. Se puede aplicar una estrategia similar: descargar los datos cuando sea posible, mantenerlos en la memoria y en Almacenamiento aislado al cierre de la aplicación (y cargar nuevamente desde Almacenamiento aislado en el inicio). Cuando una conexión está disponible, los datos en caché se pueden invalidar y actualizar. Cuando la conexión está disponible por más de algunos minutos, la aplicación debería actualizar periódicamente datos como noticias. Como se analizó anteriormente, la UI no debería advertir este trabajo en segundo plano.

En el caso de la descarga de noticias, el punto de partida es una sencilla clase NewsItem:

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

  public override stringToString()
  {
    return Headline;
  }
}

Esta clase está muy simplificada para el propósito del ejemplo, y ToString se ha sustituido para hacerlo más fácil de enlazar a la UI. Para almacenar las noticias que se han descargado, se necesita de una sencilla clase Repository, la que descarga las noticias en segundo plano tal como se muestra en la figura 6.

Figura 6 Almacenamiento de noticias descargadas en una clase 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;
    }
  }
}

En la figura 6, la recuperación de noticias está simulada por motivos de espacio. La clase Repository se suscribe a ConnectionStatusChangedEvent y cuando está conectada usa un DispatcherTimer para recuperar noticias a intervalos específicos. Se usa un DispatcherTimer en conjunto con ObservableCollection para habilitar un enlace de datos sencillo. El DispatcherTimer está integrado en la cola de Dispatcher, por lo que se ejecuta sobre el subproceso de la UI. El efecto de actualizar una ObservableCollection es que se genera un evento de tal forma que un control de enlace en la UI se actualizará automáticamente, lo que es ideal en el caso de la descarga de noticias. En Silverlight hay disponible un System.Threading.Timer, pero no se ejecuta sobre el subproceso de la UI. En este caso, cualquier operación que tenga acceso a objetos sobre el subproceso de la UI se debe llamar mediante el uso de Dispatcher.BeginInvoke.

Para usar Repository se requiere sólo de una propiedad en App.xaml.cs. Dado que Repository se suscribe en su constructor al ConnectionStatusChangedEvent, el mejor lugar para crear una instancia es en Application_StartupeventinApp.xaml.cs.

Es poco probable que datos como las preferencias del usuario los cambie otra persona que no sea el usuario mientras la aplicación está desconectada, aunque ciertamente es posible en el caso de aplicaciones que son usadas a través de una gama de dispositivos por el mismo usuario. Datos como los artículos noticiosos también suelen no cambiar. Esto significa que los datos en caché tienen la probabilidad de ser válidos y que es poco probable que existan problemas de sincronización durante la reconexión. Sin embargo, en el caso de datos volátiles, puede que se necesite de un enfoque diferente. Por ejemplo, puede ser sensato informar al usuario el momento en que se recuperaron los datos por última vez para que pueda responder de una manera apropiada. Si la aplicación necesita tomar decisiones basadas en datos volátiles, se necesitan reglas para su caducidad, junto con la notificación al usuario de que los datos requeridos ya no están disponibles debido a que se está sin conexión.

Si se pueden cambiar los datos, comparar cuándo y dónde se cambiaron habilitará en muchos casos la resolución de conflictos. Por ejemplo, un enfoque optimista supondría que la versión más reciente es la más válida y, en consecuencia, gana en cualquier conflicto, pero también es posible decidir resolver el conflicto en base al lugar donde se cargaron los datos. El conocimiento de la topología de la aplicación y el uso de escenarios es la clave para el enfoque correcto. Donde las versiones en conflicto no pueden, o no se deben resolver, se debe almacenar un registro de tales versiones y notificar a los usuarios apropiados para que puedan hacer un juicio.

También necesita considerar en qué lugar colocar la lógica de sincronización. La conclusión más simple es que debe residir en el servidor para que pueda intermediar entre versiones en conflicto; en este caso, el UploadWorker requeriría de una modificación menor, de manera que actualizara DataItem a la versión más reciente del servidor. Como se observó anteriormente, Microsoft Sync Framework finalmente se preocupará de muchos de estos problemas, dejando al desarrollador libre para concentrarse en el dominio de la aplicación.

Ensamblado de las partes

Con todas las partes analizadas, es fácil crear una sencilla aplicación Silverlight que explore estas capacidades en un entorno conectado ocasionalmente. La figura 7 muestra una captura de pantalla de dicha aplicación.

image: Sample Application for Demonstrating Network Status and Queues
Figura 7 Aplicación de ejemplo para demostrar el estado de las redes y las colas

En el ejemplo de la figura 7, la aplicación Silverlight se ejecuta fuera del explorador. Dada la naturaleza de una aplicación que está ocasionalmente conectada, es muy probable que muchos la ejecutarán fuera del explorador, porque es fácilmente accesible desde el menú inicio y desde el escritorio y se puede ejecutar sin importar la conexión de red. En cambio, una aplicación Silverlight que se ejecuta en el contexto de un explorador, necesita de una conexión de red para que el servidor web en el que reside pueda ofrecer la página web y la aplicación Silverlight.

Esta sencilla UI que aparece en la figura 7, que aunque ciertamente no está cerca de ganar un premio de diseño, proporciona un medio para ejercitar el código de ejemplo. Además de mostrar el estado actual de la red, tiene campos para ingresar datos y un cuadro de lista enlazado a la propiedad News del Repository. El XAML de ejemplo para crear la pantalla se muestra en la figura 8. El código subyacente se muestra en la figura 9.

Figura 8 XAML para UI de aplicación de ejemplo

<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>

Figura 9 Código subyacente para UI de aplicación de ejemplo

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 clase en la figura 9 genera un evento para indicar que los datos se han guardado. Un observador (en este caso App.xaml.cs) se suscribe a este evento y coloca los datos en la ObservableQueue.

Una nueva clase de aplicación

La compatibilidad de Silverlight para aplicaciones que están conectadas sólo algunas veces permite una nueva clase de aplicación. Tales aplicaciones presentan nuevas consideraciones que requieren que los desarrolladores piensen sobre cómo la aplicación debería comportarse al estar conectada y desconectada. Este artículo presenta estas consideraciones y ofrece algunas estrategias y el código de ejemplo para abordarlas. Por supuesto, dada la amplitud de escenarios que existen en un mundo a veces conectado, los ejemplos sirven sólo como un punto de partida.

Mark Bloodworth es un arquitecto de sistemas en el Equipo de promoción de desarrolladores y plataformas de Microsoft, donde trabaja con compañías en proyectos innovadores. Antes de unirse a Microsoft, fu el arquitecto de sistemas jefe de soluciones en BBC Worldwide, donde lideró un equipo responsable de la arquitectura y el análisis de sistemas. La mayor parte de sus años de trabajo ha estado centrada en las tecnologías de Microsoft, especialmente Microsoft .NET Framework, además de un poco de Java por si acaso. Mantiene un blog en remark.wordpress.com.

Dave Brown ha trabajado para Microsoft durante más de nueve años, inicialmente para Servicios de consultoría de Microsoft en tecnologías relacionadas con Internet. Trabaja actualmente en el Equipo de promoción de desarrolladores y plataformas en el Reino Unido como arquitecto de sistemas para el Microsoft Technology Centre. En esta función, comparte su tiempo entre el análisis empresarial de escenarios de clientes, el diseño de soluciones de arquitectura y la administración y desarrollo de código para soluciones de prueba de concepto. Su blog se puede encontrar en drdave.co.uk/blog.

Gracias al siguiente experto técnico: Ashish Shetty