[Evento] Potencia tus desarrollos de SharePoint con Azure

Muy buenas a todos, y antes de nada Feliz Navidad a todos los que habitualmente entran a «El blog del programador».

Hoy os quiero hablar del próximo evento que se va a celebrar, organizado por la comunidad de usuarios de SharePoint de Madrid, MadPoint, y que tendrá lugar el próximo 15 de Enero en Wayra en la calla Gran Via, 28.

En dicho evento, tendremos la oportunidad de profundizar en las oportunidades que nos ofrece Azure para usarla en nuestros desarrollos de SharePoint, tanto OnLine como OnPremise. Os dejo el enlace de la página de madpoint donde encontraréis toda la información sobre el evento

http://www.madpoint.net/2015/12/23/evento-potencia-tus-desarrollos-de-sharepoint-con-azure/

La agenda del evento, en donde tendré la oportunidad de aportar mi granito de arena, es la siguiente:

  • 16.00 – 16.15: Bienvenida e introducción
  • 16.15-16.50: Diseña tu propio Office 365 con Azure IaaS y PaaS
    • En esta sesión teórica, Miguel Tabera (MVP de Office Servers and Services) nos enseñará, desde el punto de vista de arquitectura, cómo es posible utilizar máquinas virtuales y servicios como las web apps, Azure Search, Media Services y Application Insights para diseñar nuestro propio servicio similar a Office 365 en el que proporcionemos SharePoint, Exchange, Office Vídeo, etc.
  • 17.00-17.50: Logic apps o el futuro de los flujos de trabajo
    • ¿Qué son las logic apps? ¿Cómo las podemos utilizar? ¿Se convertirá en el motor para desarrollar nuestras lógicas de negocio y flujos de trabajo en el futuro? En esta sesión, Jose Carlos Rodríguez Avilés, nos enseñará qué podemos hacer con este servicio de Azure y cómo podemos usarlo para implementar lógicas de negocio interconectando entre si la amplia gama de herramientas que usamos hoy en día.
  • 18.00-18.50: Timerjobs y eventos en SharePoint Online usando Hangfire
    • Cristian Ruiz nos mostrará un ejemplo práctico de cómo crear timerjobs y eventos remotos (evitando los límites de tiempo de SharePoint online) utilizando el motor de tareas programadas.

Como podéis ver, las ponencias son muy interesantes, os animo a seguir de cerca la información del evento, y a que participéis en el mismo. Poco a poco, desde MadPoint iréis recibiendo más información, desde twitter, linkedin y la página de web, para todos aquellos que podáis estar interesado.

Un saludo, y por si acaso que tengáis una muy buena entrada del año 2016.

 

Paginando en SharePoint 2013 con API REST

Muy buenas a todos,

Hoy quería contaros un tema con el que me he estado peleando hoy en mi proyecto con el que estoy haciendo un uso bastante intensivo de la API REST de SharePoint con AngularJS. Una experiencia que os debo decir, me está encantando.

El tema ha venido a la hora de «paginar» una consulta en SharePoint que estaba haciendo vía API REST. Según pensaba, usando las opciones $top y $skip se podría hacer la paginación sin problemas. Para ello pensaba usar el endpoint que habitualmente uso para trabajar con las listas de SharePoint

http://<misitio>/_api/web/lists/getbytitle(‘lista&#8217;)/items

Mi sorpresa ha sido darme cuenta que con este endpoint la opción $skip no funciona. Indagando un poco he descubierto que efectivamente, esta opción no funciona para lista de elementos, solo funciona para colecciones de datos como colecciones de listas.

http://sharepoint.stackexchange.com/questions/45719/paging-using-rest-odata-with-sp-2013

https://msdn.microsoft.com/en-us/library/office/fp142385.aspx

Para hacer la paginación via API REST podemos usar el antiguo endpoint OData V2 listdata.svc. Con esta versión si funciona la opción $skip correctamente:

http://<misitio>/_vti_bin/ListData.svc/<lista&gt;

Ejemplo: http://<misitio>/_vti_bin/ListData.svc/<lista&gt;?$top=2&$skip=2

Salvando este inconveniente sobre el endpoint que debemos usar, usando estas dos opciones $top y $skip, podremos paginar nuestras consultas de una forma muy sencilla.

Y esto es todo por hoy, espero que os resulte útil, como siempre, y que si os habéis encontrado con esta problemática, tardéis menos tiempo en resolverlo.

Saludos.

[OffTopic]

No suelo acostumbrar a hacer nada de esto. Pero hoy ha nacido mi sobrino, del que tendré la oportunidad además de ser su padrino. Así que esta entrada de hoy va dedicada a él y a sus padres. Aunque no lo podré conocer hasta dentro de 15 días porque me ha salido «conejero» y está en Lanzarote, que ilusión más grande.

Usando el nuevo modelo de desarrollo para SharePoint con PowerShell

Muy buenas a todos,

Hace unos meses, escribí acerca del nuevo modelo de desarrollo para SharePoint OnLine y SharePoint OnPremises propuesto por Microsoft e incluso una de mis colaboraciones en CompartiMOSS giró en torno a este tema.

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

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

Como ya decía en aquella ocasión, trabajar con las API cliente se podría hacer tanto por medio de la API REST, con la API CSOM o JSOM o incluso PowerShell. Tenía muchas ganas de ver cómo se podría trabajar con éste último y para un proyecto en el que estoy trabajando recientemente decidí que ya era el momento.

La primera intención, fue desarrollar mis propios Cmdlets o mis propios Scripts con funciones para realizar las distintas operaciones que fuese necesitando. Finalmente decidí previamente indagar por la cuenta de GitHub de OfficeDev/Pnp, donde encontré un gran trabajo especifico sobre PowerShell, proporcionando una gran cantidad de Cmdlets desarrollados que permiten trabajar con las API Cliente, concretamente CSOM, y PowerShell. Por supuesto, trabajo que decidí utilizar, y que os recomiendo a todos.

https://github.com/OfficeDev/PnP-powershell

En el artículo de hoy, os quiero mostrar cómo instalar y configurar en nuestro servidor estos Cmdlets y cómo se pueden usar algunos de ellos a modo de ejemplo. No obstante, en la página de Pnp hay una documentación muy buena sobre todas las opciones de que disponemos.

Instalando los Cmdlets en el servidor

Para poder tener disponibles todos los Cmdlets de PowerShell tendremos que seguir los siguientes pasos:

1.- En primer lugar descargar el proyecto Visual Studio desde la cuenta de GitHub de OfficeDev/Pnp-PowerShell.

download

2.- Para poder instalar el proyecto, es necesario descargar e instalar las WiX Tools en nuestro equipo.

wix

3.- A continuación abrimos la solución que descargamos anteriormente y para poder crear la solución, necesitamos agregar dos paquetes desde Nugets. El primero de ellos es el OfficeDev Core para SharePoint Online o OnPremises, en función de para qué lo vayamos a usar.

coreoffice

4.- El siguiente paquete es la librería ADAL para la autenticación.

identity

5.- Tras esto ya se puede hacer el build del proyecto lo cual dejará instalados los Cmdlets en nuestro equipo

install

Si ahora abrimos por ejemplo el IDE de PowerShell, en la lista de comandos disponibles, si ponemos en el cajón de búsqueda SPO, filtrará todos los comandos que se han añadido.

Usando los nuevo Cmdlets

A continuación voy a mostrar un breve ejemplo de cómo podríamos usar los nuevos Cmdlets con PowerShell.

Connect-SPOnline -Url $url -Credentials (Get-Credential)

Add-SPOField -DisplayName "Example" -InternalName "Example" -Group "ExampleSPO" -Type Integer
Add-SPOField -DisplayName "Example1" -InternalName "Example1" -Group "ExampleSPO" -Type Text
Add-SPOField -DisplayName "Example2" -InternalName "Example2" -Group "ExampleSPO" -Type Choice -Choices "uno","dos","tres"

Add-SPOContentType -Name "ContentTypeExample" -Description "Description to Content Type" -Group "ExampleSPO"

Add-SPOFieldToContentType -Field "Example" -ContentType "ContentTypeExample"
Add-SPOFieldToContentType -Field "Example1" -ContentType "ContentTypeExample"
Add-SPOFieldToContentType -Field "Example2" -ContentType "ContentTypeExample"

New-SPOList -Title "List Title" -Template GenericList -EnableContentTypes
Add-SPOContentTypeToList -List "List Title" -ContentType "ContentTypeExample" -DefaultContentType

En el código de ejemplo se muestra cómo conectarse a un sitio de SharePoint OnLine, crear columnas de sitio, un tipo de contenido y asociarlo a una lista que se ha creado previamente.

Y esto es todo por hoy, por si aún no lo conocíais, espero que os sea de utilidad y que en la medida de lo posible, podáis usarlo en vuestros proyectos.

Hasta la próxima.

Resolviendo el error 403 cuando añadimos elementos por medio de API REST en SharePoint 2013

Hola a todos,

Hoy os quiero mostrar en una breve entrada cómo resolver el error 403 que surge cuando hacemos una petición POST para añadir elementos en una lista a través de la API REST de SharePoint 2013.

En mi caso, estoy trabajando en un proyecto en el que utilizo SharePoint y AngularJS, por eso, los ejemplos de las peticiones que os voy a mostrar, son usando el módulo $http de AngularJS.

Cuando hacemos una petición como esta:

$http({
            url: urlBase,
            method: "POST",
            data: JSON.stringify(likeJSON),
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose",
            }
        });

El resultado que nos devuelve es un error como éste:

“The security validation for this page is invalid and might be corrupted. Please use your web browser’s Back button to try your operation again.”

Para solucionarlo, debemos añadir en la cabecera de la petición el parámetro X-RequestDigest como sigue:

$http({
            url: urlBase,
            method: "POST",
            data: JSON.stringify(likeJSON),
            headers: {
                "accept": "application/json;odata=verbose",
                "content-type": "application/json;odata=verbose",
                "X-RequestDigest": requestDigest,
            }
        });

Esta cabecera contiene información conocida como form digest. Ésta es un objeto que se inserta en una página por SharePoint y es usado para validar las peticiones de cliente

La pregunta ahora es: ¿Cómo conseguimos ese valor?. Pues lo podemos obtener por medio de la siguiente petición:

$http({
        url: "/_api/contextinfo",
        method: "POST",
        headers: {
            "accept": "application/json;odata=verbose",
            "content-type": "application/json;odata=verbose"
        }
    }).then(function (response) {
        requestDigest = response.data.d.GetContextWebInformation.FormDigestValue;
    },
    function () {

    });

Por medio de una petición POST al servicio REST con el EndPoint en la url /_api/contextinfo

De esta forma conseguimos resolver el error 403 que nos devuelven las peticiones POST al servicio REST en SharePoint.

Os dejo un enlace que me ha servido de guía, y en el que me he basado.

http://blogs.msdn.com/b/nadeemis/archive/2012/10/23/tip-handling-http-403-forbidden-when-querying-the-search-rest-service-using-the-postquery-method.aspx

Espero que os haya resultado interesante.

Un saludo

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

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.

Número #23 de la revista CompartiMOSS y mi primera colaboración con la revista

Muy buenas a todos,

Ya está disponible un nuevo número de la revista CompartiMOSS, en este caso el número #23. Como siempre en esta revista encontraréis artículos muy interesantes relacionados con el mundo de SharePoint y os animos que la descarguéis y echéis un vistazo por su contenido.

http://www.compartimoss.com/revistas/numero-23

Aquí os dejo la relación de artículos que podréis encontrar en la revista, seguro que los encontraréis interesantes:

Búsqueda de datos empresariales en SharePoint 2013 – Parte I por Martin Luis Lopez Requena
Creación de un Chat para SharePoint con SignalR por José Antonio Fraga Sánchez
Introducción a los Grupos de Office 365 – Trabaja como una red – Parte II por Juan Carlos Gonzalez Martin
La importancia de diseñar y aplicar un buen uso de Gobernanza en nuestro entorno SharePoint 2013 por Francisco Ricardo Gil González
Las Aplicaciones High-Trust – II por Edin Kapic
Programando la Ribbon en SharePoint 2010 por Juan Pablo Pussacq
REST, WebAPI 2 y SharePoint 2013 – WebAPI y OData por Gustavo Velez
Administra mejor tus aplicaciones de servicio por Miguel Tabera Pacheco
Calculo de costes en Microsoft Azure por Fabian Calvo
Entrevista Jorge Castañeda Cano por Jorge Castañeda Cano
Integración Dynamics CRM 2015 con SharePoint por Demian Adolfo Raschkovan
Introducción al Search Driven Development en SharePoint Online y SharePoint 2013 por José Carlos Rodríguez Avilés
Novedades en Power BI por Javier Menéndez Pallo

En esta ocasión, como podréis ver, he tenido la oportunidad de colaborar con un artículo. Es mi primera aportación en una revista, y encima con la responsabilidad de hacerlo en CompartiMOSS, en la que escriben los mejores en la temática de SharePoint de habla Hispana.

El artículo trata sobre una temática de la que ya hablé en el blog, sobre Search Driven Development, es una breve introducción que sirve para conocer como usar este concepto y saber lo sencillo que es montar en SharePoint 2013 y OnLine un catálogo.

Como os digo, es mi primera aportación aunque espero que no sea la última, como decía al principio, os animo a descargarla y leerla.

Un saludo a todos

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

Usando Promises con JQuery y API REST de SharePoint

Hola de nuevo a todos,

Empezando a trabajar con Javascript de forma intensiva, me he topado con la necesidad de usar un concepto del que había oído hablar hace tiempo, pero que hasta ahora no me había hecho falta. Este concepto es lo que se conoce como Promesas o Promises.

Cuando estamos trabajando con varias APIs a través de peticiones asíncronas o con una API pero realizamos varias peticiones a la vez, al final te puedes encontrar con el problema de que, peticiones que necesitas que se ejecuten después que otras lo están haciendo antes, por la propia naturaleza de las llamadas asíncronas. Puede ocurrir también que, funciones que necesitas que se ejecuten al finalizar las llamadas asíncronas lo estén haciendo antes. Para evitar esto puedes pensar en un primer momento en una montón de llamadas asíncronas anidadas en el callback de éxito de las llamadas asíncronas o por el contrario, usar las promesas.

¿Que son las promesas?

Para explicar lo que son las promesas, voy a tomar prestado la explicación de la siguiente entrada de un blog que he encontrado:

http://www.funcion13.com/2012/03/26/comprendiendo-promesas-y-deferreds-en-jquery/

La promesa (promise) en sí, es un objeto que representa un evento en el tiempo, como la llegada de datos de una petición asíncrona o el fin del procesamiento de todos los datos obtenidos en una función asíncrona. Un objeto promise es una versión de solo lectura de un objeto deferred.

Los deferred comienzan en estado pendiente, y es lógico si lo pensáis, pudiendo pasar a estado resuelto (la tarea se completó) o rechazado (si es que falló). En el momento en que un deferred cambie su estado a resuelto o rechazado no podrá cambiar nunca más.

Vamos a usar las promesas para manejar peticiones asíncronas, y controlar que se reciba una respuesta sea correcta o no. Además, con JQuery, podremos controlar, la respuesta de varias peticiones asíncronas por medio de promesas que se ejecutan simultáneamente. El código javascript, además, quedará mucho más ordenado.

Os dejo algunos enlaces que he usado yo para entender bien el concepto de promises.

http://www.danieldemmel.me/blog/2013/03/22/an-introduction-to-jquery-deferred-slash-promise/

http://joseoncode.com/2011/09/26/a-walkthrough-jquery-deferred-and-promise/

http://api.jquery.com/deferred.promise/

¿Cómo se usan las promesas?

En el ejemplo que os voy a mostrar a continuación, he usado las promesas para trabajar con la API REST de SharePoint. En mi caso, en una aplicación que estoy desarrollando, necesitaba hacer varias peticiones API REST para saber si varias listas existían para, en función de si lo hacían o no indicarlo al usuario para darle la oportunidad de crearlas.

El problema me surgía por la dificultad de controlar el resultado de las peticiones, ya que al ser asíncronas, las evaluaciones de su resultado se hacían antes de que la petición se hubiera realizado realmente, por lo que no funcionaba como quería que lo hiciera.

Para ello he usado el concepto de promesas, por medio de ellas, puedo controlar que todas las peticiones se hagan y den un resultado, y esperar hasta que se obtenga la respuesta de todas para llevar la acción que corresponda. Voy a mostraros el código y posteriormente explicarlo, en mi caso he usado la librería JQuery y el objeto Deferred de la misma:

function ExistList(listname) {

        var deferred = $.Deferred();

        var appweburl = GetQueryStringParameter("SPAppWebUrl");
        var hostweburl = GetQueryStringParameter("SPHostUrl");

        var executor = new SP.RequestExecutor(appweburl);

        executor.executeAsync({
            method: "GET",
            url: appweburl + "/_api/SP.AppContextSite(@target)/web/Lists/GetByTitle('" + listname + "')?@target='" + hostweburl + "'",
            headers: { "Accept": "application/json; odata=verbose" },
            success: function (data) {
                deferred.resolve();
            },
            error: function (data) {
                console.log(data);
                deferred.reject();
            }
        });

        return deferred.promise();
    }

function Validation(){

            var list1 = "List1";
            var list2 = "List2";

            $.when(ExistList(list1), ExistList(list2))
             .done(function () {

             })
             .fail(function () {
                 RedirectApp("Install.aspx");
             });
                
        }
  • El primer aspecto importante se encuentra en la función ExistList y es el objeto deferred, se crea al inicio de la función y al final de la función se devuelve la promesa. Dentro de la función se hace la petición asíncrona y en el callback de éxito y de error de la misma llamada, se indica si la promesa se ha cumplido (por medio de deferred.resolve()) o no (por medio de deferred.reject()) respectivamente.
  • En la función de validación el primer elemento importante es la llamada $.when() a la que le indicaremos como parámetro (tal y como podemos ver), todas las promesas que queremos que espere a obtener resultado. Los callback no se ejecutarán hasta que no se haya recibido la respuesta de todas las promesas indicadas como parámetro de la llamada.
  • En el caso de que todas las promesas hayan respondido positivamente se utiliza el callback de la llamada done.
  • En el caso de que alguna de las promesas haya respondido negativamente se ejecuta el callbak de la llamada fail

En ocasiones también podremos encontrarnos que se usa la llamada then que lleva como parámetros los dos callback de éxito y error, pero a mi me ha resultado más claro usar la llamada done y fail.

Cuando trabajamos con JQuery y usamos la llamada $.ajax(), ya de por sí devuelve una promesa por lo que no debemos encapsularla en una función para crear el objeto deferred.

Y con esto espero que si alguien no sabía o no había trabajado habitualmente con promesas, encuentre aquí como trabajar con ellas. Aunque el trabajo con promesas no es una novedad, hasta ahora no había necesitado trabajar con ellas, me ha resultado muy fácil, además de que el código queda muchísimo más limpio y organizado.

Un saludo a todos.