Pues eso… de lo que hoy te voy a hablar es de cómo actualizar el icono de nuestra aplicación para Windows Store. Y cuando digo actualizar me refiero a mostrar algún mensaje o alguna imagen o ambas dos cosas en el icono que está en la página de inicio de Windows 8. En concreto se trata de mostrar la hora actual y actualizarla cada minuto, por tanto el título podría haber sido: Actualizar el icono (tile) de una aplicación de Windows Store cada minuto.
Para que entres en situación:
En la pantalla de inicio de Windows hay algunos iconos (o tiles) que se actualizan, por ejemplo, el correo, el tiempo, los contactos, etc. Ver la figura 1.
Esas actualizaciones pueden venir de la nube de un sitio web o de forma local (porque tú quieras actualizar el icono).
Foto 1. Los iconos (tiles) de Windows 8 permiten mostrar notificaciones
De forma predeterminada las notificaciones se pueden mostrar en el icono (o tile) de la aplicación, ya sea en modo alargado o cuadrado: el icono puede ser cuadrado (smaller) o alargado (larger), pero también se puede indicar que no haya "animaciones" en el icono (nosotros como usuarios podemos decidirlo). Para ello sólo hay que pulsar con el botón secundario del ratón en el icono y del menú, u opciones mostradas en la parte inferior, podemos indicar si no queremos esa animación (Turn live tile on/off) tal como vemos en la figura 2.
Figura 2. El usuario puede activar/desactivar la notificación
Nota:
Sigue este enlace si quieres saber cómo capturar la pantalla en Windows 8 (tanto de la pantalla de inicio como del escritorio)
¿Cómo podemos hacer esas notificaciones?
Sigue leyendo y sabrás cómo hacer algunas de esas notificaciones, aunque aquí te voy a explicar sólo la de notificar con texto, tanto para la imagen grande como la pequeña, si quieres saber cómo usar más tipos de notificaciones tendrás que leerte (entre otras cosas) la ayuda de la enumeración TileTemplateType o mirar en las páginas del centro de desarrollo de Microsoft para saber cómo desarrollar aplicaciones para la tienda de Windows.
En mi caso particular, para este artículo me he basado en el inicio rápido (quickstart) de: enviar una actualización de icono (aplicaciones de la Tienda Windows con C#/VB/C++ y XAML) (Windows), aunque también estuve probando antes con la biblioteca NotificationsExtensions con idea de hacerlo más fácil y sin tanto manejo de código XML, pero no me funcionó… o lo mismo no la utilicé correctamente. Incluso estuve tentado de usar un temporizador en background, particularmente usando el código de Dave Smits: Tile Update every minute, que seguramente usaré en esta misma aplicación ya que según parece el temporizador se para cuando Windows pone en modo suspensión la aplicación (aún no lo he comprobado, pero me da la impresión de que ocurre algo de eso… ya te diré).
Vamos a lo que vamos
Lo primero que he hecho es agregar dos importaciones de espacios de nombres al código de MainPage, el primero para usar las notificaciones y el segundo para usar el Xml.Dom:
Imports Windows.UI.Notifications Imports Windows.Data.Xml.Dom using Windows.UI.Notifications; using Windows.Data.Xml.Dom;
También necesito crear otro temporizador, ya que el que teníamos en nuestra aplicación (ver los enlaces al final de la página) se puede detener si se pulsa en cualquiera de los botones "stop" de la aplicación.
Este segundo temporizador estará funcionando y haciendo comprobaciones cada 2 segundos (más o menos) e internamente comprobará si ha cambiado de minuto y en ese caso llamará al método asignarTile() para actualizar los textos del icono. Esos textos será la hora y minutos y además la fecha actual, si se muestra en el icono grande se mostrará en formato largo y si se muestra en el icono pequeño, la fecha estará reducida (porque si no, no se ve, así que).
Dicho esto, al principio de la clase definimos el temporizador y un par de variables para indicar el intervalo y llevar la cuenta del último minuto mostrado.
Visual Basic:
Private timerNotif As DispatcherTimer ' Intervalo para realizar las notificaciones Private intervaloNotif As Integer = intervalo * 2 ' para guardar el minuto en que se notificó Private minutoNotif As Integer = -1
C#
private DispatcherTimer timerNotif; // Intervalo para realizar las notificaciones // (en C# no se puede usar el valor de intervalo en la declaración) private int intervaloNotif = 900 * 2; // para guardar el minuto en que se notificó private int minutoNotif = -1;
Si te preguntas porqué no he usado un intervalo de un minuto (o algo menos), decirte que el problema es que ese minuto se cuenta desde que activamos el temporizador, por tanto si esa activación del temporizador ocurre, digamos, en el segundo 40, hasta que no llegue el segundo cuarenta del próximo minuto no se actualizaría el contenido, así que… yo he usado 1800 milisegundos, es decir, dos veces el intervalo usado en el otro temporizador, llámame romántico si quieres, jeje.
Fíjate también que en el código de C# he usado el valor 900 directamente en lugar del campo intervalo para asignar el valor de intervaloNotif, esto es así porque el compilador me decía que no se puede usar un campo (field) intervalo porque no es estático… o algo así…
También lo podría haber declarado intervalo con el modificador "const" y así sí que podría usarlo como valor a asignar a intervaloNotif, hazlo como tu veas mejor.
En el constructor tenemos que hacer las definiciones y demás cosillas para crear el nuevo temporizador. Yo lo he puesto después del código que ya teníamos (justo antes de acabar el método).
Visual Basic:
' Crear el temporizador para las notificaciones timerNotif = New DispatcherTimer() AddHandler timerNotif.Tick, AddressOf timerNotif_Tick timerNotif.Interval = New TimeSpan(0, 0, 0, 0, intervaloNotif)
C#:
// Crear el temporizador para las notificaciones timerNotif = new DispatcherTimer(); timerNotif.Tick += timerNotif_Tick; timerNotif.Interval = new TimeSpan(0, 0, 0, 0, intervaloNotif);
Ahora veamos el código del método de evento para este nuevo temporizador.
Aquí lo que hacemos es detener el temporizador, comprobar si tenemos que actualizar el icono (tile) y volver a poner en marcha el temporizador.
Para lo del minuto de lapso, lo único que hago es comprobar si el valor de minutoNotif es distinto del minuto actual y en ese caso mostrar el mensaje actualizado. Te digo esto por si quieres hacer algo cada x segundos, en se caso, tendrías que utilizar alguna que otra comparación más "sofisticada".
VB:
Private Sub timerNotif_Tick(sender As Object, e As Object) timerNotif.Stop() ' actualizar el tile cada minuto If minutoNotif <> DateTime.Now.Minute Then minutoNotif = DateTime.Now.Minute asignarTile() End If timerNotif.Start() End Sub
C#:
private void timerNotif_Tick(object sender, object e) { timerNotif.Stop(); // actualizar el tile cada minuto if (minutoNotif != DateTime.Now.Minute) { minutoNotif = DateTime.Now.Minute; asignarTile(); } timerNotif.Start(); }
Como podrás imaginar el trabajo de notificación se realiza en el método asignarTile, pero antes de ver el código de ese método, veamos qué más tenemos que hacer… por ejemplo habilitar el tema de las notificaciones mediante colas, esto en realidad no haría falta, pero he comprobado que si no se usa esa llamada al método EnableNotificationQueue del método compartido CreateTileUpdaterForApplication de la clase TileUpdaterManager y tampoco se asigna un valor a la propiedad Tag del objeto TileNotification usado para hacer las notificaciones esas notificaciones no las hace como uno espera… al menos a mí me ha pasado que me mostraba el texto que inicialmente se usó al iniciar la aplicación y después el que se iba actualizando… así que…
Dicho esto, agrega el siguiente código al principio del método OnNavigatedTo:
Nota del 8 de diciembre:
La llamada al método EnableNotificacionQueue(True) lo que en realidad hace es permitir que exista una cola de notificaciones (con un máximo de 5) y esas notificaciones se van alternando de forma automática. Por tanto, para la intención de nuestra aplicación no es precisamente lo más recomendable. Así que, comenta esa línea de código.
VB:
' Permitir las notificaciones TileUpdateManager.CreateTileUpdaterForApplication().EnableNotificationQueue(True) minutoNotif = DateTime.Now.Minute timerNotif.Start() asignarTile()
C#:
// Permitir las notificaciones TileUpdateManager.CreateTileUpdaterForApplication().EnableNotificationQueue(true); minutoNotif = DateTime.Now.Minute; timerNotif.Start(); asignarTile();
Y ya sólo nos queda el código del método asignarTile, que (tal como su nombre indica) es el que se encarga de asignar el texto (o lo que indiquemos) a mostrar en el icono de la aplicación.
Como ya te comenté, el icono (tile) puede tener dos tamaños, uno cuadrado y más pequeño (la imagen a usar en el icono es de 150 x150 píxeles) y otro alargado (la imagen es de 310 x 150), en este ejemplo utilizaré la notificación en esos dos tamaños, pero sólo con texto.
Como veremos, cuando definimos la notificación para el icono ancho hay que definir también la del icono pequeño… aquí veremos cómo hacerlo.
Y si lo tuyo son las imágenes en vez de los textos, también veremos cómo asignar imágenes, concretamente las de los iconos pequeños y grandes que están definidos en la aplicación (en la carpeta Assets), ya que haremos que se restauren las imágenes predeterminadas, si no hacemos esto, comprobarás que se queda el último texto que asignes… y en nuestro caso (un reloj que se actualiza cada minuto) no queda muy bien que digamos…
Y ya sin más dilación (o rollo por mi parte) veamos el código del método asignarTile.
VB:
''' <summary> ''' Asignar el texto a mostrar en el tile (icono) de la aplicación. ''' En el parámetro se indicará el texto a mostrar (la hora actual) ''' </summary> Private Sub asignarTile() Dim hora As String = DateTime.Now.ToString("HH:mm") ' La plantilla para el tamaño ancho: TileWideText01 ' Esta plantilla soporta una cabecera y cuatro líneas de texto Dim tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideText01) Dim tileTextAttributes = tileXml.GetElementsByTagName("text") tileTextAttributes(0).InnerText = hora tileTextAttributes(1).InnerText = DateTime.Now.ToString("dddd, dd MMMM yyyy") ' La plantilla para el tamaño pequeño: TileSquareText01 ' Esta plantilla es como la ancha Dim squareTileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileSquareText01) Dim squareTileTextAttributes = squareTileXml.GetElementsByTagName("text") squareTileTextAttributes(0).InnerText = hora squareTileTextAttributes(1).InnerText = DateTime.Now.ToString("dd/MMM/yyyy") ' La plantilla del icono pequeño la agregamos a la del icono ancho ' y así el propio Windows mostrará el texto que corresponda según el tamaño del icono Dim node = tileXml.ImportNode(squareTileXml.GetElementsByTagName("binding").Item(0), True) tileXml.GetElementsByTagName("visual").Item(0).AppendChild(node) Dim tileNotification = New TileNotification(tileXml) tileNotification.Tag = "ClockW8_eGi" TileUpdateManager.CreateTileUpdaterForApplication().Update(tileNotification) End Sub
C#:
/// <summary> /// Asignar el texto a mostrar en el tile (icono) de la aplicación. /// En el parámetro se indicará el texto a mostrar (la hora actual) /// </summary> private void asignarTile() { string hora = DateTime.Now.ToString("HH:mm"); // La plantilla para el tamaño ancho: TileWideText01 // Esta plantilla soporta una cabecera y cuatro líneas de texto var tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideText01); var tileTextAttributes = tileXml.GetElementsByTagName("text"); tileTextAttributes[0].InnerText = hora; tileTextAttributes[1].InnerText = DateTime.Now.ToString("dddd, dd MMMM yyyy"); // La plantilla para el tamaño pequeño: TileSquareText01 // Esta plantilla es como la ancha var squareTileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileSquareText01); var squareTileTextAttributes = squareTileXml.GetElementsByTagName("text"); squareTileTextAttributes[0].InnerText = hora; squareTileTextAttributes[1].InnerText = DateTime.Now.ToString("dd/MMM/yyyy"); // La plantilla del icono pequeño la agregamos a la del icono ancho // y así el propio Windows mostrará el texto que corresponda según el tamaño del icono var node = tileXml.ImportNode(squareTileXml.GetElementsByTagName("binding")[0], true); tileXml.GetElementsByTagName("visual")[0].AppendChild(node); var tileNotification = new TileNotification(tileXml); tileNotification.Tag = "ClockW8_eGi"; TileUpdateManager.CreateTileUpdaterForApplication().Update(tileNotification); }
A ver que te explico de este código que ya no te haya explicado… hmmm… no se me ocurre nada…
Bueno, sí, aclararte unas cosas:
tileXml tiene la plantilla del icono ancho, asignamos el texto de la cabecera con la hora y en uno de los textos secundarios indicamos la fecha completa.
squareTileXml tiene la plantilla del icono pequeño y también asignamos la fecha y la hora, pero como el icono es más "chico" la fecha la abreviamos.
node lo usamos para contener el icono pequeño y agregarlo al grande.
tileNotification es el objeto que tenemos que agregar a las notificaciones y como vemos en el constructor, lo creamos a partir de un objeto XmlDocument, concretamente el de la variable tileXml.
Y finalmente llamamos al método compartido CreateTileUpdaterForApplication de la clase TileUpdateManager para actualizar las notificaciones.
Si ejecutas el código tal como lo tenemos ahora, comprobarás que mientras el reloj esté funcionando, cada minuto se actualizará la hora en el icono mostrado en la página de inicio de Windows 8 (en la figura 1 lo puedes ver en la parte superior derecha).
De todas formas, para que comprendas lo que quiero hacer, aquí tienes dos imágenes en las que se muestra el icono mientras se está ejecutando la aplicación (figura 3) con el texto a mostrar y cuando la aplicación está detenida (figura 4) con la imagen que he indicado para esta aplicación.
Figura 3. EL icono mostrando la hora y fecha actual
(actualizada durante la ejecución de la aplicación)
Figura 4. La imagen "normal" de la aplicación.
Como te decía antes, el problema es que al terminar la ejecución de la aplicación el icono mostrará el último valor que asignó (tanto al icono grande como al pequeño) mientras estaba en funcionamiento.
Nota del 8 de diciembre:
Esto que te comento a continuación no soluciona el problema.
Dejo las explicaciones y el código, pero no sirve para lo que se busca, que no es otra cosa que dejar el logo original en el icono (o tile).
Además de que al ponerlo en el método de evento OnSuspending resulta que cada vez que se suspendía la aplicación (el Windows 8 suspende las aplicaciones cuando "cree" que ya no están haciendo algo interesante) se mostraban los logos en el icono y no se actualizaba con la fecha y hora.
Esto se puede arreglar de otra forma (anoche encontré la forma de hacer todo esto de las notificaciones sin siquiera tener un temporizador e incluso que se sigan mostrando cuando la aplicación termine.
La forma de hacer esas notificaciones las publicaré en otro artículo, pero si quieres ir experimentando, decirte que es usando la clase ScheduledTileNotification y agregándola con el método AddToSchedule de TileUpdateManager.CreateTileUpdaterForApplication().El problema es que yo iba probando cosas, pero según parece cuando se "distribuyen" (deploy) las aplicaciones de Visual Studio 2012 en el propio equipo con Windows 8 no se hace correctamente, o al menos a mí me ha dado la impresión de que la aplicación seguía actuando de forma "distinta" a como yo tenía programado (de programar: escribir código).
En fin…Nota del 10 de diciembre:
Aquí tienes el código con un ejemplo que actualiza cada x tiempo el contenido del tile y lo hace hasta cuando tu le digas, incluso si la aplicación está finalizada.
Actualizar cada minuto el icono (tile) de una aplicación de Windows Store hasta la hora indicada
Ahora lo que hay que hacer es asignar a los iconos las imágenes de la aplicación, concretamente las dos usadas en el icono pequeño y en el grande.
Esas imágenes las puedes ver en la carpeta Assets y el nombre predeterminado que tienen (tu se lo puedes cambiar, siempre que también se lo indiques en el fichero Package.appmanifest) son: WideLogo.png para el icono grande y Logo.png para el icono pequeño.
Asignaremos estas imágenes cuando finalice la aplicación… sí, piensa dónde pondrías ese código y veremos si es así… o no…
Veamos el código para asignar las imágenes predeterminadas de los dos iconos (tiles) que estamos modificando. Ese método lo he llamado restaurarTiles.
VB:
Private Sub restaurarTiles() Dim tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideImage) Dim tileImageAttributes = tileXml.GetElementsByTagName("image") TryCast(tileImageAttributes(0), XmlElement).SetAttribute("src", "ms-appx:///Assets/WideLogo.png") TryCast(tileImageAttributes(0), XmlElement).SetAttribute("alt", "WideLogo") Dim squareTileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileSquareImage) Dim squareTileTextAttributes = squareTileXml.GetElementsByTagName("image") TryCast(squareTileTextAttributes(0), XmlElement).SetAttribute("src", "ms-appx:///Assets/Logo.png") TryCast(squareTileTextAttributes(0), XmlElement).SetAttribute("alt", "Logo") Dim node = tileXml.ImportNode(squareTileXml.GetElementsByTagName("binding").Item(0), True) tileXml.GetElementsByTagName("visual").Item(0).AppendChild(node) Dim tileNotification = New TileNotification(tileXml) tileNotification.Tag = "ClockW8clr_eGi" TileUpdateManager.CreateTileUpdaterForApplication().Update(tileNotification) End Sub
C#:
private void restaurarTiles() { var tileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideImage); var tileImageAttributes = tileXml.GetElementsByTagName("image"); ((XmlElement)tileImageAttributes[0]).SetAttribute("src", "ms-appx:///Assets/WideLogo.png"); ((XmlElement)tileImageAttributes[0]).SetAttribute("alt", "WideLogo"); var squareTileXml = TileUpdateManager.GetTemplateContent(TileTemplateType.TileSquareImage); var squareTileTextAttributes = squareTileXml.GetElementsByTagName("image"); ((XmlElement)squareTileTextAttributes[0]).SetAttribute("src", "ms-appx:///Assets/Logo.png"); ((XmlElement)squareTileTextAttributes[0]).SetAttribute("alt", "Logo"); var node = tileXml.ImportNode(squareTileXml.GetElementsByTagName("binding")[0], true); tileXml.GetElementsByTagName("visual")[0].AppendChild(node); var tileNotification = new TileNotification(tileXml); tileNotification.Tag = "ClockW8clr_eGi"; TileUpdateManager.CreateTileUpdaterForApplication().Update(tileNotification); }
¿Desde dónde llamar al método restaurarTiles?
Como te habrás imaginado el sitio desde el que debemos llamar a ese método no es del método de evento equivalente al Form_Unload / Form_Closing de las aplicaciones de escritorio, que en la aplicación de Windows Store "supuestamente" es el método MainPage_Unloaded, pero por lo que yo he probado, a ese método de evento (de MainPage) no llega nunca… o al menos ahí no llega a pararse si pongo un breakpoint… así que… me puse a mirar en los distintos eventos que produce la página y no vi ninguno que me convenciera. Y cuando me pongo a buscar sobre el "life cicle" (ciclo de vida) de una aplicación de Windows Store sólo me he encontrado con que una aplicación puede estar en ejecución, en modo suspendido o no está en ejecución.
También me he encontrado con la información de que si una aplicación el usuario la cierra (pulsando Alt+F4 o mediante la opción de cierre) ésta se suspende durante 10 segundos y después finaliza.
Si quieres saber más sobre esto último puedes ver lo que dicen en este enlace:
Administrar el ciclo de vida y el estado de la aplicación (aplicaciones de la Tienda Windows.
La cuestión es que yo he puesto la llamada a ese método en el evento OnSuspending de la clase App, es decir en el código del fichero App.xaml.
Después de la llamada a restaurarTiles hago una llamada al método Clear de CreateTileUpdaterForApplication para que el tile (o icono) se quede "quietecico" y no siga haciendo cambios entre las últimas notificaciones que ha tenido la aplicación.
Y este es el código:
VB:
Private Sub OnSuspending(sender As Object, e As SuspendingEventArgs) Handles Me.Suspending Dim deferral As SuspendingDeferral = e.SuspendingOperation.GetDeferral() ' TODO: Save application state and stop any background activity deferral.Complete() restaurarTiles() ' Esto es necesario para que no siga actualizándose "a su bola" TileUpdateManager.CreateTileUpdaterForApplication().Clear() End Sub
C#:
private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); //TODO: Save application state and stop any background activity deferral.Complete(); restaurarTiles(); // Esto es necesario para que no siga actualizándose "a su bola" TileUpdateManager.CreateTileUpdaterForApplication().Clear(); }
Nota del 8 de diciembre:
Por favor lee los comentarios que he puesto hoy sábado sobre algunas cosas que en realidad no funcionan como (al menos yo) esperaba.
Nota del 10 de diciembre:
Mira este otro artículo y lo mismo ves que sí funciona esto:
Actualizar cada minuto el icono (tile) de una aplicación de Windows Store hasta la hora indicada
Y ya está… bastante por hoy ¿no?
Pues… casi… pero lo que te voy a contar lo haré en otro "capítulo" de esta novela por entregas… y será para adaptar nuestro reloj a la ventana "snapped", es decir, cuando el usuario arrastra la ventana principal a uno de los laterales de la pantalla…
Nos vemos.
Guillermo
Enlaces a los otros artículos de este "paso a paso":
- Usar un temporizador en las aplicaciones de Windows store (Visual Basic)
- Usar un temporizador en las aplicaciones de Windows store (C#)
- Pon una AppBar en tu aplicación de Windows Store
- Acceder a los recursos de una aplicación de Windows Store desde código (vb, c#, xaml)
- Actualizar el icono (tile) de una aplicación de Windows Store (cada minuto)
- Saber cuando la aplicación está en modo Snapped y actuar en consecuencia
—
Actualizaciones:
Resulta que no todo lo dicho aquí es "operativo" y por tanto, hay que rectificar…
El 7 de diciembre de 2012 a las 00:20 publiqué el original.
El 8 de diciembre a las 19:50 publiqué las rectificaciones / actualizaciones.
El 10 de diciembre a las 07:06 pongo el enlace al nuevo artículo con un ejemplo "operativo" y sin usar timer.
…