DNM+ Online
dotnetmania 2.0
Desarrollo de una aplicación Metro en C# y XAML
Hasta la celebración del BUILD, circulaban muchos rumores sobre cómo iba a ser el modelo de desarrollo de aplicaciones en Windows 8. Ya se había filtrado la nueva interfaz con estética Metro y circulaban rumores sobre "la muerte de .NET", o que debería usarse HTML5 y JavaScript para desarrollar aplicaciones en Windows 8. La propia Microsoft no hizo mucho al respecto para acallar o confirmar cualquier rumor, salvo remitirnos a la celebración del BUILD. Finalmente la conferencia tuvo lugar, y con ella ha quedado todo un poco más claro.

Windows 8 tiene como dos modos de funcionamiento: uno que podríamos llamar modo Metro y otro que podríamos llamar modo Desktop. Estos dos modos no son excluyentes; es decir, se puede cambiar de uno a otro en cualquier momento:

  • En el modo Desktop, la apariencia de Windows 8 es casi idéntica a la de un Windows 7 (figura 1). Todos los conceptos que conocemos siguen estando presentes y el modelo de desarrollo sigue siendo el mismo. Es decir, seguiremos usando los conceptos que ya conocemos: .NET (WPF, Silverlight, Windows Forms) o bien la API Win32. No hay ningún cambio sustancial. Eso es lo primero que debemos tener presente: todo el conocimiento que tenemos sobre el desarrollo de aplicaciones Windows se puede aprovechar para desarrollar aplicaciones para Windows 8 en su modo Desktop.

Figura 1. Windows 8 en modo Desktop... ¿o es Windows 7?

  • El modo Metro (figura 2) está pensado para una experiencia mucho más inmersiva y, por supuesto, multi-táctil. Es, pues, una experiencia pensada principalmente para tablets. Y aquí sí que hay un nuevo modelo de desarrollo al que deberíamos prestar mucha atención. Microsoft ha desarrollado una API completamente nueva, llamada WinRT, que mediante lo que Microsoft llama proyecciones puede usarse con C++, C#/VB.NET o JavaScript. Pero lo importante es que, optemos por el lenguaje que optemos, la API subyacente va a ser la misma, esa nueva WinRT. Aunque, por supuesto, eso no quita que los modelos de aplicación sean distintos entre sí.

Figura 2. Windows 8 en modo Metro. Aquí sí que todo es distinto.

Dos modelos de aplicación, una sola API

JavaScript y HTML5

Si usted proviene del desarrollo web moderno, es decir, con mucho Ajax y grandes dosis de procesamiento en cliente, se va a sentir muy cómodo con este modelo de desarrollo. En él, toda la nueva API de WinRT se expone a través de varios archivos .js que se usan de la forma normal. La interfaz de usuario se define a través de HTML y, como es normal en las aplicaciones web, se modifica el DOM de forma dinámica para ir cambiando la interfaz de usuario. Por supuesto, cualquier librería JavaScript estándar (como jQuery) puede usarse sin problemas. El papel que juega WinRT en este modelo de desarrollo es proporcionar acceso a toda la funcionalidad propia de Windows que la aplicación puede requerir: leer archivos, conectarse a Internet, usar audio. Porque aunque usemos HTML y JavaScript, lo que obtendremos no es una aplicación Web; es una aplicación Windows 8.

C++/C#/VB.NET y XAML

El modelo más cercano a los desarrolladores que vienen de .NET (ya sea en Silverlight o en WPF) es el de usar C#, VB.NET o bien C++. En este modelo usamos XAML para definir la interfaz de usuario, de igual manera a como se hace en Silverlight o WPF. Pero, ojo: aunque se usa XAML, debe tenerse presente que no estamos usando ni Silverlight, ni WPF. De hecho, los espacios de nombres son distintos: si WPF reside básicamente en los espacios de nombre System.Windows.* cuando desarrollemos para Metro, todos los controles que usaremos residen en el espacio de nombres Windows.UI.Xaml.*.

Lo cierto es que Microsoft ha hecho un gran trabajo al trasladar casi todos los conceptos que existen en WPF y Silverlight, de forma que si ya usaba XAML para definir interfaces (ya fuese en WPF o Silverlight), todos sus conocimientos siguen vigentes. La gran diferencia viene en el code behind. Para empezar, no vamos a tener todo .NET a nuestra disposición, sino solo un subconjunto (llamado .NET Core Profile1), y para muchas tareas vamos a tener que usar las nuevas clases de WinRT. Aquí un inciso técnico: WinRT no está implementada usando .NET, sino que es más bien un COM con esteroides. Pero aunque WinRT sea básicamente COM, expone su información en el formato usado por .NET, de forma que para nosotros su uso es completamente transparente. Podéis olvidaros, pues, de wrappers, interop o P/Invoke para acceder a WinRT: vamos a usar clases y objetos de forma totalmente transparente. A diferencia de las clases de .NET, que residen en el espacio de nombres System., las clases de WinRT residen en el espacio de nombres Windows..

Desarrollando para Metro con C# y XAML

A continuación, vamos a ver cómo desarrollar una aplicación Metro usando C# y XAML. Vamos a desarrollar todo un clásico como es un visor de fotos, que es una aplicación lo suficientemente sencilla para poder ser vista toda de golpe, pero a la vez con la suficiente complejidad para permitirnos mostrar algunos aspectos del desarrollo de aplicaciones Metro. La interfaz de usuario va a ser muy sencilla: la aplicación tendrá un título, un botón para cargar las imágenes, un indicador de progreso, la lista de imágenes y la zona central donde se va a mostrar la imagen seleccionada. Lo primero es, pues, definir un XAML básico (listado 1). Como se puede observar, no hay diferencias visibles entre este código XAML y uno equivalente que usaríamos para WPF y Silverlight. Es en el code behind donde empezarán a verse diferencias sustanciales.

<UserControl.Resources>
    <Style x:Key="H1" TargetType="TextBlock">
        <Setter Property="FontSize" Value="18" />
    </Style>
</UserControl.Resources>
<Grid Background="Black" HorizontalAlignment="Left" 
                         VerticalAlignment="Top">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Grid.Column="0" Style="{StaticResource H1}"
               Text="Un simple visor de imágenes en Metro C#/XAML" />
    <StackPanel Grid.Row="1" Grid.Column="0" >
        <Button Content="Pulsa aquí" Click="btnImages_Click" />
        <TextBlock x:Name="txtStatus" Margin="10,0,0,0"/>
    </StackPanel>
    <Image Grid.Row="2" Grid.Column="0" HorizontalAlignment="Center"
           VerticalAlignment="Center" Source="{Binding CentralImage}" 
           ManipulationMode="TranslateX"
           ManipulationStarted="ImgCentral_ManipulationStarted" 
           ManipulationCompleted="ImgCentral_ManipulationCompleted" />
    <ItemsControl Grid.Row="3" Grid.Column="0" x:Name="lstImages" 
                  ItemsSource="{Binding Images}"
                  Style="{StaticResource icHorizontal}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Button Command="{Binding LoadImage}" 
                        CommandParameter="{Binding}">
                    <Button.Content>
                        <StackPanel Orientation="Vertical" Margin="5">
                            <Image Source="{Binding Thumb}" Margin="2" />
                            <TextBlock Text="{Binding Name}" Margin="2" />
                        </StackPanel>
                    </Button.Content>
                </Button>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>
Listado 1. Código XAML inicial

Dado que utilizaremos el patrón MVVM para desarrollar la aplicación, vamos a definir nuestro ViewModel. Básicamente, necesitamos que éste contenga una lista de objetos que nos den información sobre las imágenes, y luego enlazaremos esa lista al ItemsControl que muestra las miniaturas. El código del ViewModel puede ser como el que se presenta en el listado 2. La clase ImageInfo contiene la información que necesitamos de las imágenes: su nombre, su ruta, su miniatura y su índice dentro de la lista de imágenes.

public class ImagesViewModel : INotifyPropertyChanged
{
    private readonly List<ImageInfo> _images;

    public IEnumerable<ImageInfo> Images { get { return _images; } }
    private BitmapImage _central;
    private ImageInfo _current;

    public BitmapImage CentralImage
    {
        get { return _central; }
        set
        {
            _central = value;
        }
    }

    public ImagesViewModel()
    {
        _images = new List<ImageInfo>();
    }
}
Listado 2. Código inicial del ViewModel

Ahora ya estamos listos para cargar todas las imágenes que el usuario tenga en la carpeta de imágenes. Para ello, añadimos un manejador en el evento Click y empezamos a codificar. Lo primero que debemos tener presente es que deberemos usar WinRT para obtener acceso a las imágenes. En una aplicación .NET tradicional, usaríamos System.IO.Directory para enumerar los ficheros y System.IO.FileStream para cargar los datos de cada uno de los ficheros. Pues bien, ninguna de esas clases está disponible: recordad que, aunque usamos .NET, tan solo un subconjunto de las funcionalidades de éste está disponible, y por norma general es el subconjunto que menos tiene que ver con el sistema operativo. Para el resto de tareas, deberemos usar WinRT.

Un mundo asíncrono

Una de las principales decisiones que tomó Microsoft al diseñar WinRT es que cualquier método que pudiera tardar más de 50ms en su ejecución tendría una versión asíncrona. Y no sólo eso, sino que solamente existiría la versión asíncrona de dicho método. Si usted proviene de Silverlight, ya estará acostumbrado a lidiar con la asincronía al llamar a servicios web. Pues bien, WinRT lleva esto al extremo, y deberemos usar la asincronía para casi todo. Tareas tan comunes como enumerar los ficheros de un directorio o leer un fichero tan solo pueden ser implementadas asíncronamente para WinRT. La razón de este proceder es que Microsoft quiere que las aplicaciones Metro tengan un comportamiento mucho más dinámico, que nunca den la sensación de estar "colgadas" a la espera de alguna operación que pueda tardar demasiado. Y el hecho de que solo exista la versión asíncrona (en lugar de una síncrona y otra asíncrona) es que Microsoft ha constatado que siempre que existen las dos versiones de una API, los desarrolladores optamos mayoritariamente por la síncrona, sin duda porque es más fácil de usar. Por suerte, C# 5.0 vendrá a nuestro rescate con la incorporación de dos nuevas palabras clave que nos van a facilitar enormemente el uso de métodos asíncronos: async y await. La primera de ellas indica que un método en algún momento de su ejecución realizará una llamada asíncrona a un método; mientras que la segunda permite realizar dicha llamada asíncrona y a la vez establecer un punto de sincronización: el código que sigue a await se ejecutará solamente una vez completada la llamada asíncrona (podemos entender que el código que sigue a await es el callback). El uso de async/await constituye lo que en programación se conoce como co-rutina2: una rutina que tiene distintos puntos de entrada y salida.

El ejemplo del listado 3 muestra como async y await nos simplifican la creación del método asíncrono y su llamada. El método OperacionQueTarda realiza determinadas tareas costosas, pero termina devolviendo un objeto Task que encapsula dichas tareas. El método Button_Click (un manejador de eventos estándar) ha sido decorado con async para indicar que quiere usar await para realizar una espera asíncrona sobre algún método.

Una vez se ha llamado a OperacionQueTarda, el código que sigue al await (la llamada a MessageBox) no será ejecutado hasta que la ejecución del método OperacionQueTarda haya finalizado. Mientras tanto, se continúa ejecutando el código de la tarea principal (es decir, se sale del método Button_Click), por lo que la interfaz de usuario no da nunca la sensación de quedarse "bloqueada" esperando (de hecho, se podrían encadenar varias llamadas a OperacionQueTarda pulsando sucesivas veces el botón). Este código de ejemplo proviene de una aplicación WPF 4.5; async y await son novedades de C# 5.0, en ningún modo exclusivas de WinRT.

async private void Button_Click(object sender, RoutedEventArgs e)
{
    await OperacionQueTarda();
    MessageBox.Show("La operación ha finalizado");
}

private Task OperacionQueTarda()
{
    var task = new Task(() =>
    {
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(300);
        }
    });

    task.Start();
    return task;
}
Listado 3. Ejemplo de async/await

Usando WinRT

En el manejador del evento Click del botón vamos a cargar todas las imágenes que el usuario tenga en su carpeta de imágenes. Para ello, primero obtenemos una referencia a la carpeta de imágenes, y luego enumeramos todos los elementos con GetItemsAsync. Es una convención de WinRT que todos los métodos que sean asíncronos terminen con el sufijo Async. Una vez tengamos todos los elementos, vamos a añadirlos al ViewModel junto con su miniatura, que podemos obtener a través de la llamada GetThumbnailAsync para cada elemento. En el listado 4 se puede observar cómo el uso de await nos permite codificar esas llamadas asíncronas de una forma idéntica a como realizaríamos llamadas síncronas. Sin async y await, el uso de WinRT sería, sin duda alguna, mucho más pesado.

public async Task LoadThumbsAsync()
{
    var picsFolder = Windows.Storage.KnownFolders.PicturesLibrary;
    var pics = await picsFolder.GetItemsAsync();
    var data = pics.Where(x => 
       x.IsOfType(Windows.Storage.StorageItemTypes.File));
    var images = new List<ImageInfo>();
    int idx = 0;
    foreach (var item in data)
    {
        var thumb = await item.GetThumbnailAsync
            (Windows.Storage.FileProperties.ThumbnailMode.PicturesView);
        var bitmapData = GetBitmapData(thumb);
        _images.Add(new ImageInfo(this)
        {
            Name = item.Name,
            Path = item.Path,
            Thumb = bitmapData,
            Index = idx++
        });
    }
}
Listado 4. Método asíncrono para cargar las imágenes

Es importante observar que el uso de await para llamar a los métodos asíncronos GetItemsAsync y GetThumbnailAsync convierte a su vez a nuestro método en asíncrono. Los métodos marcados con async deben devolver a su vez un objeto awaitable (para permitir que alguien les llame asíncronamente). Estos objetos son Task, Task o, en general, cualquier clase que implemente la interfaz IAsyncInfo.

Añadiendo permisos a la aplicación

Las aplicaciones WinRT se ejecutan bajo un modelo de seguridad muy estricto. Al igual que como sucede en la mayoría de plataformas móviles, la aplicación debe informar qué permisos requiere. Dicha información será transferida al usuario cuando instale la aplicación para que pueda decidir si quiere instalarla o no. Si nosotros intentamos mediante código realizar alguna acción para la cual no hemos pedido permiso (por ejemplo, leer un archivo), recibiremos una excepción. Además, como las aplicaciones WinRT van a ser desplegadas a través del AppStore, seguramente van a estar sometidas a diversos controles de seguridad (al igual que ocurre actualmente con las aplicaciones para Windows Phone).

Nuestra aplicación debe poder acceder a la carpeta de imágenes del usuario, así que debemos pedir permiso para ello. Para hacerlo, lo más cómodo es usar el editor de permisos incorporado en Visual Studio 2011. Para ello, hacemos doble clic en el archivo Package.appxmanifest desde el Explorador de soluciones para que se nos abra el editor del manifiesto (figura 3). En el manifiesto va mucha más información (como el icono de la aplicación), pero a nosotros nos interesa la pestaña de capacidades (Capabilities).

La figura 3 muestra el editor de permisos de la aplicación. Simplemente tenemos que marcar aquellos que deseemos. En nuestro caso, queremos acceso a la carpeta de imágenes del usuario, así que marcamos la casilla "Picture Library Access".

Figura 3. Editor de permisos de la aplicación

Añadiendo la imagen central

Una vez tenemos cargadas todas las imágenes en la lista, ahora debemos cargar la imagen seleccionada en la zona central. Para ello, debemos modificar la propiedad CentralImage del ViewModel e informar a la interfaz de usuario de que dicha propiedad ha sido modificada para que se refresque.

Así pues, cuando se seleccione un elemento de la lista de imágenes, simplemente cargaremos la imagen y guardaremos el bitmap en la propiedad CentralImage del ViewModel. Al igual que en Silverlight o WPF, implementaremos INotifyPropertyChanged para indicar a la vista que el valor de la propiedad ha cambiado y ésta pueda refrescarse. Aquí un inciso: WinRT tiene su propia INotifyPropertyChanged, que es distinta (aunque idéntica) a la INotifyPropertyChanged que se usa en WPF. Por lo tanto, atención, porque debemos implementar Windows.UI.Xaml.Data.INotifyPropertyChanged en lugar de System.ComponentModel.INotifyPropertyChanged. Esta dualidad ha sido reconocida por Microsoft, que ha dicho que resolverá el problema en sucesivas versiones. Del mismo modo, el uso de ObservableCollection no funciona en WinRT, ya que éste no reconoce INotifyCollectionChanged. En su lugar, debe usarse cualquier clase que implemente IObservableVector, aunque en la API actual no viene ninguna implementación de serie (de nuevo, esta situación ha sido reconocida por Microsoft)3. Para simplificar el ejemplo, aquí se ha usado un simple IEnumerable para enlazar la lista de imágenes, ya que suponemos que su contenido no cambiará una vez enlazada. Este es el motivo por el cual, si se observa el código del ejemplo completo, se verá que no se asigna el DataContext de la vista hasta que se ha leído la lista de imágenes. Así pues, el código en el ViewModel para cargar la imagen a tamaño real queda como se muestra en el listado 5.

private async Task LoadFullImage(ImageInfo x)
{
    var picsFolder = Windows.Storage.KnownFolders.PicturesLibrary;
    var image = await picsFolder.GetFileAsync(x.Path.Substring( 
       x.Path.LastIndexOf(‘\\’) + 1));
    var reader = await image.OpenAsync(Windows.Storage.FileAccessMode.Read);
    var fullBitmap = GetBitmapData(reader);
    _current = x;
    CentralImage = fullBitmap;
}
Listado 5. Código para cargar la imagen a tamaño real

Un poco de soporte para touch

Debemos tener siempre presente que uno de los destinos principales de las aplicaciones Metro serán tablets o bien portátiles con pantalla multi-táctil: debemos preparar nuestras aplicaciones para que puedan ser usadas con los dedos, en lugar de con teclado y ratón. Por supuesto, todo lo que hemos hecho hasta el momento funciona se use la aplicación con ratón o se use en una pantalla táctil, pero ahora vamos a añadir un poco de soporte exclusivo para touch: vamos a implementar que si el usuario arrastra la imagen hacia la izquierda se muestre la siguiente imagen (al igual que ocurre en todos los visores de imágenes para móviles o tablets).

El soporte para touch se basa en lo que Microsoft llama manipulaciones: ¿arrastras algo de un sitio para otro? Es una manipulación de traslación. ¿Haces el clásico gesto de separar dos dedos en diagonal para ampliar o reducir algo? Es una manipulación de escalado. De igual modo, si mantienes un dedo fijo en algún punto y rotas otro a su alrededor esto será una manipulación de rotación. Dado que queremos actuar cuando el usuario arrastre la imagen, debemos estar atentos a la manipulación de traslación. Para ello, lo primero es indicar a la imagen que soporte este tipo de manipulaciones, mediante la propiedad ManipulationMode. Si no establecemos esta propiedad, la imagen ignorará todas las manipulaciones que se efectúen sobre ella.

Una vez indicado que queremos que la imagen nos informe cuando se realice una manipulación, ya podremos hacer uso de los tres eventos clásicos: ManipulationStarted, ManipulationDelta y ManipulationCompleted. El primero de ellos se lanza una vez empieza una manipulación, el segundo se lanza varias veces a medida que se está realizando la manipulación, y el último se lanza una sola vez, cuando la operación termina. Que los usemos todos o solo algunos de ellos dependerá de lo que queramos hacer. En nuestro caso, vamos a empezar usando tan sólo ManipulationCompleted para ver si el usuario ha arrastrado la imagen hacia la izquierda (y además un valor determinado, para prevenir arrastres involuntarios) y, si es el caso, simplemente cargar la siguiente imagen. El código es extremadamente simple (ver listado 6). Simplemente comprobamos que el arrastre hacia la izquierda tiene una extensión mínima, y en tal caso llamamos al método SelectNextImage de nuestro ViewModel, cuyo código es también muy simple.

private void ImgCentral_ManipulationCompleted(object sender, 
   ManipulationCompletedEventArgs e)
{
    var imgCentral = sender as Image;
    if (e.TotalManipulation.Translation.X < -100.0)
    {
        ViewModel.SelectNextImage();
    }
    e.Handled = true;
}

public void SelectNextImage()
{
    int nextIndex = _current.Index < _images.Count - 1 ? _current.Index + 1 : 0;
    LoadFullImage(_images[nextIndex]);
}
Listado 6. Código necesario para cargar la siguiente imagen

Probando el código para touch

Si, como yo, usted no dispone de una tablet preparada para funcionar con Windows 8, tranquilo, porque eso no impedirá que pueda probar sus desarrollos de touch: Visual Studio 2011 incorpora un emulador de tablet para esos casos. Esto es muy importante, porque los eventos de manipulación no tienen contrapartida con el teclado y el ratón.

Para emular las acciones de touch, simplemente configure Visual Studio para que utilice el simulador a la hora de depurar la aplicación, en lugar de su máquina (figura 4). A partir de ese momento, cuando depure su aplicación se lanzará el simulador, y podrá probar sin problemas la manipulación. La figura 5 muestra el simulador de tablets en acción. La columna de botones situada a la derecha nos permite probar rápidamente las manipulaciones más comunes (tales como rotar, escalar, o trasladar).

Figura 4. Configurando VS 2011 para usar el simulador

Figura 5. El simulador en acción

Proporcionando feedback al usuario

A continuación, un paso más es usar ManipulationStarted para ofrecer al usuario un feedback de que hemos entendido su gesto. Eso es importante: debemos siempre proporcionar feedback a todo lo que hace el usuario, lo que hará que su experiencia sea mucho mejor (estará más seguro de que lo que hace está siendo tenido en cuenta por la aplicación).

En nuestro ejemplo, vamos a proporcionar un feedback muy simple: cuando el usuario empiece la manipulación, modificaremos la opacidad de la imagen para difuminarla un poco, como muestra el listado 7. De este modo, el usuario se dará cuenta de que su movimiento ha sido procesado. Por supuesto, debemos modificar también el gestor del evento ManipulationCompleted para restablecer la opacidad al finalizar la manipulación.

private void ImgCentral_ManipulationStarted(object sender, 
   ManipulationStartedEventArgs e)
{
    var imgCentral = sender as Image;
    imgCentral.Opacity = 0.5;
}
Listado 7. Proporcionando un feedback al usuario

Conclusiones

En este artículo hemos explorado la superficie de WinRT para ver cómo podemos desarrollar una aplicación Metro para Windows 8, con XAML para definir la interfaz de usuario y C# para el code behind. Aunque WinRT no es WPF ni Silverlight, casi todos nuestros conocimientos de XAML seguirán siendo aplicables, y tan solo debemos empezar a aprender la nueva API de WinRT. Si es usted un desarrollador de WPF o Silverlight, la llegada de Windows 8 y WinRT no es una amenaza, sino una oportunidad: ¡está en una posición excelente para empezar a desarrollar rápidamente aplicaciones para Windows 8 que ofrezcan una experiencia inmersiva y muy personal a los futuros usuarios de las tablets que en breve van a ir apareciendo!

blog comments powered by Disqus
autor
referencias