Windows 8

Cómo compartir código entre aplicaciones para Windows Phone 8 y para Windows 8

Doug Holland

Descargar el ejemplo de código

En Visual Studio 2012 encontramos un excelente conjunto de herramientas para crear aplicaciones para Windows y Windows Phone 8. Por lo tanto, resulta pertinente explorar la cantidad de código que se puede compartir entre las versiones para la Tienda Windows y para Windows Phone de las aplicaciones.

Podemos escribir las aplicaciones de la Tienda Windows en diferentes lenguajes de programación: XAML con C#, Visual Basic, C++ e incluso HTML5 con JavaScript.

Por lo general, las aplicaciones de Windows Phone 8 se escriben en XAML con C# o Visual Basic, aunque ahora el SDK de Windows Phone 8 permite escribir aplicaciones Direct3D con XAML y C++. Si bien el SDK de Windows Phone 8 también cuenta con una plantilla para las aplicaciones basadas en HTML5, estas solamente se basan en XAML y las páginas web basadas en HTML5 se hospedan en el control WebBrowser.

En este artículo exploraré tres estrategias para compartir código entre las aplicaciones de la Tienda Windows y de Windows Phone: Bibliotecas de clases portables (PCL), componentes de Windows en tiempo de ejecución (WinRT) (junto con los componentes de Windows Phone en tiempo de ejecución) y la opción Agregar como vínculo de Visual Studio. Puede encontrar más información sobre cómo compartir código entre las aplicaciones de la Tienda Windows y de Windows Phone en el Centro de desarrollo (aka.ms/sharecode).

Cabe mencionar que aunque hay varias similitudes entre las aplicaciones de la Tienda Windows y las de Windows Phone (por ejemplo, los mosaicos dinámicos), ­estas son plataformas distintas, para las que se deberían diseñar experiencias de usuario específicas.

Arquitectura

Los principios arquitectónicos que permiten aumentar el porcentaje de código que se puede compartir son, en general, aquellos que promueven una separación de conceptos. Si ya emplea patrones que promueven la separación de conceptos, como los patrones Model-View-ViewModel (MVVM) y Model-View-Controller (MVC), le resultará fácil poner en práctica el uso compartido de código; también es el caso si en la arquitectura emplea patrones de inserción de dependencias. Definitivamente debería contemplar el uso de patrones al crear la arquitectura de las aplicaciones nuevas para aumentar el nivel de código compartido que se puede lograr. Con las aplicaciones existentes puede considerar la posibilidad de refactorizar la arquitectura para promover una separación de conceptos y, por lo tanto, compartir el código. La separación de conceptos que proporciona MVVM o MVC conlleva beneficios adicionales, como la posibilidad de que los diseñadores y desarrolladores trabajen en forma simultánea. Los diseñadores diseñan la experiencia de usuario con herramientas como Expression Blend mientras que los desarrolladores escriben el código en Visual Studio para que la experiencia de usuario cobre vida.

Bibliotecas de clases portables

El proyecto PCL de Visual Studio 2012 permite un desarrollo en diferentes plataformas, lo que nos ofrece la opción de elegir los marcos de destino compatibles con el ensamblado resultante. La plantilla de proyecto PCL, que se presentó como un complemento opcional en Visual Studio 2010, ahora se incluye en Visual Studio Professional 2012 y las versiones posteriores.

Por lo tanto, ¿qué código se puede compartir dentro de una PCL?

Las Bibliotecas de clases portables se llaman así debido a que permiten compartir código portable. Y para que el código sea portable, debe tratarse de código administrado y debe estar escrito en C# o en Visual Basic. Como una PCL produce un solo archivo binario, el código portable no emplea directivas de compilación condicionales; en vez de eso, las funciones que dependen de la plataforma se abstraen mediante interfaces o mediante clases base abstractas. Cuando el código portable tiene que interactuar con el código que depende de la plataforma, se emplean los patrones de inserción de dependencias para entregar implementaciones específicas para cada plataforma de las abstracciones. Al compilarse, la PCL da como resultado un ensamblado único al que se puede hacer referencia desde cualquier proyecto basado en los marcos de destino.

La Figura 1 muestra uno de los métodos arquitectónicos recomendados para compartir código mediante las PCL. Con el patrón MVVM, los modelos de vista y los modelos se contienen dentro de la PCL, junto con las abstracciones de cualquier función que dependa de la plataforma. Las aplicaciones de la Tienda Windows y de Windows Phone proporcionan la lógica de inicio, las vistas y las implementaciones de cualquier abstracción de las funciones que dependen de la plataforma. Aunque el patrón de diseño MVVM no es estrictamente necesario para obtener un código portable, la separación de conceptos que este promueve conduce a una arquitectura limpia y extensible.

Sharing Code Using the MVVM Design PatternFigura 1 Compartir código mediante el patrón de diseño MVVM

El cuadro de diálogo Agregar biblioteca de clases portables de Visual Studio 2012 es donde puede seleccionar los marcos de destino que reconocerá el ensamblado resultante.

Es posible que al principio piense que debería activar la casilla de Silverlight 5, pero esto no es necesario para compartir código entre las aplicaciones para la Tienda Windows y para Windows Phone. De hecho, si selecciona Silverlight 5, el código portable no podrá aprovechar algunos de los nuevos tipos extremadamente útiles, como la clase CallerMemberNameAttribute, que se incorporaron en Microsoft .NET Framework 4.5.

Si previamente ya ha desarrollado para Windows Phone, debe conocer la clase MessageBox, que permite presentar mensajes al usuario. Las aplicaciones de la Tienda Windows usan la clase MessageDialog de Windows en tiempo de ejecución para lograr esto. Veamos cómo abstraer esta funcionalidad que depende de la plataforma en una PCL.

La interfaz IMessagingManager de la Figura 2 abstrae la funcionalidad de mostrar mensajes al usuario, que depende de la plataforma. La interfaz IMessagingManager proporciona el método ShowAsync sobrecargado que recibe el mensaje y el título del mensaje que se presentará al usuario.

Figura 2 Interfaz IMessagingManager

/// <summary>
/// Provides an abstraction for platform-specific user messaging capabilities.
/// </summary>
public interface IMessagingManager
{
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <returns>A <see cref="T:MessagingResult"/>
  value representing the user's response.</returns>
  Task<MessagingResult> ShowAsync(string message, string title);
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <param name="buttons">The buttons to be displayed.</param>
  /// <returns>A <see cref="T:MessagingResult"/>
  value representing the user's response.</returns>
  Task<MessagingResult> ShowAsync(string message, string title,
    MessagingButtons buttons);
}

El método ShowAsync se sobrecarga para poder especificar, de manera opcional, los botones que se mostrarán junto con el mensaje. La enumeración MessagingButtons proporciona una abstracción independiente de la plataforma para mostrar un botón Aceptar, los botones Aceptar y Cancelar, o los botones Sí y No (ver Figura 3).

Figura 3 Enumeración MessagingButtons

/// <summary>
/// Specifies the buttons to include when a message is displayed.
/// </summary>
public enum MessagingButtons
{
  /// <summary>
  /// Displays only the OK button.
  /// </summary>
  OK = 0,
  /// <summary>
  /// Displays both the OK and Cancel buttons.
  /// </summary>
  OKCancel = 1,
  /// <summary>
  /// Displays both the Yes and No buttons.
  /// </summary>
  YesNo = 2
}

Los valores enteros subyacentes de la enumeración MessagingButtons se asignaron a la enumeración MessageBoxButton de Windows Phone a propósito, para convertir en forma segura la enumeración MessagingButtons en la enumeración MessageBoxButton.

ShowAsync es un método asincrónico que devuelve un Task­<MessagingResult>, que indica el botón que usó el usuario para descartar el mensaje. La enumeración MessagingResult (ver Figura 4) también es una abstracción que no depende de la plataforma.

Figura 4 Enumeración MessagingResult

/// <summary>
/// Represents the result of a message being displayed to the user.
/// </summary>
public enum MessagingResult
{
  /// <summary>
  /// This value is not currently used.
  /// </summary>
  None = 0,
  /// <summary>
  /// The user clicked the OK button.
    /// </summary>
    OK = 1,
    /// <summary>
    /// The user clicked the Cancel button.
    /// </summary>
    Cancel = 2,
    /// <summary>
    /// The user clicked the Yes button.
    /// </summary>
    Yes = 6,
   /// <summary>
  /// The user clicked the No button.
  /// </summary>
  No = 7
}

En este ejemplo, la interfaz IMessagingManager y las enumeraciones Messaging­Buttons y MessagingResult son portables y, por lo tanto, se pueden compartir dentro de una PCL.

Una vez que abstrajimos en la PCL las funciones que dependen de la plataforma, debemos proporcionar las implementaciones propias de cada plataforma de la interfaz IMessagingManager, para las aplicaciones de la Tienda Windows y de Windows Phone. La Figura 5 muestra la implementación para las aplicaciones de Windows Phone y la Figura 6 muestra la implementación para las aplicaciones de la Tienda Windows.

Figura 5 MessagingManager: implementación para Windows Phone

/// <summary>
/// Windows Phone implementation of the <see cref="T:IMessagingManager"/> interface.
/// </summary>
internal class MessagingManager : IMessagingManager
{
  /// <summary>
  /// Initializes a new instance of the <see cref="T:MessagingManager"/> class.
  /// </summary>
  public MessagingManager()
  {
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <returns>A <see cref="T:MessagingResult"/>
      value representing the users response.</returns>
  public async Task<MessagingResult> ShowAsync(string message, string title)
  {
    MessagingResult result = await this.ShowAsync(message, title,
      MessagingButtons.OK);
    return result;
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <param name="buttons">The buttons to be displayed.</param>
  /// <exception cref="T:ArgumentException"/>
  /// The specified value for message or title is <c>null</c> or empty.
  /// </exception>
  /// <returns>A <see cref="T:MessagingResult"/>
  /// value representing the users response.</returns>
  public async Task<MessagingResult> ShowAsync(
    string message, string title, MessagingButtons buttons)
  {
    if (string.IsNullOrEmpty(message))
    {
      throw new ArgumentException(
        "The specified message cannot be null or empty.", "message");
    }
    if (string.IsNullOrEmpty(title))
    {
      throw new ArgumentException(
        "The specified title cannot be null or empty.", "title");
    }
    MessageBoxResult result = MessageBoxResult.None;
    // Determine whether the calling thread is the thread
    // associated with the Dispatcher.
    if (App.RootFrame.Dispatcher.CheckAccess())
    {
      result = MessageBox.Show(message, title, 
        (MessageBoxButton)buttons);
    }
    else
    {
      // Execute asynchronously on the thread the Dispatcher is associated with.
      App.RootFrame.Dispatcher.BeginInvoke(() =>
      {
        result = MessageBox.Show(message, title, 
          (MessageBoxButton)buttons);
      });
    }
    return (MessagingResult) result;
  }
}

Figura 6 MessagingManager: implementación para la Tienda Windows

/// <summary>
/// Windows Store implementation of the <see cref="T:IMessagingManager"/> interface.
/// </summary>
internal class MessagingManager : IMessagingManager
{
  /// <summary>
  /// Initializes a new instance of the <see cref="T:MessagingManager"/> class.
  /// </summary>
  public MessagingManager()
  {
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <returns>A <see cref="T:MessagingResult"/>
      value representing the users response.</returns>
  public async Task<MessagingResult> ShowAsync(string message, string title)
  {
    MessagingResult result = await this.ShowAsync(message, title,
      MessagingButtons.OK);
    return result;
  }
  /// <summary>
  /// Displays the specified message using platform-specific
  /// user-messaging capabilities.
  /// </summary>
  /// <param name="message">The message to be displayed to the user.</param>
  /// <param name="title">The title of the message.</param>
  /// <param name="buttons">The buttons to be displayed.</param>
  /// <exception cref="T:ArgumentException"/>
  /// The specified value for message or title is <c>null</c> or empty.
  /// </exception>
  /// <exception cref="T:NotSupportedException"/>
  /// The specified <see cref="T:MessagingButtons"/> value is not supported.
  /// </exception>
  /// <returns>A <see cref="T:MessagingResult"/>
  /// value representing the users response.</returns>
  public async Task<MessagingResult> ShowAsync(
    string message, string title, MessagingButtons buttons)
  {
    if (string.IsNullOrEmpty(message))
    {
      throw new ArgumentException(
        "The specified message cannot be null or empty.", "message");
    }
    if (string.IsNullOrEmpty(title))
    {
      throw new ArgumentException(
        "The specified title cannot be null or empty.", "title");
    }
    MessageDialog dialog = new MessageDialog(message, title);
    MessagingResult result = MessagingResult.None;
    switch (buttons)
    {
      case MessagingButtons.OK:
        dialog.Commands.Add(new UICommand("OK",
          new UICommandInvokedHandler((o) => result = MessagingResult.OK)));
        break;
      case MessagingButtons.OKCancel:
        dialog.Commands.Add(new UICommand("OK",
          new UICommandInvokedHandler((o) => result = MessagingResult.OK)));
        dialog.Commands.Add(new UICommand("Cancel",
          new UICommandInvokedHandler((o) => result = MessagingResult.Cancel)));
        break;
      case MessagingButtons.YesNo:
        dialog.Commands.Add(new UICommand("Yes",
          new UICommandInvokedHandler((o) => result = MessagingResult.Yes)));
        dialog.Commands.Add(new UICommand("No",
          new UICommandInvokedHandler((o) => result = MessagingResult.No)));
        break;
      default:
        throw new NotSupportedException(
          string.Format("MessagingButtons.{0} is not supported.",
          buttons.ToString()));
            }
    dialog.DefaultCommandIndex = 1;
    // Determine whether the calling thread is the
    // thread associated with the Dispatcher.
    if (Window.Current.Dispatcher.HasThreadAccess)
    {
      await dialog.ShowAsync();
    }
    else
    {
      // Execute asynchronously on the thread the Dispatcher is associated with.
      await Window.Current.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal, async () =>
      {
        await dialog.ShowAsync();
      });
    }
    return result;
  }
}

La versión para Windows Phone de la clase MessagingManager usa la clase MessageBox propia de cada plataforma para mostrar el mensaje. Los valores enteros subyacentes de la enumeración MessagingButtons se asignaron a la enumeración MessageBoxButton de Windows Phone a propósito, lo que permite convertir en forma segura la enumeración MessagingButtons a la enumeración MessageBoxButton. De igual modo, los valores enteros subyacentes de la enumeración MessagingResult permiten convertir en forma segura a la enumeración MessageBoxResult.

La versión para la Tienda Windows de la clase MessagingManager en la Figura 6 emplea la clase MessageDialog de Windows en tiempo de ejecución para mostrar el mensaje. Los valores enteros subyacentes de la enumeración MessagingButtons se asignaron a la enumeración MessageBoxButton de Windows Phone a propósito, lo que permite convertir en forma segura la enumeración MessagingButtons a la enumeración MessageBoxButton.

Inserción de dependencias

Ahora que la arquitectura de la aplicación se definió tal como se ilustró en la Figura 1, IMessagingManager proporciona la abstracción específica para cada plataforma para los usuarios de mensajería. Ahora emplearé patrones de inserción de dependencias para insertar las implementaciones propias de cada plataforma de esta abstracción en el código portable. En el ejemplo de la Figura 7, HelloWorldViewModel emplea la inserción del constructor para insertar una implementación propia de la plataforma de la interfaz IMessagingManager. Luego, el método HelloWorldView­Model.DisplayMessage usa la implementación insertada para enviar un mensaje al usuario. Para obtener más información sobre la inserción de dependencias, le recomiendo leer “Dependency Injection in .NET” de Mark Seemann (Manning Publications, 2011, bit.ly/dotnetdi).

Figura 7 Clase portable HelloWorldViewModel

/// <summary>
/// Provides a portable view model for the Hello World app.
/// </summary>
public class HelloWorldViewModel : BindableBase
{
  /// <summary>
  /// The message to be displayed by the messaging manager.
  /// </summary>
  private string message;
  /// <summary>
  /// The title of the message to be displayed by the messaging manager.
  /// </summary>
  private string title;
  /// <summary>
  /// Platform specific instance of the <see cref="T:IMessagingManager"/> interface.
  /// </summary>
  private IMessagingManager MessagingManager;
  /// <summary>
  /// Initializes a new instance of the <see cref="T:HelloWorldViewModel"/> class.
  /// </summary>
  public HelloWorldViewModel(IMessagingManager messagingManager,
    string message, string title)
  {
    this.messagingManager = MessagingManager;
    this.message = message;
    this.title = title;
    this.DisplayMessageCommand = new Command(this.DisplayMessage);
  }
  /// <summary>
  /// Gets the display message command.
  /// </summary>
  /// <value>The display message command.</value>
  public ICommand DisplayMessageCommand
  {
    get;
    private set;
  }
  /// <summary>
  /// Displays the message using the platform-specific messaging manager.
  /// </summary>
  private async void DisplayMessage()
  {
    await this.messagingManager.ShowAsync(
      this.message, this.title, MessagingButtons.OK);
  }
}

Componentes de Windows en tiempo de ejecución

Los componentes de Windows en tiempo de ejecución permiten compartir código que no es portable entre las aplicaciones de la Tienda Windows y de Windows Phone. Sin embargo, los componentes no son compatibles en el nivel binario, así que también deberemos crear proyectos comparables para los componentes de Windows en tiempo de ejecución y de Windows Phone en tiempo de ejecución, para poder usar el código en ambas plataformas. Aunque tendremos que incluir los proyectos dentro de nuestra solución, tanto para los componentes de Windows en tiempo de ejecución como para los componentes de Windows Phone en tiempo de ejecución, estos proyectos se crean con los mismos archivos de código fuente de C++.

Gracias a la capacidad para compartir el código C++ nativo para las aplicaciones de la Tienda Windows y de Windows Phone, los componentes de Windows en tiempo de ejecución son una excelente opción para escribir operaciones con grandes cantidades de cálculos en C++, para lograr el mejor rendimiento posible.

Las definiciones de API dentro de los componentes de Windows en tiempo de ejecución se exponen en los metadatos de los archivos .winmd. Con estos metadatos, las proyecciones de lenguaje permiten que el lenguaje del código consumidor determine la forma en que se consumirá la API dentro de ese lenguaje. En la Figura8 se muestran los lenguajes permitidos para crear y consumir los componentes de Windows en tiempo de ejecución. En el momento de escribir este artículo, solo C++ permite crear ambos tipos de componentes.

Figura 8 Creación y consumo de componentes de Windows en tiempo de ejecución

Plataforma Creación Consumo
Componentes de Windows en tiempo de ejecución C++, C#, Visual Basic C++, C#, Visual Basic, JavaScript
Componentes de Windows Phone en tiempo de ejecución C++ C++, C#, Visual Basic

En el siguiente ejemplo mostraré cómo una clase en C++, diseñada para calcular números de Fibonacci, se puede compartir entre aplicaciones de la Tienda Windows y de Windows Phone. La Figura 9 y la Figura 10 muestran la implementación de la clase FibonacciCalculator en C++/Component Extensions (CX).

Figura 9 Fibonacci.h

#pragma once
namespace MsdnMagazine_Fibonacci
{
  public ref class FibonacciCalculator sealed
  {
  public:
    FibonacciCalculator();
    uint64 GetFibonacci(uint32 number);
  private:
    uint64 GetFibonacci(uint32 number, uint64 p0, uint64 p1);
  };    
}

Figura 10 Fibonacci.cpp

#include "pch.h"
#include "Fibonacci.h"
using namespace Platform;
using namespace MsdnMagazine_Fibonacci;
FibonacciCalculator::FibonacciCalculator()
{
}
uint64 FibonacciCalculator::GetFibonacci(uint32 number)
{
  return number == 0 ? 0L : GetFibonacci(number, 0, 1);
}
uint64 FibonacciCalculator::GetFibonacci(uint32 number, 
  uint64 p0, uint64 p1)
{
  return number == 1 ? p1 : GetFibonacci(number - 1, 
    p1, p0 + p1);
}

La Figura 11 muestra en el Explorador de soluciones de Visual Studio la estructura de la solución de las muestras que acompañan a este artículo. También se muestran los mismos archivos de código fuente de C++ que se encuentran en ambos componentes.

Visual Studio Solution Explorer
Figura 11 Explorador de soluciones de Visual Studio

Función Agregar como vínculo de Visual Studio

Al agregar un elemento existente a un proyecto en Visual Studio, es posible que se haya percatado de la pequeña flecha que aparece a la derecha del botón Agregar. Si hace clic en ella, tiene la opción de Agregar o Agregar como vínculo. Si elige la opción predeterminada de Agregar un archivo, el archivo se copiará al proyecto y las dos copias del mismo existirán de manera independiente en el disco y dentro del control de código fuente (en caso de usarlo). Si opta por Agregar como vínculo, solo existirá una instancia única del archivo en el disco y dentro del control de código fuente, lo que podría ser muy útil para el control de versiones. Este es el comportamiento predeterminado para agregar archivos existentes en los proyectos de Visual C++ y, por lo tanto, el cuadro de diálogo Agregar elemento existente no ofrece esta opción en el botón Agregar. El Centro de desarrollo ofrece orientación adicional sobre cómo compartir código mediante la opción Agregar como vínculo en bit.ly/addaslink.

Las API de Windows en tiempo de ejecución no son portables y, por lo tanto, no se pueden compartir dentro de una PCL. Windows 8 y Windows Phone 8 exponen un subconjunto de las API de Windows en tiempo de ejecución, por lo que el código se puede escribir usando este subconjunto y luego se puede compartir entre ambas aplicaciones mediante Agregar como vínculo. En el Centro de desarrollo puede encontrar más información sobre el subconjunto compartido de las API de Windows en tiempo de ejecución bit.ly/wpruntime.

En resumen

Con las versiones de Windows 8 y Windows Phone 8, puede comenzar a explorar las formas en las que podría compartir código para estas dos plataformas. En este artículo exploré cómo el código portable se puede compartir mediante las PCL compatibles en el nivel binario y cómo abstraer las funciones que dependen de la plataforma. También demostré cómo se puede compartir el código nativo que no es portable mediante los componentes de Windows en tiempo de ejecución. Finalmente, analicé la opción Agregar como vínculo de Visual Studio.

En términos de arquitectura observé que los patrones que promueven la separación de conceptos, como MVVM, pueden resultar útiles para compartir código y que los patrones de inserción de dependencias permiten que el código compartido haga uso de las funciones propias de cada plataforma. El Centro de desarrollo de Windows Phone entrega más información sobre cómo compartir código entre las aplicaciones para la Tienda Windows y para Windows Phone en aka.ms/sharecode y también proporciona la aplicación de ejemplo PixPresenter en bit.ly/pixpresenter.

Doug Holland trabaja como arquitecto experto en jefe en el equipo de evangelización de desarrolladores y plataformas de Microsoft. Ha pasado los últimos años trabajando con socios estratégicos, para aportar aplicaciones orientadas al cliente para Windows y Windows Phone. Fue MVP de Visual C#, desarrollador experto de Intel y autor del libro “Professional Windows 8 Programming: Application Development with C# and XAML” (Wrox, 2012), disponible en bit.ly/prowin8book.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Andrew Byrne (Microsoft), Doug Rothaus (Microsoft) y Marian Laparu (Microsoft)