Categoría: Trucos

Mejorar el rendimiento del canvas HTML5 desde C#

IMG_4102
Desarrollar aplicaciones para la Tienda Windows en JavaScript tiene sus ventajas e inconvenientes. Aunque en la mayoría de los casos no lo vamos a notar, cuando tratamos grandes cantidades de datos y tenemos que realizar modificaciones sobre estas, lo más probable es que se penalice el rendimiento.

Estoy desarrollando la nueva versión de Fingerpaint y quiero añadir una herramienta de relleno de color.
No es complicado de hacer, hay bastantes algoritmos bien optimizados y el context tiene los métodos getImageData y putImageData que nos dan el array de datos para modificar el contenido del canvas a nivel de pixel:

width = context.canvas.width;
height = context.canvas.height;
imageData = context.getImageData(0, 0, width, height);
data = imageData.data;
for(var i=0;i<data.length;i++){
  //tratar los píxeles de data uno por uno
}

El problema de hacer esto en JavaScript es que el lenguaje no está optimizado para este tipo de operaciones e intentar hacer un relleno un poco inteligente (estilo cubo de pintura) va a tardar mucho más de lo que estamos acostumbrados con cualquier aplicación de dibujo moderna.

Componentes Windows Runtime (WinRT)

Por suerte, mi aplicación FingerPaint es para Windows 8 (en realidad 8.1) y puedo usar librerías desarrolladas en otros lenguajes, como C# o C++, que me permitirán realizar estas operaciones mucho más rápido.
Es muy fácil tener código en otros lenguajes dentro de una aplicación JavaScript de la tienda Windows, sólo tenemos que añadir una nueva librería de tipo Windows Runtime Component en el lenguaje que prefiramos. Con este tipo de proyectos estamos indicando que es una librería que puede utilizarse desde cualquier otro lenguaje.

Lo que estamos haciendo es crear una librería con WinMD, el mismo sistema de metadatos que las librerías de sistema en Windows Runtime. En esta imagen creamos un nuevo componente en C#:
Windows Runtime Component

A continuación tenéis el ejemplo de código. Está dividido en dos métodos:

public sealed class Fill
{
   private byte[] _data;

   public void BeginFill(int x, int y, int width, int height, 
       [ReadOnlyArray] byte[] bitmap, string hexColor, byte tolerance)
   {
     _data=bitmap;
     //
     //aquí irá mi código de flood fill que modificará los datos
     //dentro de _data, pero al ser ReadOnly no volverán a JavaScript
     //modificados.
     //
   }

   public void EndFill([WriteOnlyArray] byte[] target)
   {
     _data.CopyTo(target,0);
   }
}

Si os fijáis, la clase es sealed y los parámetros array están decorados con los atributos Read/WriteOnlyArray. Estas dos características son requisito indispensable para que nuestra clase y métodos se puedan utilizar desde cualquier otro lenguaje.

Añadiremos el proyecto que hemos creado en las referencias de nuestra aplicación JavaScript y desde el código JavaScript las llamadas se verán así, como si fueran llamadas a cualquier otra librería JavaScript, pero tú y yo sabemos que está hecho en C#:

width = context.canvas.width;
height = context.canvas.height;
imageData = context.getImageData(0, 0, width, height);
data = imageData.data;
//creamos un objeto Fill
var helper=new ImageHelpers.Fill();
//calculamos el fill
helper.beginFill(x,y,width,height,data,"#00ff00",50);
//escribimos el resultado en imageData
helper.endFill(data);
//escribimos el resultado en el canvas
context.putImageData(imageData, 0, 0);

Para poder modificar el canvas necesitamos que nuestro método externo modifique directamente el array obtenido con getImageData, pero cuando trabajamos con componentes de WinRT y queremos pasar un array sólo podemos hacerlo en uno de estos dos modos:

  • ReadOnlyArray: trabajamos sobre una copia del array y cualquier modificación no se traspasará entre los contextos, ni siquiera si devolvemos el array como valor de retorno
  • WriteOnlyArray: podemos escribir sobre el array original… pero este siempre viene vacío, no veremos la información original

Adicionalmente, canvas no nos permite introducir un array de datos dentro de imageData que no haya sido creado por el propio canvas, así que si utilizáramos un parámetro de retorno tendríamos que copiar uno por uno los bytes del array, con lo que perderíamos el rendimiento que ganamos por hacerlo en C#.
Entonces, y por eso el artículo de hoy, tenemos que realizar la operación en dos pasos: en el primero obtenemos los datos originales y en el segundo escribimos sobre la memoria del canvas desde C#. Estas operaciones son bastante rápidas y casi no penalizan el rendimiento (entre 1 y 3 milisegundos en un i5), así que nos queda bastante margen para modificar el contenido del canvas y mejorar considerablemente la velocidad del fill con nuestro método C#.

Depuración híbrida

Para poder depurar nuestro código C# dentro de una aplicación JavaScript tendremos que indicar en las propiedades del proyecto qué tipo de lenguaje queremos depurar, a alguien se le olvidó poner Managed+Script así que por ahora sólo podremos depurar en uno o en otro, si hubiéramos elegido C++ como lenguaje no tendríamos este problema, pero tendríamos otros :P.
Script or Managed debug

Happy finger painting!

Anuncios

Compartir sin contratos en Windows 8

Thumbs Up - Like
Ya sabemos que en Windows 8 y 8.1 existe una forma muy buena de compartir contenido: a través del contrato de compartir. En las presentaciones y documentación que hay sobre el tema descubrimos la enorme ventaja que supone el contrato de compartir: sólo lo programas una vez, luego es el usuario que decide con qué aplicación quiere compartir. Es decir, si el usuario tiene Facebook, Twitter, Linkedin, WordPress o lo que sea que utilice para compartir su vida, tendrá probablemente una/varias/todas esas Apps instaladas en su equipo y es la propia aplicación de la red social preferida del usuario que sabe cómo compartir el contenido en esa red.
Gracias a esto, nosotros programamos la función compartir que proporciona lo que nuestra aplicación sabe compartir: una imagen, un enlace web, un texto, un archivo, etc., o todo a la vez, y cuando esa información llega hasta la App que elija el usuario es esa App la encargada de publicar el contenido en la red social de turno.

Compartir en Windows 8

Todo eso se hace con la clase DataTransferManager que nos permitirá compartir nuestro contenido en unas pocas líneas, tal como se reproduce en el ejemplo:

 function setupShare() {
        var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
        dataTransferManager.addEventListener("datarequested", function (e) {
            var request = e.request;
            request.data.properties.title = "Share Demonstration";
            request.data.setText("Hello World!");
        });
    }

Vale, pero aún así yo quiero compartir en una red en concreto desde mi App

Si todavía no te ha convencido el sistema, o no te fías de que el usuario se haya descargado la App, nada te impide utilizar las diferentes APIs de cada plataforma para compartir contenido (o poner Me Gusta, Favoritos, etc.). Normalmente necesitas:

  1. Darte de alta como desarrollador de la plataforma
  2. Obtener una clave de API
  3. Realizar una conexión para obtener un token
  4. Mantener es token hasta que caduque y solicitar uno nuevo cada vez que ocurra esto.
  5. Finalmente llamar a una url que te deja compartir

O puede… ser aún peor, necesitas que el usuario haga un login en la plataforma y te de permiso para hacer un montón de cosas con su cuenta, en su nombre. No hay problema, también Windows 8/8.1 te permite simplificar eso con el WebAuthenticationBroker del que os hablaré otro día.

Y ¿no hay una forma más fácil?

Por suerte, muchas plataformas te permiten lanzar una url que va a su web, el usuario hace el login directamente en la plataforma y luego acepta la publicación, sin necesidad de clave de api ni de más programación por nuestra parte que lanzar una simple url. Aquí van dos ejemplos en C#, aunque en JavaScript no serán muy diferentes:

  • Con Facebook no se puede hacer directamente un like, tendrías que poner un control WebBrowser e incrustar el botón de Like, pero en cambio puedes hacer muy fácilmente un share:
    string url = "https://jmservera.com/2013/10/07/optimizar-el-canvas-de-apps-windows-en-html5javascript/";
    
    string shareUrl = string.Format("https://www.facebook.com/sharer/sharer.php?u={0}",
        System.Net.WebUtility.UrlEncode(url));
    
    await Windows.System.Launcher.LaunchUriAsync(
        new Uri(shareUrl));
    
  • En twitter podrías hacer algo así al pulsar el botón:
    string url = "https://jmservera.com/2013/10/07/optimizar-el-canvas-de-apps-windows-en-html5javascript/";
    string texto = "Cómo optimizar código JS en Canvas";
    
    string shareUrl = string.Format("http://www.twitter.com/intent/tweet?url={0}&text={1}",
        System.Net.WebUtility.UrlEncode(url),
        System.Net.WebUtility.UrlEncode(texto));
    
    await Windows.System.Launcher.LaunchUriAsync(
        new Uri(shareUrl));
    

¿Y si quiero poner un Like?

Facebook te da un código para incrustar en tu página, para que puedas poner un Like de la misma, pero eso no funcionará directamente en una App de la Tienda Windows, ni siquiera en las Apps HTML5/JavaScript. Ya sabréis que las Apps no permiten ejecutar código externo directamente… a no ser que las metamos en un iFrame o en el nuevo WebView, en este ejemplo en versión HTML5:

<x-ms-webview id="webview"
                  height="35" width="300"
                  src="http://www.facebook.com/plugins/like.php?href=http%3A%2F%2Fjmservera.com&amp;width=300&amp;height=35&amp;colorscheme=dark&amp;layout=standard&amp;action=like&amp;show_faces=false&amp;send=false&amp;"
                  style="border:none; overflow:hidden; width:300px; height:35px;background-color:rgb(29,29,29)">
    </x-ms-webview>

Espero que os sirvan.
Happy sharing!

Optimizar el Canvas de Apps Windows en HTML5/JavaScript

En un artículo anterior sobre el patrón Memento en JavaScript introducía cómo podíamos ir guardando el estado de un elemento canvas para crear un sistema de deshacer/repetir en una app de dibujo. En dicha app había recibido algunas quejas sobre el rendimiento a partir de incluir esta funcionalidad, especialmente al ejecutar la aplicación en tabletas ARM con Windows RT.

Si observáis el código, obtenemos una copia de la imagen dibujada sobre el canvas con la siguiente llamada:

var img = this._ctx.getImageData(x, y, w, h);

En un principio pensé que el problema estaba en capturar toda la pantalla cada vez, pero intentar resolver eso complicaba mucho el desarrollo y tampoco se ganaba en rendimiento, sino todo lo contrario. Ya escribiré sobre ello en otro post.

En realidad el problema está en el rendimiento de los métodos getImageData y putImageData, que si buscáis un poco por internet os recomendarán utilizarlo lo menos posible, o sea, que ni se te ocurra hacerlo en un bucle. El caso es que aparte de algún artículo donde analizan el rendimiento de dicho método y de cómo algunos navegadores lo mejoran, para guardar la imagen de un canvas os dirán que tenéis que utilizar alguno de los siguientes métodos:

  • getImageData o getImageDataHD: copia todos los píxeles a un array, normalmente para poder modificar a nivel de píxel y luego volver a volcar al canvas con un putImageData.
  • toDataURL o toBlob: para guardar los datos, incluso con compresión png, para poder enviarlos a un servidor (aunque esto en Windows8 lo podemos hacer con métodos nativos).

El problema de estos métodos es que tienen un rendimiento bastante pobre, y si tenemos que copiar toda la pantalla cuando el usuario levanta el dedo de la puede que afecte al rendimiento de nuestras rutinas de dibujo, de hecho en mi App estaba afectando y mucho. Debemos recordar que muchos usuarios tendrán una versión RT con un chip ARM que no tiene el rendimiento de un desktop.

Canvas inception

Hay otra forma de guardar el estado de un canvas cuando no necesitamos actuar sobre píxeles individuales o guardar el contenido a un archvivo: pintando dentro de un canvas el contenido del canvas original.
Para guardar el canvas, basta crear uno nuevo de las mismas dimensiones y ejecutar el método drawImage que, además de pintar imágenes, si le damos otro canvas como parámetro pintará el contenido del canvas origen dentro del destino:

var newCanvas=document.createElement("canvas");
var newContext=newCanvas.getContext("2d");

var w=originalCanvas.width;
var h=originalCanvas.height;

newCanvas.width=w;
newCanvas.height=h;

newContext.drawImage(originalCanvas,0,0,w,h,0,0,w,h);

¿Tánto se gana?

Se puede ganar mucho rendimiento con este pequeño cambio. En algunos casos la mejora es tan espectacular que es difícil de creer, pero para que lo comprobéis vosotros mismos aquí os dejo este jsfiddle:
imagecopy

Luego, si queremos recuperar la imagen sólo tenemos que volver a pintar sobre el canvas original el canvas que hemos guardado. Aunque debemos ir con cuidado, no es conveniente ir creando infinitos canvas y esperar que el Garbage Collector vaya recogiendo lo que no necesitamos, aparte de que algunos navegadores no estarán demasiado contentos con eso. Es mucho mejor crear un pool de elementos canvas e ir reutilizándolos. Si queréis ver cómo ha quedado el nuevo patrón memento ahí tenéis el código, aunque por culpa del pool de canvas no está tan desacoplado como me gustaría:

(function mementoNS(global) {
    global.CanvasState = global.CanvasState || {};
})(this);

(function mementoInit(CanvasState) {
    CanvasState.memento = function (state) {
        this._state = state;
    };
    Object.defineProperty(CanvasState.memento.prototype, "state", {
        get: function () { return this._state; },
        set: undefined
    });
})(this.CanvasState);

//this is a buffer pool to avoid possible memory problems
(function bufferPoolInit(CanvasState) {
    CanvasState.bufferPool = {
        _buffer:[],
        addLevels: function (levels) {
            var buffer = CanvasState.bufferPool._buffer;
            for (var i = 0; i < levels; i++) {
                buffer.push([document.createElement("canvas"), false]);
            }
        },
        getBuffer: function () {
            var buffer = CanvasState.bufferPool._buffer;
            for (var i = 0; i < buffer.length; i++) {
                if (!buffer[i][1]) {
                    buffer[i][1] = true;
                    return { id: i, buffer: buffer[i][0] };
                }
            }
        },
        releaseBuffer: function (id) {
            CanvasState.bufferPool._buffer[id][1] = false;
        }
    }

})(this.CanvasState);

(function originatorInit(CanvasState) {
    CanvasState.originator = function (canvas) {
        this._canvas = canvas;
        this._ctx = canvas.getContext("2d");
    };
    CanvasState.originator.prototype.saveToMemento = function (x, y, w, h) {
        if (x === undefined)
            x = 0;
        if (y === undefined)
            y = 0;
        if (w === undefined)
            w = this._canvas.width;
        if (h === undefined)
            h = this._canvas.height;
        var buffer = CanvasState.bufferPool.getBuffer();
        var bufferContext = buffer.buffer.getContext("2d");
        buffer.buffer.width = w;
        buffer.buffer.height = h;
        bufferContext.drawImage(this._canvas, x, y, w, h, 0, 0, w, h);
        var memento = new CanvasState.memento({ image: buffer, x: x, y: y, w: w, h: h });
        return memento;
    };
    CanvasState.originator.prototype.restoreFromMemento = function (memento) {
        var state = memento.state;
        this._ctx.clearRect(state.x,state.y,state.w,state.h);
        this._ctx.drawImage(state.image.buffer, state.x, state.y);
    };
})(this.CanvasState);

(function caretakerInit(CanvasState) {
    CanvasState.caretaker = function (maxLevels) {
        CanvasState.bufferPool.addLevels(maxLevels+1);
        this._undoStates = [];
        this._redoStates = [];
        this._maxLevels = maxLevels;
    };
    CanvasState.caretaker.prototype.addMemento = function (memento) {
        this._undoStates.push(memento);
        this._redoStates.forEach(function (s) {
            CanvasState.bufferPool.releaseBuffer(s._state.image.id);
        });
        this._redoStates = [];
        if (this._undoStates.length > this._maxLevels) {
            CanvasState.bufferPool.releaseBuffer(this._undoStates[0]._state.image.id);
            this._undoStates.splice(0, 1);
        }
    };
    CanvasState.caretaker.prototype.getUndoMemento = function () {
        if (this._undoStates.length > 1) {
            var state = this._undoStates.pop();
            this._redoStates.push(state);
            return this._undoStates[this._undoStates.length-1];
        }
        else
            throw "Undo not allowed, states array empty";
    };
    CanvasState.caretaker.prototype.getRedoMemento = function () {
        if (this._redoStates.length > 0) {
            var state = this._redoStates.pop();
            this._undoStates.push(state);
            return state;
        }
        else
            throw "Redo not allowed, states array empty";
    }
    Object.defineProperty(CanvasState.caretaker.prototype, "canUndo", {
        get: function () { return this._undoStates.length>1; },
        set: undefined 
    });
    Object.defineProperty(CanvasState.caretaker.prototype, "canRedo", {
        get: function () { return this._redoStates.length > 0; },
        set: undefined
    });
})(this.CanvasState);

Cambios en la publicación de Juegos para Windows Store

Si habéis publicado algún juego en la Tienda Windows sabréis que hace falta un archivo GDF para la clasificación por edades en los distintos países donde se publica la aplicación. También habréis sufrido la locura de tener que crear una dll de C++ para embeber el archivo GDF aunque vuestro juego esté hecho en C# o JavaScript.

Bien, pues hace muy poco han cambiado las reglas y el archivo GDF se sube a la tienda por separado, ya no hace falta generar la dll sino que al subir el paquete del juego a la tienda tenemos un lugar en el portal donde cargar el GDF y los certificados pertinentes:

Image

Cuidado con las instrucciones en la web en castellano pues todavía no se han traducido. En el msdn inglés ya lo tenéis: http://msdn.microsoft.com/en-us/library/windows/apps/hh465153.aspx

Warning : DEP0810 : This app references Microsoft.WinJS.1.0 …

Si os aparece este warning en el Visual Studio 2012 cuando desarrolláis aplicaciones JavaScript para la Tienda Windows, significa que la versión de WinJS que tenéis en el VS no coincide con la que está instalada en el equipo. Lo más probable es que os falte una actualización de las extensiones de JavaScript.

El pasado día 8 actualizaron la versión de WinJS a 1.0.9200.20789. La podéis encontrar aquí: http://www.microsoft.com/en-us/download/details.aspx?id=30687

Reducir el tamaño de las aplicaciones JavaScript de la Tienda Windows

Compressed Car
Cuando utilizo librerías externas en mis aplicaciones de la tienda Windows suelo tirar de NuGet, es la forma más cómoda de encontrar e instalar las librerías más comunes, pero tiene dos pequeñas pegas que conviene saber antes de publicar las aplicaciones:

UTF-8

Muchas de las librerías vienen con un código de página diferente de UTF-8 + BOM. Resulta que el Kit de pruebas de Windows 8 detecta que si la página de código no es esta última baja el rendimiento y nos dará error de certificación. Este kit es el mismo que se pasa como proceso automatizado en la tienda, así que si no pasa en nuestro equipo tampoco pasará la certificación en la tienda. Resolverlo es muy sencillo, basta con volver a guardar el archivo .js como UTF8 + BOM. Para ello abrimos el archivo y en el menú de archivo seleccionamos Advanced Save Options…:
Advanced Save Options...

En las opciones avanzadas podremos cambiar el Encoding a UTF-8 with signature:
Unicode UTF-8 with signature

Archivos innecesarios

En el caso de librerías de JavaScript, los paquetes de NuGet suelen tener más archivos que la propia librería. Pueden incluir, por ejemplo, la versión completa para que podamos depurar, la versión minimizada para desplegar, algún Readme y otros archivos para que funcione mejor el intellisense de JavaScript. En el siguiente ejemplo tenemos la última versión de la librería jquery, en producción sólo necesitaremos el archivo jquery-2.0.3.min.js, el resto podemos eliminarlos del paquete, pero nos vendrá bien mantenerlos en la solución para poder depurar y tener intellisense:

Unused jquery files

Para evitar que esos archivos se incluyan en el paquete, sólo tenemos que cambiar la propiedad Package Action del archivo a None:
Package Action: none

Una vez hecho esto con todos los archivos que hemos importado con NuGet ya tendremos la solución limpia y mejor preparada para publicar en el Windows Store.

Las nuevas características de búsqueda en Windows 8.1

Windows 8.1 Preview presenta grandes novedades con respecto a la versión actual. Una que me ha chocado mucho es el nuevo sistema de búsqueda. Tiene cosas muy buenas y otras no tanto, pero tengamos en cuenta que todavía es una versión preliminar y puede cambiar mucho hasta el lanzamiento.

found dog

Desde el punto de vista del desarrollador, en la versión anterior era muy fácil y directo integrarse con el contrato de búsqueda; permitía a los usuarios buscar dentro de nuestra aplicación aunque ésta estuviera cerrada, haciendo que nuestra aplicación estuviera siempre presente.

Cookbook search old

A pesar de los esfuerzos de Microsoft por explicar las bondades del sistema de búsqueda, en el mundo real™ los usuarios no acababan de entender el sistema o simplemente no encontraban el botón de búsqueda dentro de la aplicación y pensaban que la aplicación no podía buscar. En las aplicaciones no había ninguna evidencia de que pudieran buscar y al final poníamos un botón dentro de la aplicación que abría el charm de búsqueda. Además, cuando empiezas a tener muchas aplicaciones que implementan el contrato de búsqueda la lista de posibilidades se vuelve demasiado larga para ser manejable.

Por este motivo y porque la funcionalidad del búsqueda dentro de cualquier aplicación sólo la utilizaba el 7% de los usuarios, el equipo de Windows decidió cambiar el sistema de búsqueda para mejorarlo y hacerlo más eficiente.

¿Dónde están las Apps?

El nuevo charm de búsqueda ya no muestra una lista de aplicaciones disponibles. En su lugar nos permite buscar dentro de todo el sistema, integrando los resultados del contenido indexado en nuestro equipo con otros datos devueltos por Bing, Xbox Music y otros.
Las aplicaciones antiguas que implementan el contrato de búsqueda de Windows 8 no perderán esa funcionalidad; podremos seleccionar la aplicación para que nos devuelva la página de resultados, pero sólo nos aparecerá esta opción cuando sea la aplicación activa y no estará seleccionada por defecto, pero esto último se puede cambiar.

cookbok search 0

Una vez seleccionada nuestra aplicación en lugar del “Search everywhere” podremos seguir usándola como antes, aunque no tendremos la posibilidad de cambiar a otra aplicación:

Cookbok search

¿Cómo haremos para que se pueda buscar dentro de nuestra aplicación desde cualquier sitio?

Está algo más escondido para nosotros, pero en realidad será mejor que antes, pues parece ser que podremos ofrecer resultados dentro de la búsqueda general. Todavía no está disponible en la preview que podemos probar ahora, si bien en esta charla del Build podemos ver cómo será.
Además, la funcionalidad “Search everywhere” permite encontrar nuestra aplicación si el usuario la tiene instalada y, como me suele ocurrir mucho, no recuerda exactamente el nombre:

search everywhere

¿Y cómo implemento ahora las búsquedas?

Podemos optar por seguir haciéndolo como antes o bien utilizar los nuevos controles y capacidades de búsqueda.

Charm de búsqueda

En el caso de que queramos seguir con la implementación antigua, lo recomendación es que pongamos un botón en la zona superior derecha que abra el charm de búsqueda, así nuestros usuarios sabrán que nuestra aplicación puede buscar. Al ejecutar el siguiente código, el charm estará enfocado en nuestra aplicación en lugar de en el “Search everywhere”, e incluso podemos poner un término de búsqueda por defecto:

Windows.ApplicationModel.Search.SearchPane.getForCurrentView().show("buscar...");

Todavía tenemos una opción mejor, hacer que el cuadro de búsqueda aparezca al empezar a escribir, tal como hacen las aplicaciones de calidad y la pantalla de inicio. El código para añadir esta funcionalidad es muy sencillo:

Windows.ApplicationModel.Search.SearchPane.getForCurrentView().showOnKeyboardInput = true;

El control SearchBox

Si queremos ajustarnos a las nuevas recomendaciones de búsqueda, el equipo de Windows nos lo ha puesto muy fácil. Tenemos un nuevo control de búsqueda que tiene eventos muy parecidos al contrato de búsqueda, pero es un control visual que podemos poner en nuestro UI, tanto para XAML como para HTML:

<div class="pagesearch" data-win-control="WinJS.UI.SearchBox"></div>

Una vez en nuestra página, podremos usar el código que utilizábamos antes en nuestro contrato de búsqueda que utilizaba la consulta:

var search = element.querySelector(".pagesearch").winControl;
search.onquerysubmitted = function (e) {
    console.log(e.detail.queryText);
};

Sólo que ahora tendremos que navegar nosotros a la página de resultados.

También podemos añadir sugerencias de búsqueda:

search.onsuggestionsrequested = function (e) {
    var query = e.detail.queryText.toLowerCase();
    var suggestionCollection = e.detail.searchSuggestionCollection;
    var suggestionList = ["Almería","Albacete", "Alicante"];
    for (var i = 0, len = suggestionList.length; i < len; i++) {
        if (suggestionList[i].substr(0, query.length).toLowerCase() === query) {
            suggestionCollection.appendQuerySuggestion(suggestionList[i]);
        }
    }
};

Activar el foco del control al usar el teclado en la aplicación:

search.winControl.focusOnKeyboardInput = true;

Y ofrecer resultados recomendados con imágenes y separadores para identificarlos del resto, lo que dará un toque de calidad a nuestra aplicación:

search.onsuggestionsrequested = function (e) {

    var queryText = e.detail.queryText,
        query = queryText.toLowerCase(),
        suggestionCollection = e.detail.searchSuggestionCollection;
    var suggestionList = [{
        name: "Almería",
        thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Bandera_de_Almer%C3%ADa.svg/200px-Bandera_de_Almer%C3%ADa.svg.png"
    },
        {
            name: "Albacete",
            thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Albacete-Bandera.svg/200px-Albacete-Bandera.svg.png"
        },
        {
            name: "Alicante",
            thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_Alicante.svg/200px-Flag_of_Alicante.svg.png"
        }];

    for (var i = 0, len = suggestionList.length; i < len; i++) {
        if (suggestionList[i].name.substr(0, query.length).toLowerCase() === query) {
            if (suggestionCollection.size <2) {
                var reference = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(
                    new Windows.Foundation.Uri(suggestionList[i].thumb));
                suggestionCollection.appendResultSuggestion(suggestionList[i].name,
                    "El mejor resultado!", "id1", reference, "mejor resultado");
            }
            else {
                if (suggestionCollection.size == 2)
                    suggestionCollection.appendSearchSeparator("Más sugerencias");
                suggestionCollection.appendQuerySuggestion(suggestionList[i].name);
            }
        }
    }
};

sugerencias

Por lo que podemos ver es un control muy completo, que además cachea las últimas búsquedas para ofrecerlas también como sugerencia.

El indexador de Windows

Además del nuevo control de búsqueda, podemos añadir y consultar elementos con el indexador de Windows mediante la clase ContentIndexer. Antes de que os frotéis las manos como hice yo, leed la explicación que aparece en la documentación:

Content is separated per app package, per user, and a package can’t query another package’s data.

Podemos utilizar la potencia del indexador de Windows, pero sólo para y por nuestra aplicación, los datos que añadamos al índice son privados para nuestra aplicación y usuario.

Dicho esto,

  • La clase ContentIndexer nos permite añadir elementos al índice de Windows, de manera que podremos aprovechar la potencia del indexador de Windows dentro de nuestra aplicación. Por ahora el contenido que añadamos al índice será sólo para nuestra aplicación, el resto de las aplicaciones no pueden verlo.
  • También podemos añadir contenido indexado utilizando carpetas locales y creando ficheros appcontent-ms.
  • Con las clases del espacio de nombres Windows.Storage.Search podremos realizar búsquedas dentro del contenido indexado utilizando Advanced Query Syntax, pero sólo del contenido propio.
  • Todavía no sabemos cómo podremos hacer para aparecer dentro de las búsquedas generales de Windows Search.

Conclusiones

Tras estudiar el uso que hacían de la búsqueda los usuarios de las aplicaciones de la tienda Windows, el equipo de Windows se dio cuenta que sistema necesitaba un cambio. Este cambio ha tenido algunas consecuencias pero tenemos la promesa de que va a mejorar. Mientras tanto, ya podemos aprovechar la potencia que nos proporcionan los nuevos controles y funcionalidades del sistema de búsqueda.

Referencias

Abrir en nueva ventana en Windows 8.1

Abrir en nueva ventana en Windows 8.1

La versión de Tienda Windows de IE 11 te da la opción de abrir enlaces en otra pestaña y también en una ventana nueva. Gracias al sistema de división flexible de pantallas de Windows 8.1, con la segunda opción se abre la nueva ventana al lado de la anterior, permitiendo así la navegación en múltiples paginas simultáneamente.

Este ha sido mi primer comentario realizado íntegramente con una tableta Surface RT, pantallazo incluído y utilizando únicamente el teclado en pantalla. La verdad es que me está resultando más cómodo de lo que me pensaba. Saludos desde la playa 😎.

Importar estilos XAML en otro proyecto Windows Store

Si queremos compartir los estilos entre diferentes librerías en las aplicaciones de Windows Store debemos tener en cuenta que los estilos no estarán embebidos dentro de la librería tal como ocurre en las aplicaciones WPF, sino que se guarda el XAML dentro de una carpeta con el nombre de la librería. Esta diferencia implica que el enlace al XAML no será igual.

Así, si nuestra librería referenciada es MiLibreria.Estilos y tenemos el estilo en el raíz, deberemos escribir el ResourceDictionary de la siguiente manera:

<ResourceDictionary Source="ms-appx:///MiLibreria.Estilos/StandardStyles.xaml"/>

Es fácil de comprobar, pues al compilar nuestro proyecto veremos como dentro de nuestra carpeta de compilación se crea una carpeta para la librería referenciada que contiene los archivos XAML.

Consultas asíncronas LINQ mediante extensiones

Una parte que falta por ahora en LINQ es una forma directa de ejecutar una query de forma asíncrona.
Si tenemos una query cualquiera como por ejemplo:

var query = from media in context.MediaFiles
           select new MediaItem
           {
               Title=media.Title,
               Path = media.Path,
               MimeType = media.MimeType,
               Date=media.CreationTime,
               Size=media.Length
           };

Para ejecutar la consulta en modo asíncrono no tenemos ningún método asíncrono directamente (no confundir con paralelo como en PLINQ) y tenemos que meterlo en una Task:

var result = await Task<List<MediaItem>>.Run(() =>
{
    return query.ToList();
});

Para no tener que escribir tantas veces la creación de Task podemos meter ese código en un método de extensión, gracias a que tenemos generics:

public static class QueryExtensions
{
    public static Task<List<T>> ToListAsync<T>(this IQueryable<T> query)
    {
        return Task<List<T>>.Run(() =>
        {
           return query.ToList();
        });
    }
}

Como bien dice Kenneth en el comentario, en la extensión no nos hace falta el async/await, pues la Task es por definición “awaitable”

Ahora ya podemos escribir la llamada de forma más clara:

var mediaFiles = await query.ToListAsync();

Con este sistema podremos ir creando todos los que necesitemos en una clase de extensión para su uso con cualquier elemento IQueryable.