Número #24 de CompartiMOSS y nueva colaboración con la revista

Muy buenas a todos,

El pasado miércoles durante el CEUS by Iberian Conference se presentó un nuevo número de la revista especializada en SharePoint CompartiMOSS. En esta ocasión, la revista trae mucho contenido interesante y cada vez más comienzan a aparecer artículos relacionados con Azure, Machine Learning, SharePoint OnLine y siempre sin olvidar la versión de SharePoint OnPremise.

numero24

Para acceder al número de la revista, podéis hacer click aquí. Os dejo también acceso a todo el contenido del número de la revista, para que os podáis hacer una idea del contenido de la misma.

Una vez más he dejado mi pequeña aportación a esta revista que espero que os guste. En esta ocasión os escribo sobre el nuevo modelo de desarrollo que se propone desde Microsoft para SharePoint, tanto la versión OnLine como la versión OnPremise. Algo ya he comentado por aquí, con algunos artículos sobre el tema que iban saliendo mientras preparaba el artículo.

Para terminar muchas gracias a todos los que hacen posible que esta revista siga adelante.

Un saludo y hasta la próxima

Anuncios

Search Driven Development. Usando la API REST de SharePoint para el servicio de búsqueda

Muy buenas a todos,

Recientemente he publicado algunos artículos sobre Search Driven Development para SharePoint Online y SharePoint 2013. Además hace poco tiempo tuve la oportunidad de participar en una mesa redonda con la gente de MadPoint en la que se hablaba de técnicas de desarrollo tanto para SharePoint OnLine como 2013 y Office 365. En esta mesa redonda salió la posibilidad de usar en los desarrollos la API REST del servicio de búsquedas de SharePoint y los nuevos frameworks de Javascript para crear nuestra propia solución basada en búsquedas.

Hasta entonces no había tenido la oportunidad de probar esta API REST, pero hoy os traigo una entrada en la que os quiero enseñar como usar las búsquedas a través de la API REST que nos proporciona esta versión de SharePoint.

En una primera parte de la entrada os enseñaré que consultas REST podemos hacer al servicio y lo que nos devuelve y a continuación veremos ejemplos de cómo utilizar el mismo en una app for SharePoint (o SharePoint Add-ins??).

Llamadas REST para el servicio de búsqueda

Uno de los aspectos importantes que tenemos que tener en cuenta cuando trabajamos con la API REST del servicio de búsqueda es que las consultas al mismo podemos hacerlas a través de peticiones GET y a través de peticiones POST. Esto es así, porque como es sabido por todos, las peticiones GET tienen limitaciones en cuanto al número de caracteres y puede ocurrir que una consulta que queramos hacer sobrepase ese límite, por lo que tendremos la oportunidad de usar las peticiones POST para superar dicha restricción.

A continuación voy a enseñar algunos ejemplos básicos de cómo utilizar ambos tipos de peticiones, aunque las posibilidades que tenemos a la hora de hacer las peticiones son muy extensas. Para esto os dejo el enlace a la referencia de la MSDN, donde podréis analizar al completo las opciones de la API.

https://msdn.microsoft.com/es-es/library/office/jj163876.aspx

Usando peticiones GET

Las peticiones GET se hacen a través de la siguiente URL:

http://servidor/_api/search/query

Consulta con un texto a buscar
/_api/search/query?querytext=’textoAconsultar’
Consulta usando una query
/_api/search/query?querytemplate='{searchterms} FileExtension: doc’
Usando la ordenación
/_api/search/query?querytext=’textAconsultar’&sortlist=’created:ascending,rank:descending,’
Indicando los refinadores a utilizar
/_api/search/query?querytext=’textAconsultar’&refiners=’author,size,fileExtension’
Filtrando la consulta usando el refinamiento
/_api/search/query?querytext=’textAconsultar’&refinementfilters=’fileExtension:equals(“docx”)’

Usando peticiones POST

Las peticiones POST se hacen a través de la siguiente URL:

http://servidor/_api/search/postquery

Consulta con un texto a buscar
{'request':
    { 
      '__metadata' : {'type' : 
                   'Microsoft.Office.Server.Search.REST.SearchRequest'},
      'Querytext' : 'ejemploAconsultar'
    }
}

Son muy importantes las mayúsculas y minúsculas en las consultas usando el método POST. Por ejemplo, en la consulta anterior, tenemos que usar ‘Querytext’ y no ‘QueryText’ o ‘querytext’ como hacemos en la consulta GET.

Estas consultas devuelven un JSON con toda la información necesaria de la búsqueda. Al igual que el resto de servicios REST, este de búsquedas es compatible con JSON Light, por lo que si lo deseeamos podemos obtener respuestas más ligeras y rápidas. Los resultados se encuentran en el objeto devuelto en:

data.body.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results (para GET)
data.body.d.postquery.PrimaryQueryResult.RelevantResult.Table.Rows.results (para POST)

Este elemento “results” json es un array que contiene una fila para cada uno de los resultados, en cada columna o celda de esta fila tenemos una de las propiedades del objeto devuelto. Para simplificar un poco la tarea, os voy a indicar en qué posición se encuentra la información que habitualmente es más importante de los objetos devueltos.

  • Posición 3: Title
  • Posición 4: Author
  • Posición 5: Size
  • Posición 6: Path
  • Posición 7: Description
  • Posición 8: Write
  • Posición 11: HitHighlightedSummary
  • Posición 18: FileExtension
  • Posición 31: FileType

El mismo servicio, si le hemos indicado para qué propiedades queremos obtener refinadores, nos devuelve los refinadores para esa búsqueda en:

data.body.d.query.PrimaryQueryResult.RefinementResults.Refiners.results

Esto nos devuelve, para cada refinador, un objeto de la siguiente forma:

{'Entries':
    {'results'
         {
             RefinementCount: count,
             RefinementToken: token,
             RefinementName: name,
             RefinementValue: value
         },
         {
             RefinementCount: count,
             RefinementToken: token,
             RefinementName: name,
             RefinementValue: value
         }
    },
  'Name':Refiner
}

Cada uno de los elementos contenidos en el objeto Entries, nos indica una opción de refinamiento para el refinado correspondiente. A continuación vamos a ver cómo he usado este servicio en una aplicación para SharePoint a modo de prueba.

Usando la API REST para búsquedas en una App for SharePoint

El ejemplo lo he realizado usando JQuery para manipular el DOM de la página del mismo. En primer lugar vamos a ver una captura de la aplicación para explicar que hace cada sección de la misma:

appsearchexample

En la aplicación que os quiero enseñar he hecho 5 ejemplos de como usar la API REST del servicio de búsqueda:

  • El primero ejemplo es una consulta usando GET a partir de una cadena de texto.
  • El segundo ejemplo es una consulta por POST a partir de una cadena de texto.
  • El tercer ejemplo usar una query para hacer la llamada al servicio de búsqueda por GET.
  • En el cuarto ejemplo se usa la ordenación.
  • El quinto ejemplo utiliza los refinadores.

Código del primer ejemplo

$("#searchbutton1").click(function () {

        var searchText = $("#searchbox1").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList1");

                $('#resultsList1 > li').remove();

                var jsonObject = JSON.parse(data.body);

                console.log(jsonObject);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for(var i = 0; i < results.length; i++)
                {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value + "
" + results[i].Cells.results[5].Value + "&nbsp;" + results[i].Cells.results[4].Value + "
" + results[i].Cells.results[11].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

Código del segundo ejemplo

$("#searchbutton2").click(function () {

        var searchText = $("#searchbox2").val();

        executor.executeAsync({
            method: "POST",
            url: appweburl + "/_api/search/postquery",
            body: "{'request': { '__metadata' : {'type' : 'Microsoft.Office.Server.Search.REST.SearchRequest'}, 'Querytext' : '" + searchText + "' }}",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList2");

                $('#resultsList2 > li').remove();

                var jsonObject = JSON.parse(data.body);
                var results = jsonObject.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

Código del tercer ejemplo

executor.executeAsync({
        method: "GET",
        url: appweburl + "/_api/search/query?querytemplate='{searchterms} FileExtension: doc'",
        headers: {
            "accept": "application/json;odata=verbose",
            "content-type": "application/json;odata=verbose"
        },
        success: function (data) {

            var element = document.getElementById("resultsList3");

            $('#resultsList3 > li').remove();

            var jsonObject = JSON.parse(data.body);

            var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

            for (var i = 0; i < results.length; i++) {
                var li = document.createElement("li");

                li.innerText = results[i].Cells.results[3].Value;

                element.appendChild(li);
            }
        },
        error: function (data) {
        }
    });

Los tres primeros ejemplos son muy similares. Entre el primero y el segundo la única diferencia es que una petición se hace por GET y otra petición se hace por POST. En el tercero, se vuelve a usar una consulta por GET pero en este caso en lugar de un texto lo que se pasa es una consulta como tal. Todos los ejemplos usan una lista para representar los resultados devueltos.

Código del cuarto ejemplo

$("#searchbutton4").click(function () {

        var searchText = $("#searchbox4").val();

        executor.executeAsync({
            method: "POST",
            url: appweburl + "/_api/search/postquery",
            body: "{'request': { '__metadata' : {'type' : 'Microsoft.Office.Server.Search.REST.SearchRequest'}, 'Querytext' : '" + searchText + "' }}",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList4");

                $('#resultsList4 > li').remove();

                var jsonObject = JSON.parse(data.body);
                var results = jsonObject.d.postquery.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

    $("#orderbutton4").change(function () {

        var searchText = $("#searchbox4").val();
        var value = $("#orderbutton4").val();
        var order = "";

        if(value == "asc")
        {
            order = "created:ascending";
        }
        else
        {
            order = "created:descending";
        }

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&sortlist='" + order + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList4");

                $('#resultsList4 > li').remove();

                var jsonObject = JSON.parse(data.body);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value + " " + results[i].Cells.results[8].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;
    });

Básicamente este ejemplo funciona de la misma forma que los anteriores. Lo único extraordinario, tiene que ver con que queremos usar la ordenación por la propiedad “size” de los resultados. Hemos creado un combo donde podemos seleccionar, si queremos, orden ascendente y descendente para el tamaño. Lo que se ha hecho es crear otro evento que se dispara cuando cambia el valor seleccionado del combo para que se haga la ordenación. Para ello se añade a la consulta la parte correspondiente (sortlist) en función de si hemos seleccionado ascendente o descendente.

Código del último ejemplo

$("#searchbutton5").click(function () {

        var searchText = $("#searchbox5").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&refiners='author,size'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList5");

                $('#resultsList5 > li').remove();

                var jsonObject = JSON.parse(data.body);

                console.log(jsonObject);

                var refiners = jsonObject.d.query.PrimaryQueryResult.RefinementResults.Refiners.results;

                $("#titlerefiner").text(refiners[0].Name);

                $.each(refiners[0].Entries.results, function (i, item) {
                    $('#valuerefiners').append($('<option>', {
                        value: refiners[0].Name + ":" + item.RefinementToken,
                        text: item.RefinementName
                    }));
                });

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;

    });

    $("#valuerefiners").change(function () {

        var searchText = $("#searchbox5").val();
        var value = $("#valuerefiners").val();

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/search/query?querytext='" + searchText + "'&refinementfilters='" + value + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {

                var element = document.getElementById("resultsList5");

                $('#resultsList5 > li').remove();

                var jsonObject = JSON.parse(data.body);

                var results = jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

                for (var i = 0; i < results.length; i++) {
                    var li = document.createElement("li");

                    li.innerText = results[i].Cells.results[3].Value;

                    element.appendChild(li);
                }
            },
            error: function (data) {
            }
        });

        return false;
    });

Este ejemplo igualmente es similar en cuanto a la forma en la que se representan los datos a los anteriores. La diferencia aquí es que a la petición le estamos indicando las propiedades de los resultados obtenidos por las que queremos poder refinar (Línea 7). Una vez que se obtienen los resultados, se carga uno de los refinadores en un combo preparado para tal efecto (Líneas 22 a 31), este combo se carga automáticamente con los valores de refinamiento obtenidos.

Además se añade otro evento para cuando ese combo cambia, con la selección de un refinamiento, de manera que se haga el filtrado correspondiente. Para ello, solo se añade a la petición el refinementfilters con el contenido a filtrar (Línea 58).

Conclusiones

Trabajar con las búsquedas de SharePoint OnLine y 2013 nos ofrece una gran cantidad de oportunidades a la hora de desarrollar de una forma sencilla, la API REST del servicio de búsqueda es muy potente y merece la pena tenerla en cuenta. En la entrada de hoy, os he introducido al uso de la misma y os he mostrado un ejemplo de cómo usarla en una ejemplo de una aplicación para SharePoint.

Y esto es todo por hoy, espero que os resulte interesante. ¿Cuál es el siguiente paso?, bueno, como sabéis me gusta mucho Polymer, y creo que podría ser interesante crear un componente Polymer que use este servicio de búsqueda de forma completa. Espero en breve compartirlo con vosotros y ponerlo disponible para que lo podáis utilizar.

Un saludo y hasta la próxima

Nuevo modelo de desarrollo de Sharepoint. Adaptando nuestras soluciones de granja

Buenas tardes a todos,

Últimamemente se viene haciendo mucho hincapié desde Microsoft en el nuevo modelo de desarrollo de SharePoint, modelo que deja atrás el Feature Framework con el que veníamos trabajando últimamente y que se basa en un uso más intensivo de las APIs de Cliente y las aplicaciones para SharePoint. Obviamente, esta nueva propuesta va en detrimento de la API de Servidor y las soluciones de granja que habían sido la base de la extensibilidad de SharePoint en las versiones anteriores. Este nuevo modelo de desarrollo no solo se propone para la nueva plataforma de SharePoint OnLine, sino que se puede emplear igualmente en entornos On-Prem.

Aunque ya se ha comentado mucho sobre esto, los motivos principales que promueven este nuevo modelo podrían ser los siguientes:

  • La aparición de la plataforma OnLine, donde es obvio que las soluciones de granja, donde predomina la API de servidor, no tienen sitio.
  • Proponer un modelo de desarrollo que facilite la migración tanto a nuevas versiones On-Prem como a la versión OnLine, evitando los problemas de Upgrade del modelo anterior de desarrollo.
  • Evitar los riesgos que provocaba el anterior modelo de desarrollo, relacionado con el rendimiento como los leaks de memoria.

Un tema importante a tener en cuenta, es el hecho de que en los desarrollos sobre plataformas On-Prem, si bien se recomienda desde Microsoft adoptar de forma progresiva el nuevo modelo de desarrollo, todavía podremos seguir extendiendo la plataforma usando soluciones de granja, que estarán totalmente soportadas (ojo!!! no así las soluciones SandBox que están deprecated), al menos de momento. Lo que si nos recomiendan desde el equipo de producto es ir adaptando esas soluciones de granja para que nos sea más fácil adoptar poco a poco el nuevo modelo de desarrollo.

En este artículo os quiero enseñar algunos pasos que podríamos seguir para, si bien, en algunas ocasiones continuar trabajando en nuestros proyectos con el modelo clásico, intentar hacerlo de manera que vayamos desarrollando, de la forma más parecida posible, con los patrones y las buenas prácticas del nuevo modelo de desarrollo que nos proponen.

Adaptando nuestras soluciones .wsp para aproximarnos al nuevo modelo de desarrollo

En el ejemplo que vamos a ver a continuación nuestro objetivo será desplegar una solución de SharePoint que va a añadir una serie de columnas de sitio y tipos de Contenido. Vamos a ver la diferencia entre el approach que usaríamos habitualmente con el Feature Framework y la propuesta que nos hacen para ir aproximándonos al nuevo modelo de desarrollo.

En el primer caso usaríamos una serie de ficheros XML que añadirían tanto los tipos de contenido como las columnas de sitio. Los pasos habituales que seguiríamos serían básicamente los siguientes:

  1. Crear una característica
  2. Crear un elemento vacío de SharePoint
  3. Añadir la columnas de sitio al elemento recién creado
  4. Crear un elemento del tipo: “Tipo de Contenido”
  5. Establecer las columnas de dicho tipo de Contenido
  6. Si Visual Studio no lo ha hecho ya automáticamente (que es lo más lógico), añadiremos los elementos a la característica que habíamos creado.

El código de los ficheros .xml que vamos a añadir es el siguiente:

Para las columnas de sitio:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Field ID='{6A4E55B4-3AB1-448F-B51D-01224423C71F}' Group="Ejemplo" Type='DateTime' Name='Fecha' DisplayName='Fecha' SourceID='http://schemas.microsoft.com/sharepoint/v3' StaticName='Fecha' />
  <Field ID='{014AB8C6-02AA-41EC-B5A1-CD46668E6087}' Group="Ejemplo" Type='Note' Name='Comentario' DisplayName='Comentario' NumLines='30' StaticName='Comentario' SourceID='http://schemas.microsoft.com/sharepoint/v3' RichText='TRUE' RichTextMode='FullHtml' IsolateStyles='FALSE' />
  <Field ID='{18377DDE-58DA-4668-9FC5-EAB7A7ED70FC}' Group="Ejemplo" Type='User' Name='Valorador' DisplayName='Valorador' SourceID='http://schemas.microsoft.com/sharepoint/v3' StaticName='Valorador' />
  <Field ID='{9F764C8E-D966-416D-8148-3CC0E4E88BDA}' Group="Ejemplo" Type='Text' Name='Ruta' DisplayName='Ruta' SourceID='http://schemas.microsoft.com/sharepoint/v3' StaticName='Ruta' />
</Elements>

Para los tipos de contenido:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <!-- Tipo de contenido primario: Elemento (0x01) -->
  <ContentType ID="0x01002b1edca8d3f94d9dbeb9ca8da78b2460"
               Name="TipoContenido Ejemplo"
               Group="Ejemplo"
               Description="Descripción de ejemplo de tipo de contenido"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
      <FieldRef ID='{6A4E55B4-3AB1-448F-B51D-01224423C71F}' Name='Fecha' Required='TRUE'/>
      <FieldRef ID='{014AB8C6-02AA-41EC-B5A1-CD46668E6087}' Name='Comentario' Required='TRUE' />
      <FieldRef ID='{18377DDE-58DA-4668-9FC5-EAB7A7ED70FC}' Name='Valorador' Required='TRUE' />
      <FieldRef ID='{9F764C8E-D966-416D-8148-3CC0E4E88BDA}' Name='Ruta' Required='TRUE' />
    </FieldRefs>
  </ContentType>
</Elements>

¿Qué problemas tiene esta forma de desplegar nuestros elementos en SharePoint?. Utilizando estos ficheros xml se crean una serie de dependencias del fichero con las bases de datos de contenido que pueden darnos problemas a la hora de hacer una migración de nuestro entorno.

Captura de pantalla 2015-04-19 a las 13.33.15

Para evitar estos problemas de dependencias que se crean usando el Feature Framework de esta forma, podemos plantear otro approach, que nos aproxima a la forma de trabajar en el nuevo modelo de desarrollo. La solución, en este caso, pasaría por usar el receptor de eventos de la característica y crear los elementos programáticamente en el evento de activación de la misma.

El código que añadiríamos en el receptor de eventos sería el siguiente:

public class Feature1EventReceiver : SPFeatureReceiver
{
    // Quite las marcas de comentario al método siguiente para controlar el evento generado una vez activada una característica.

    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        SPSite site = properties.Feature.Parent as SPSite;

        using (SPWeb web = site.OpenWeb())
        {
            Helper.CreateTextColumn(web, "Ejemplo1", 255);
            Helper.CreateNumberColumn(web, "EjemploNumber1", 0);

            Helper.CreateContentType(web, "EjemploTipoContenido");
            Helper.AddColumnToContentType(web, "EjemploTipoContenido", "Ejemplo1");
            Helper.AddColumnToContentType(web, "EjemploTipoContenido", "EjemploNumber1");
        }
    }
} 

Como podéis ver en el código, y porque no se extienda tanto éste como el post, los elementos se crean por medio de helpers cuyo contenido no he añadido, aunque si los queréis, puedo añadirlos más adelante.

De esta forma, estaremos eliminando esos problemas de dependencias que se crean, y además estaremos dando un paso hacia la forma de trabajar en el nuevo modelo de desarrollo de SharePoint.

Captura de pantalla 2015-04-19 a las 13.33.50

La misma técnica podríamos usar para, por ejemplo, crear listas y bibliotecas, haciéndolo programáticamente en lugar de usando el despliegue por medio de ficheros XML. La línea a seguir, debería ser, en la medida de lo posible, evitar los ficheros XML para desplegar elementos y reemplazarlos por el uso de la API de SharePoint, lo que se acercará más al uso de las APIs de cliente en apps que nos proponen como nuevo modelo de desarrollo.

Os dejo algunos enlaces interesantes sobre el tema y que os pueden ayudar:

http://www.microsoftvirtualacademy.com/training-courses/transform-sharepoint-customizations-to-sharepoint-app-model

https://github.com/OfficeDev/PnP

En próximas entradas os contaré algunas cosillas más sobre el nuevo modelo de desarrollo y hacia dónde deberíamos de irnos enfocando cuando nos enfrentamos a una solución basada en SharePoint que requiere extender la plataforma.

Un saludo a todos.

JSON Light en API REST de SharePoint

Muy buenas a todos,

Uno de los medios de que disponemos a la hora de acceder a la información de SharePoint es por medio de la API REST. Hoy he conocido una de las opciones que nos ofrece SharePoint para, a través de su API REST, obtener respuestas más ligeras y sencillas. Esto es el soporte para JSON Light. Os dejo un extracto de la entrada del blog oficial de Office donde se explica esta nueva posibilidad de que disponemos.

One of the big bits of feedback we got from developers using the REST SharePoint API was about the payload of the data that was returned. Our response was to add support for JSON Light. JSON Light is an open standard that allows developers to provide in the header of the request how much metadata is returned

Os dejo el enlace a la entrada del blog oficial para el que quiera ver el post completo

http://blogs.office.com/2014/08/13/json-light-support-rest-sharepoint-api-released/

¿Y cómo usamos esto?. Habitualmente cuando hacemos una llamada a API REST, en el header de la petición incluimos la siguiente línea accept: application/json;odata=verbose. Con el soporte para JSON Light, disponemos de nuevas opciones para el parámetro odata que nos devolverán peticiones con distinta cantidad de metadatos en función de la opción que hayamos seleccionado. Las opciones de que disponemos son las siguientes:

  • odata=verbose: Este es el modo que veníamos usando hasta ahora
  • odata=minimalmetadata: Este modo nos devuelve una cantidad limitada de metadatos
  • odata=nometadata: Es el modo más ligero de que disponemos
  • Si no indicamos ningún valor para odata, por defecto tomará el valor minimalmetadata

Bueno, hasta aquí, prácticamente solo me he dedicado a transcribir en español y de una forma resumida lo que viene en el artículo del blog. Lo que he querido hacer es comprobar por mí mismo cómo funcionaban las distintas opciones, la información que devolvía cada una y los tiempos de respuesta. Y eso es lo que os quería enseñar en la entrada.

He creado una SharePoint Hosted App en la que he incluido el siguiente código.

$(document).ready(function () {
    
    var appweburl = GetParameter("SPAppWebUrl");
    var hostweburl = GetParameter("SPHostUrl");

    var executor = new SP.RequestExecutor(appweburl);

    executor.executeAsync({
        method: "GET",
        url: appweburl + "/_api/SP.AppContextSite(@target)/web/Lists/getbytitle('Noticias')/Items?@target='" + hostweburl + "'",
        headers: {
            "accept": "application/json;odata=verbose",
            "content-type": "application/json;odata=verbose"
        },
        success: function (data) {
            console.log(JSON.parse(data.body));
        },
        error: function (data) {
        }
    });

    executor.executeAsync({
        method: "GET",
        url: appweburl + "/_api/SP.AppContextSite(@target)/web/Lists/getbytitle('Noticias')/Items?@target='" + hostweburl + "'",
        headers: {
            "accept": "application/json;odata=minimalmetadata",
            "content-type": "application/json;odata=minimalmetadata"
        },
        success: function (data) {
            console.log(JSON.parse(data.body));
        },
        error: function (data) {
        }
    });

    executor.executeAsync({
        method: "GET",
        url: appweburl + "/_api/SP.AppContextSite(@target)/web/Lists/getbytitle('Noticias')/Items?@target='" + hostweburl + "'",
        headers: {
            "accept": "application/json;odata=nometadata",
            "content-type": "application/json;odata=nometadata"
        },
        success: function (data) {
            console.log(JSON.parse(data.body));
        },
        error: function (data) {
        }
    });


});

function GetParameter(paramToRetrieve) {
    var params =
            document.URL.split("?")[1].split("&");
    var strParams = "";
    for (var i = 0; i < params.length; i = i + 1) {
        var singleParam = params[i].split("=");
        if (singleParam[0] == paramToRetrieve)
            return decodeURIComponent(singleParam[1]);
    }
}

El código como veréis es muy sencillo. Hago la misma petición API REST, pero cada una con un valor para odata distinto y lo imprimo en consola para ver que devuelve. Este es el resultado

odata=verbose

verbose

odata=minimalmetadata

minimalmetadata

odata=nometadata

nometadata

Como podéis ver, la complejidad de las respuestas disminuye con cada tipo que usamos, cada una de ellas contiene menos información y los objetos que nos devuelve son más simples, por lo que, en ocasiones, en función de la información de la respuesta a la que queramos acceder, podemos usar peticiones que nos devuelve datos más ligeros.

¿Y como influye esto en el tamaño de la respuesta?, eso era otra de las cosas que quería comprobar, ya que como sabéis al trabajar del lado del cliente, uno de los aspectos más críticos, es intentar tener respuestas lo más ligeras posibles. Os dejo una captura donde se puede ver como se comporta cada una de las peticiones.

responsesize

Como podéis observar, las peticiones cada vez son más ligeras en función del modo que utilizamos.

Y nada más por ahora, quería probar y contar estas nuevas opciones que he descubierto hoy a la hora de usar la API REST y contaros lo que iba viendo. Espero que os sirva de ayuda por si no lo sabíais.

Saludos a todos.

Accediendo a los datos de SharePoint OnLine a través de CSOM

Hola a todos,

Hoy quiero seguir avanzando en el conocimiento de las distintas APIs que tenemos disponibles para acceder a SharePoint. Hasta ahora, en entradas anteriores, había hablado sobre el uso de la API Javascript y sobre todo de la API REST.

Introducción a la API REST de SharePoint 2013

Creando una app para SharePoint OnLine

Con estas APIs se pueden desarrollar SharePoint Hosted Apps, alojadas en SharePoint, que se desarrollan con HTML5 + CSS3 y Javascript principalmente. Pero además, podemos crear otro tipo de aplicaciones, que no están alojadas en SharePoint como las anteriores y que están alojadas en un sitio externo a éste, como puede ser un sitio de Azure. Las SharePoint Provider Apps pueden ser proyectos ASP.NET como WebForms o MVC. Para acceder a SharePoint desde este tipo de aplicaciones tenemos disponible la API CSOM (Client Side Object Model). Esta API es C#, lo que a priori, para los que hemos desarrollado habitualmente en el modelo de servidor puede suponer una ventaja.

El objetivo de esta entrada es hacer una pequeña introducción de cómo tenemos que proceder para realizar algunas de las operaciones básicas que pueden sernos útiles a la hora de trabajar con SharePoint.

Estos ejemplos se han basado en la creación de una Provider Hosted App con una aplicación MVC. Dicha aplicación, tiene permisos para manejar la colección de sitios, concedidos a través del AppManifest.xml.

Accediendo a listas

El código que vamos a usar para acceder a una lista por medio de la API CSOM es el siguiente:

public class NewsModel
{
    public string Title { get; set; }
    public string Cuerpo { get; set; }
    public string ID { get; set; }
    public DateTime Created { get; set; }
}
public void SeeList(string listname)
{
      var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);

      using (var clientContext = spContext.CreateUserClientContextForSPHost())
      {
           if (clientContext != null)
           {
                List news = clientContext.Web.Lists.GetByTitle(listname);

                CamlQuery query = new CamlQuery();
                query.ViewXml = "<View><Query><OrderBy><FieldRef Name='ID' Ascending='False' /></OrderBy></Query><ViewFields><FieldRef Name='Title' /></ViewFields><QueryOptions /><RowLimit>2</RowLimit></View>";
                    
                ListItemCollection items = news.GetItems(query);

                clientContext.Load(items);
                clientContext.ExecuteQuery();

                List<NewsModel> list = new List<NewsModel>();

                foreach(ListItem item in items)
                {
                    NewsModel newNews = new NewsModel
                    {
                         Title = item["Title"].ToString(),
                         ID = item.Id.ToString()
                    };

                    list.Add(newNews);
                 }

            }
        }
}

A la hora de hacer una consulta hay algunos aspectos que se debe de tener en cuenta:

  • Al ser una llamada desde el cliente, es importante intentar que las consultas sean lo más óptimas posibles, no olvidando establecer el RowLimit y las columnas que se quieren leer en la consulta, evitando leer información innecesaria.
  • Se pueden hacer todas las llamadas a métodos de la API que se quieran antes de llamar a la función clientContext.Load() y clientContext.ExecuteQuery(). Pero si lo que queremos es acceder a las propiedades de un elemento, es obligatorio previamente, hacer las llamadas a estos dos métodos del contexto para que se ejecute la consulta y la información esté ya cargada. En caso contrario, obtendremos una excepción de tipo NullReference.

Añadiendo elementos a una lista

Vamos a ver ahora cómo podríamos añadir un elemento en una lista por medio de CSOM. Para ello, se supone la existencia de una lista que tiene los campos Título,Body y Gist.

public void AddElementToList(string listname, string title, string body, string gist)
{
    var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);

    using (var clientContext = spContext.CreateUserClientContextForSPHost())
    {
          if (clientContext != null)
          {
                List examples = clientContext.Web.Lists.GetByTitle(listname);

                ListItemCreationInformation creationInformation = new ListItemCreationInformation();

                ListItem newItem = examples.AddItem(creationInformation);
                newItem["Title"] = title;
                newItem["Body"] = body;
                newItem["Gist"] = gist;

                newItem.Update();

                clientContext.ExecuteQuery();
           }
     }
}

Creando una lista

Vamos a ver por último, como crear una lista usando este modelo de objetos.La última función, creará una lista de tipo genérico y añadirá algunos campos a la misma.

public void CreateList(string ListName)
{
      var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);

      using (var clientContext = spContext.CreateUserClientContextForSPHost())
      {
          if (clientContext != null)
          {
              ListCreationInformation informationToList = new ListCreationInformation
              {
                   Title = ListName,
                   TemplateType = (int)ListTemplateType.GenericList,
                   Description = "This is a Client Created List"
              };

              List newList = clientContext.Web.Lists.Add(informationToList);

              newList.Fields.AddFieldAsXml("<Field DisplayName='Body' Type='Note' Required='True' />", true, AddFieldOptions.DefaultValue);
              newList.Fields.AddFieldAsXml("<Field DisplayName='Gist' Type='Text' Required='True' />", true, AddFieldOptions.DefaultValue);

              newList.Update();

              clientContext.ExecuteQuery();

           }
      }
}

Con estos ejemplos, espero que quede una ligera idea de, qué y cómo se puede trabajar con SharePoint y C# desde el modelo de objetos de cliente en SharePoint Provider Apps.

Este modelo de objetos tiene mucha relevancia dentro del modelo de provisionamiento remoto que está promoviendo Microsoft para sustituir al tradicional modelo de desarrollo basado en el Feature Framework por el nuevo modelo de desarrollo basado en apps. Aunque de este tema os hablaré más adelante.

Y nada más por hoy, espero que como siempre os sea de utilidad.

Subiendo una App a la tienda de aplicaciones de SharePoint

Muy buenas a todos,

Hoy después de varios días de envíos y reenvíos he recibido el mensaje de aprobación de mi App Room Reservation para que comience a formar parte de la tienda oficial de aplicaciones de SharePoint.

RoomReservation: Mi primera SharePoint Hosted App

Quería aprovechar para contaros los pasos y el proceso que he seguido para subir la App, y por si alguien se anima, contaros algunas cosas que me han pasado y ahorraros algunos reenvíos de la aplicación a la tienda.

¿Dónde Subimos nuestra App?

Si queremos subir una app, tenemos que entrar en la web de Microsoft:

https://sellerdashboard.microsoft.com/

Una vez que creemos y configuremos nuestra cuenta (que pasa por un breve proceso de validación, pero que no tiene ningún problema), podremos acceder al Panel de Vendedores. Cuando creamos la cuenta, también nos pide configurar la información de pagos e impuestos, pero no es obligatorio hacerlo, salvo que queramos cobrar por la app que estamos desarrollando

Captura de pantalla 2015-03-05 a las 22.18.11

Como véis, fácilmente vemos la opción de añadir una App, y si pulsamos sobre ella, nos pregunta, en primer lugar, el tipo de App que queremos añadir a la tienda.

Captura de pantalla 2015-03-05 a las 22.18.25

Cuando seleccionamos la opción, ya nos comienza a pedir la información de la App que vamos a subir. Información como la categorización de la App una vez que esté en la tienda, el logo, el archivo con la App, versión, etc.

Captura de pantalla 2015-03-05 a las 22.18.49

Captura de pantalla 2015-03-05 a las 22.19.08

Captura de pantalla 2015-03-05 a las 22.19.20

Dos aspectos importantes dentro de la información general, es el vínculo al documento de soporte y el vínculo documento de privacidad. En mi caso, el documento de soporte ha sido mi repositorio de Git y el documento de privacidad está en mi blog y es el siguiente:

Documento de Privacidad

Tras indicar toda la información correctamente, pasamos a la siguiente pantalla, donde indicaremos los detalles de la App. En los detalles indicaremos los idiomas para los que nuestra aplicación estará disponible y para cada uno de los idiomas, deberemos de indicar los metadatos de la App: Nombre, Descripción breve, Descripción detallada, y todos ellos para cada uno de los idiomas que hayamos definido para nuestra App.

Captura de pantalla 2015-03-05 a las 22.20.32

A continuación hay otras dos pantallas, una primera para indicar si la App tiene zonas de bloqueo (lugares donde la App no estará disponible) y por último las condiciones de pago de la misma. En mi caso al seleccionar gratuita, no había nada más que configurar.

Tras esto comienza el proceso de validación que tiene varias fases:

  • En primer lugar realiza una serie de comprobaciones automáticas
  • Prueba y evaluación de la App
  • Aprobación y despliegue en la tienda de aplicaciones

Cada vez que por algún motivo la App no ha sido aprobada, recibes un informe muy completo, indicando todos los motivos que han provocado el rechazo, y que tiene el siguiente aspecto:

Captura de pantalla 2015-03-05 a las 22.54.24

La verdad que a pesar de que son muy estrictos a la hora de aprobar la App, los informes me han parecido bastante claros y completos, incluso después de algún reenvío, hasta me daban las gracias por haber corregido las cosas del informe anterior

Aspectos a tener en cuenta cuando vamos a subir nuestra App

Ahora por último algunas cosas a tener en cuenta que me han provocado tener que reenviar la App varias veces.

  • La versión que indicas en los detalles generales de la App debe ser la misma que la versión que indicas en el AppManifest.xml, en mi caso indiqué 1.4 cuando tiene que ser 1.4.0.0.
  • El logo debe ser igualmente el mismo tanto en la propia App como en la información general que estás definiendo, y debe de tener unas dimensiones de 96×96 píxeles exactamente
  • Las Apps de la tienda no pueden tener permisos Full Control
  • Las Apps deben funcionar correctamente en los navegadores Chrome, FireFox y IE desde la versión 9. Este punto es importante. A veces, se usan aspectos relativos a CSS3 o a HTML5 que, obviamente, no son compatibles con IE9 pero que no hacen que no funcione la App, solo que no se vea correctamente y esto puede provocar el rechazo de la misma. En ese caso basta con añadir en la descripción de la App una nota que indique que, hay aspectos que no funcionan completamente en IE9 porque no son soportados.
  • Es importante que coincidan los idiomas que están definidos en el panel del vendedor para la app, con los SupportedLocales del AppManifest.xml, y que la app, si defines varios idiomas, funcione correctamente con los mismos
  • Debes definir los metadatos para la información en todos los idiomas.

Y esto es todo, espero que como siempre, os sea útil, yo por mi parte, ya tengo la App en la tienda de SharePoint 😉 (si aún no aparece es porque como dicen durante el proceso de aprobación, puede tardar un poco desde que la aprueban hasta que se despliega en la tienda).

Un saludo a todos

Campos de tipo DateTime y API REST en SharePoint

Hola a todos,

Como os comentaba ayer, para poner en práctica los últimos avances que he tenido con las aplicaciones de SharePoint, he desarrollado una SharePoint Hosted App que utiliza, para la comunicación y las operaciones con SharePoint, API REST.

RoomReservation: Mi primera SharePoint Hosted App

Código en GitHub

Lo que hoy os quiero contar, es una de las cosas que más me ha entretenido en el desarrollo de la aplicación y tiene que ver con la manipulación de campos de tipo DateTime y la API REST. En sí, no tiene nada especialmente complejo, aunque a continuación veremos las particularidades que hay que tener en cuenta.

Filtrando campos de tipo DateTime en una petición API REST

El filtrado de campos de tipo fecha a través de API REST es muy sencillo, tan solo hay que pasar la fecha en el formato correcto:

https:///_api/web/lists/getbytitle(list)/items?$filter=DateFieldName eq datetime’yyyy-mm-ddThh:mm:ss’

El formato de la fecha como podéis ver es el siguiente “yyyy-mm-ddThh:mm:ss”

Datos devueltos en una consulta de API REST de tipo DateTime

Con los datos de tipo DateTime que nos devuelve una consulta API REST, empieza a haber algunas cosas a tener en cuenta. SharePoint, a través de API REST, nos devuelve todos los campos de tipo DateTime formateados como UTC. Por tanto, si mostramos los datos tal cual nos los devuelve API REST y nuestra configuración regional es distinta de la UTC (por ejemplo, en la península es UTC +1), no veremos los resultados que esperábamos. Por ejemplo, si hemos almacenado la siguiente fecha 2015-03-03T10:30:00 en el calendario de SharePoint y estamos en UTC+1, nos devolverá 2015-03-03T09:30:00.

Para solventar esto tenemos que aplicar la corrección correspondiente al TimeOffSet con respecto a la hora en formato UTC. Os pongo el código que he usado yo en mi aplicación y os cuento lo más importante. Para manipular las fechas he utilizado una librería de javascript llamada moment.js que añade algunas funcionalidades extra al objeto de tipo Date básico de Javascript, y que permitirá hacer sumas y restas a fechas.


var offset;

function GetTimeOffset(){
        var deferred = $.Deferred();

        var appweburl = MyApp.AppSPUtils.GetQueryStringParameter("SPAppWebUrl");
        var hostweburl = MyApp.AppSPUtils.GetQueryStringParameter("SPHostUrl");

        executor = new SP.RequestExecutor(appweburl);

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/SP.AppContextSite(@target)/web/regionalsettings/timezone?@target='" + hostweburl + "'",
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose"
            },
            success: function (data) {
                var jsonObject = JSON.parse(data.body);
                offset = jsonObject.d.Information.Bias;

                deferred.resolve();
            },
            error: function (data) {
                deferred.reject();
            }
        });

        return deferred.promise();
    }
function ConvertToGetLocalTime(UTCdate) {

        var date = moment(UTCdate);

        if (date.utcOffset() > 0)
            date = date.subtract(offset, 'm');
        else
            date = date.add(((-1) * offset), 'm');

        return date.toISOString();
    }

El primer código que vemos, nos permite obtener por medio de API REST, el TimeOffSet de la configuración regional que tenemos en nuestro sitio de SharePoint con respecto a UTC. Si por ejemplo, estamos en UTC+1, el valor que obtendremos será -60, que son los minutos que habría que restar a la hora, para convertirla a formato UTC.

Por tanto, una vez obtenido el offset de nuestra configuración regional de sitio, aplicamos la conversión que vemos en la función ConvertToGetLocalTime que recibirá un string con el formato de fecha obtenido de la consulta por API REST (yyyy-mm-ddThh:mm:ss) y devuelve una fecha con el mismo formato y adaptado a la hora local configurada en nuestro sitio.

Añadiendo elementos a una lista con campos de tipo DateTime con API REST

Cuando lo que queremos es usar POST para añadir a través de API REST un elemento que tiene campos de tipo DateTime, el problema es el contrario. Lo que la API REST considera que recibe es una fecha en formato UTC y al almacenarla, la convertirá en el formato correspondiente a la configuración regional establecida. Por ejemplo, si pasamos como campo 2015-03-03T10:30:00 y tenemos una configuración UTC+1, lo que se almacenará será 2015-03-03T11:30:00.

Para solucionar esto, aplicaremos la configuración inversa que en el caso anterior por medio de la siguiente función:

function ConvertToPostLocalTime(UTCdate) {

        var date = moment(UTCdate);

        if (date.utcOffset() > 0)
            date = date.add(offset, 'm');
        else
            date = date.subtract(((-1) * offset), 'm');

        return date.toISOString();
    }

Y esto es todo por hoy, como podéis ver, trabajar con fechas y API REST requiere tener en cuenta la configuración regional y aplicar las conversiones a las fechas que correspondan, ya que ésta siempre trabaja con UTC tanto a la hora de hacer POST como GET.

Espero que os sea útil y os ahorréis ese tiempo de más que me ha llevado a la hora de trabajar con esto.

Saludos a todos.