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.

login-azuread: Un Componente Web con Polymer y Javascript ADAL

Hola a todos,

En la entrada anterior os contaba como se podía usar la librería Javascript ADAL para autenticar una aplicación contra Azure AD usando Javascript y terminaba diciendo que os contaría sobre el componente Polymer que había desarrollado para la autenticación.

Autenticación de nuestra Aplicación usando Javascript ADAL

Lo que he hecho ha sido crear un componente web que permite de una forma sencilla hacer la autenticación y que añadiéndolo a cualquiera de nuestras aplicaciones, podamos gestionar el sign in contra Azure AD de la misma. El código del componente web lo podéis encontrar en mi cuenta de GitHub.

Azuread-login en GitHub

El modo de empleo es muy sencillo, solo tendremos que insertar la siguiente línea en donde queramos tener nuestro modulo de login.

<login-azuread domain="<tenantdomainhere>" clientid="<clientidhere>"
</login-azuread>

Antes, deberemos haber registrado la aplicación en Azure AD, lo que nos proporcionará un client ID. Para que el componente funcione correctamente, deberemos indicar en el atributo domain, el dominio de nuestro tenant y en el atributo clientid el dato obtenido tras el registro de la aplicación en Azure AD.

¿Cuál es el resultado?

Vamos a ver como funciona el componente web una vez lo usamos en un ejemplo.

Cuando un usuario no se ha autenticado.

azureadlogin1

azureadlogin2

Cuando un usuario se ha autenticado
azureadlogin3

Además el componente se ha diseñado de manera que los elementos que lo forman, no se encuentran dentro del Shadow DOM que usa Polymer, sino que están renderizados por el DOM de la página, de manera que podemos estilarlos por medio del CSS de nuestra aplicación. Para ello, solo tenemos que redefinir las clases loginad-name, loginad-a, logoutad-a.

¿Vemos algo de código?

El código es muy sencillo, y aunque lo podéis ver en GitHub os lo pongo aquí y explico algunos detalles. El componente lo forman dos ficheros, uno .html y otro .js

<script src="../../Scripts/polymer.min.js"></script>
<script src="../../Scripts/adal.js"></script>

<polymer-element name="login-azuread" attributes="domain clientID" />
    <template>
        <content select="span"></content>
        <content select="a"></content>
        <content select="a"></content>
    </template>
    <script src="login.js"></script>
</polymer-element>

El fichero .html contiene la definición del componente web. Como véis el template utiliza la etiqueta content, que nos va a permitir que el contenido del componente no esté renderizado en el Shadow DOM y si en el DOM de la página.

window.config = {
    tenant: '',
    clientId: '',
    postLogoutRedirectUri: window.location.origin,
    cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
};

var authContext;

Polymer({
    userName: '',
    domain: '',
    clientID: '',
    ready: function () {
        window.config.tenant = this.domain;
        window.config.clientId = this.clientID;

        authContext = new AuthenticationContext(config);

        var isCallback = authContext.isCallback(window.location.hash);
        authContext.handleWindowCallback();

        if (isCallback &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; !authContext.getLoginError()) {
            window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
        }

        // Check Login Status, Update UI
        var user = authContext.getCachedUser();

        if (user)
            this.userName = user.userName;
        else
            this.userName = '';

    },
    attached: function () {
        var name = document.createElement("span");
        name.innerHTML = this.userName;
        name.className = "loginad-name";

        var login = document.createElement("a");
        login.href = "javascript:;";
        login.innerText = "Login";
        login.className = "loginad-a";
        login.addEventListener("click", this.login, true);

        if (this.userName != '')
            login.style.display = "none";
        else
            login.style.display = "inline-block";

        var logout = document.createElement("a");
        logout.href = "javascript:;";
        logout.innerText = "Logout";
        logout.className = "logoutad-a";
        logout.addEventListener("click", this.logout, true);

        if (this.userName != '')
            logout.style.display = "inline-block";
        else
            logout.style.display = "none";

        this.appendChild(name);
        this.appendChild(login);
        this.appendChild(logout);
    },
    login: function () {
        authContext.login();
    },
    logout: function () {
        authContext.logOut();
    }
});

La particularidad de este código es que, como podemos ver, utiliza la función ready de Polymer para cargar el objeto de configuración y comprobar si está autenticado el usuario y la función attached para inyectar todos los elementos que forman el componente, con la funcionalidad de cada uno.

¿Es mejorable?, como siempre, claro que lo es. Y me encantaría recibir feedback de qué más podríamos hacer para que este componente fuera realmente útil y pudiéramos usarlo en nuestras aplicaciones como una alternativa más.

Saludos, espero que lo hayáis encontrado interesante.

Autenticación de nuestra Aplicación usando Javascript ADAL

Muy buenas a todos,

En una entrada anterior, vimos como podíamos desarrollar una aplicación web con MVC .NET que nos permitiera acceder a la información guardada en nuestro Office 365. Una parte importante de ese desarrollo era la autenticación contra Azure AD, lo que se hacía por medio del paquete Nugget ADAL (Active Directory Authentication Library).

Hace unos días tuve conocimiento de una librería de Javascript que nos permitía hacer la autenticación contra Azure AD (conocida como Javascript ADAL) y que se podía utilizar en el desarrollo de Single Page Applications de una forma muy sencilla. Así que decidí probarlo para seguir abriendo y conociendo el abanico de oportunidades para trabajar contra Azure AD y luego con la API de Office 365. El primer paso obviamente será el login para después acceder y consultar la API usando CORS.

Así que en la entrada de hoy, os voy a mostrar cómo hacer el login usando esta librería de Javascript. Para el ejemplo he creado un proyecto de MVC vacío en el que he hecho todo el desarrollo. Antes de nada, os dejo, como siempre, los enlaces que me han servido de guía y donde creo que hay información muy valiosa sobre esta librería.

http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1/

https://github.com/AzureADSamples/SinglePageApp-jQuery-DotNet

Disponemos de una versión de la librería integrada con Angularjs y otra versión que podremos usar con JQuery y otros frameworks que será la versión que voy a usar en mi ejemplo.

Empezando con el ejemplo

Obviamente el primer paso en cualquier caso, es registrar nuestra aplicación en el Azure Active Directory de nuestro Tenant. Tras esto, tenemos que descargar el fichero adal.js.

El siguiente paso que he seguido es crear la vista del ejemplo, que es tal y como veréis a continuación

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <script src="~/Scripts/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
    <script src="~/Scripts/adal.js"></script>
    <script src="~/Scripts/App/app.js"></script>
</head>
<body>
    <div>
        <h2>Index</h2>

        <a href="javascript:;" class="app-login">Login</a>
        <a href="javascript:;" class="app-logout">Logout</a>
    </div>

</body>
</html>

Esta vista tiene dos botones, uno para hacer login y otro para hacer logout, nada más. El fichero que tiene todo el código es el fichero app.js que podéis ver ahora.

(function () {

    //1.- Initial configuration
    window.config = {
        tenant: '<tentantdomain>',
        clientId: '<clientid>',
        postLogoutRedirectUri: window.location.origin,
        cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
    };
    var authContext = new AuthenticationContext(config);

    //2.- Handle redirect after login
    var isCallback = authContext.isCallback(window.location.hash);
    authContext.handleWindowCallback();

    if (isCallback && !authContext.getLoginError()) {
        window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
    }

    //3.- Check for authenticated information
    var user = authContext.getCachedUser();
    if (user) {
        console.log(user);
    } else {
        console.log("error");
    }

    //4.- Handle events for login & logout
    var $signInButton = $(".app-login");

    $signInButton.click(function () {
        authContext.login();
    });

    var $signOutButton = $(".app-logout");

    $signOutButton.click(function () {
        authContext.logOut();
    });
}());

El código de este ejemplo es una simplificación del ejemplo de Github que os he puesto un poco más arriba y que he usado para entender el proceso que seguía. Lo único que hace este código es mostrar en la consola de depuración los datos del usuario si está logueado o mostrar un texto de error si no lo está. Los puntos más importantes del código son los siguiente:

  1. El primer paso es crear el objeto de configuración, con los datos del tenant y client id que hemos obtenido al registrar la aplicación
  2. A continuación se indica el callback que maneja la redirección después del login
  3. Se comprueba y se muestra la información de logueo del usuario
  4. Por último se crean los eventos que dispararán el login y el logout asociados al click en los enlaces

Y esto es todo, si lo probamos comprobaremos que el resultado es el esperado, cuando no estamos logueados, nos muestra la pantalla de signin de windows para indicar nuestro usuario y contraseña del Azure AD de nuestro tenant.

Lo que os voy a enseñar en próximas entradas es un componente web de Polymer que he creado que utiliza este código para implementar la funcionalidad de signin y signout de una manera muy sencilla y que creo que puede ser de utilidad. Este componente, de hecho ya está en mi GitHub

Un saludo a todos y buen finde.

Usando Bower y Grunt en Visual Studio 2013

Muy buenas a todos,

Hoy os quiero contar los pasos que he dado para usar en mis proyectos de Visual Studio 2013 bower, usándolo como gestor de paquetes en el lado del cliente, y grunt, como task runner. Ambos están siendo muy comúnmente usados en el mundo del desarrollo de aplicaciones web y si bien, la versión de Visual Studio 2015 los incluye de forma nativa, podemos configurar nuestros proyectos web de Visual Studio 2013 para usar estas dos herramientas tan útiles.

Para todo aquel que le pueda interesar, dejo la entrada del blog en la que me he basado para hacer toda la configuración, aunque si accedéis veréis que en el post, se usa otro gestor de tareas llamado gulp en lugar de grunt.

http://blogs.msdn.com/b/cdndevs/archive/2015/02/17/using-bower-with-visual-studio-2013.aspx

Pero antes de entrar en detalle, vamos a ver qué son exactamente cada una de estas herramientas.

¿Qué es Bower?

Bower works by fetching and installing packages from all over, taking care of hunting, finding, downloading, and saving the stuff you’re looking for.

La primera pregunta que se nos plantea viendo lo que hace Bower es ¿Y Nugget?. La respuesta a esta pregunta es muy sencilla. Desde su aparición Nugget se ha descubierto como una herramienta muy eficaz para la gestión de paquetes de servidor, pero no así para paquetes de cliente. Bower se ha convertido en el estándar de facto en el desarrollo web. Es poco probable, que un desarrollador que crea una nueva librería en Javascript la ponga disponible como paquete Nugget, sin embargo es frecuente que esté disponible en Bower en todas sus versiones. Esto provoca desactualización o imposibilidad de usar determinadas librerías a través de Nugget, lo que desaconseja su uso como herramientas de gestión de paquetes del lado del cliente.

¿Qué es Grunt?

In one word: automation. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes.

En resumen, con Grunt vamos a poder automatizar determinadas tareas que van a ayudar a tener nuestra aplicación lista para poner en producción con acciones como: minimizar y agrupar los estilos, minimizar y agrupar los javascript, etc. En la entrada veremos una tarea que he creado que puede ser necesaria y recomendable cuando usamos Bower.

Configurando y usando Bower

Para la configuración de Bower, podéis fácilmente seguir la entrada del blog de la msdn sin ningún problema, es muy sencilla de seguir y está muy detallada, de hecho yo la he seguido y es lo que de forma simplificada explicaré por aquí, solo hay una cosa que cambia, porque a mi al menos, o no me quedaba claro en la entrada inicial o no estaba debidamente reflejado. Vamos a ello.

    1. El primer paso es instalar node.js, ya que tanto Bower como Grunt lo utilizan. Enlace de descarga de node.js
    2. A continuación, abrimos la línea de comandos e instalamos bower
npm install bower -g
    1. Tras esto, necesitaremos tener instalado Git, Bower utiliza Git para descargar los paquetes. Concretamente instalaremos msysgit. Durante la instalación, tendremos que seleccionar la opción que vemos en la imagen a continuación para que se realice toda la configuración correctamente

gitinstall

    1. Ya tenemos listo Bower para ser utilizado. Si lo vamos a usar dentro de un proyecto de MVC, antes tendremos que tener en cuenta que por defecto, la plantilla de este tipo de proyectos en Visual Studio incluye una serie de paquetes, entre ellos: bootstrap, jquery, modernizr, etc. Por lo que antes de comenzar a usar bower en nuestro proyecto, tenemos que desinstalarlos. Para ello accedemos a la consola de Nuget y ejecutamos el siguiente comando para cada uno de los paquetes
Uninstall-Package <package>

Ahora que ya tenemos listo Bower vamos a usarlo dentro de nuestro proyecto. Esto lo haremos a través de la línea de comandos. En mi caso, no he usado la interfaz de línea de comandos propia de Windows (cmd), ya que me daba errores al usarla con Bower y Git, y buscando por internet vi que recomendaban usar una interfaz de línea de comandos que se instala con msysgit. Esta es Git Bash.

Vamos ahora a entrar al directorio del proyecto, y comenzaremos con el uso de Bower. Lo primero que haremos será iniciar el fichero de configuración de Bower. Para ello ejecutamos:

bower init

Una vez creado el fichero bower.json, podemos comenzar a instalar paquetes, esto lo haremos de la siguiente forma.

bower install jquery --save
bower install polymer --save

Una Buena Práctica recomendada para el uso de los paquetes que se obtienen con Bower, es no agregar directamente todos los archivos descargados con Bower a nuestro proyecto, ya que los paquetes en muchas ocasiones contienen más archivos de los que realmente necesitaremos, por lo que se recomienda copiar al proyecto solo aquellos archivos que realmente sean necesarios. ¿Cómo hacemos esto, manualmente?. La respuesta evidentemente es no, para esto tenemos nuestro Task Runner, Grunt 😉

Configurando y usando Grunt

En primer lugar, para añadir el soporte a Grunt para nuestro proyecto vamos a crear dos ficheros que añadiremos a la raíz del mismo package.json y gruntfile.js. El fichero package.json inicialmente contendrá la siguiente información.

{
"name": "WebApplication1",
"version": "1.0.0",
"description": "WebApplication package project",
"main": "gruntfile.js",
"license": "ISC";
}

A continuación y como ejemplo, la tarea que vamos a crear va a copiar los ficheros necesarios de los paquetes que hemos obtenido mediante Bower a la carpeta Scripts, donde se encuentran los ficheros js de nuestro proyecto. El código del fichero gruntfile.js es el siguiente:

module.exports = function (grunt) {

    grunt.initConfig({
        copy: {
            main: {
                files: [
                  {
                      expand: true,
                      cwd: '../bower_components/webcomponentsjs/',
                      src: ['webcomponents.js'],
                      dest: 'Scripts'
                  },
                  {
                      expand: true,
                      cwd: '../bower_components/jquery/dist',
                      src: ['jquery.min.js'],
                      dest: 'Scripts'
                  }
                ]
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.registerTask('build', ['copy']);
};

Obviamente, aún podríamos hacer algunas tareas de minimización y agrupamiento de ficheros con grunt y que serían necesarias, aunque eso lo dejo para más adelante, es posible que en alguna entrada futura os cuente como hacerlo.

Para que funcione correctamente, necesitamos un plugin de Grunt, «grunt-contrib-copy». Cuando necesitemos añadir determinados plugins para nuestras tareas de Grunt, lo haremos desde la línea de comandos usando la utilidad npm de node.js. Nos vamos al directorio del proyecto y ejecutamos el siguiente comando.

npm install grunt-contrib-copy --save-dev

Una vez que ya tenemos instalado el plugin, desde la línea de comandos en el directorio en el que está nuestro archivo gruntfile.js (la raíz del proyecto), ejecutamos grunt. Esto llevará a cabo la tarea que hemos creado. La primera vez que ejecutamos grunt, tendremos que añadir manualmente los archivos que se han añadido a la tarea al proyecto, aunque esta acción no tendremos que volver a hacerla.

Como hemos visto, hasta ahora estamos usando la línea de comando para ejecutar Grunt. Pero, ¿Y si queremos que se ejecute automáticamente Grunt cuando hacemos el deploy del proyecto, esto es posible?, la respuesta es si. Para esto tenemos varias extensiones de Visual Studio que nos lo permiten. En mi caso he utilizado Task Runner Explorer.

La descargamos y la instalamos y ya podremos usarla en nuestro Visual Studio 2013. Vamos a View->Other Windows->Task Runner Explorer y cuando se abre la nueva ventana, lo primero que hacemos es pulsar sobre el botón actualizar para que capture todas las tareas creadas si aún no lo habia hecho.

taskrunnerexplorer

Aquí vemos que podemos configurar si queremos que una determinada tarea se ejecute antes del deploy de la solución, con lo que podremos automatizar correctamente la ejecucion de Grunt y no tendremos que hacerlo manualmente a través de la línea de comandos.

run

binding

Os dejo para terminar, algunos enlaces más de utilidad relacionado con lo que hemos visto en la entrada:

http://gruntjs.com/
http://bower.io/
https://visualstudiogallery.msdn.microsoft.com/8e1b4368-4afb-467a-bc13-9650572db708

Y esto es todo por hoy, yo a partir de ahora voy a utilizar estas dos herramientas en todos mis proyectos, por las ventajas que ofrecen, espero que todos podáis hacerlo.

Un saludo y hasta la próxima

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.

Usando la API de Office 365

Muy buenas de nuevo a todos,

En la entrada anterior, os contaba como registrar aplicaciones en nuestro Azure AD, y os contaba que lo utilizaría posteriormente para crear aplicaciones web que pudieran acceder a la API de Office 365. Hoy os quiero contar como preparar un proyecto MVC para acceder y usar esta API y mostrar el código que he creado para probarlo todo.

Registrando una aplicación en Azure Active Directory

Actualmente disponemos de muchas opciones cuando nos planteamos extender nuestra plataforma de Office 365, y en función de los requerimientos podremos elegir unas u otras. Si lo que queremos es extender nuestro SharePoint OnLine, podemos optar por desarrollar una SharePoint Hosted o SharePoint Provider App, que nos permitirán manipular la información  de nuestro SharePoint en Office 365 por medio de API REST, JSOM o CSOM. Si en lugar de eso, queremos extender la funcionalidad de nuestro Office 365, y acceder y manipular la información de todo la suite de productividad, entonces podremos usar la API de Office 365, que no solo nos da acceso a SharePoint, sino que nos permite trabajar con Outlook, One Drive, Calendarios, etc.

Esta API de Office 365 y el modelo de aplicaciones de azure registradas en el Azure AD, parece que le va ganando mucho terreno al conjunto tradicional de opciones de desarrollo para SharePoint OnLine. La pregunta ahora es cual de todos ellos tendrá mayor proyección. Recientemente en algún WebCast, he preguntado sobre estos temas a algunos de los MVPs de SharePoint de España, y como conclusión creo que saco en claro que, si bien este último modelo que os comentaba y la API de Office 365 parece que seguirán creciendo y ganando en importancia, las apps para SharePoint y el modelo de objetos de cliente que se está convirtiendo en la herramienta básica de trabajo para los desarrolladores en SharePoint, seguirá teniendo su hueco en este mundo de posibilidades.

Así que toca empezar a ver cómo trabajar con esta API y este es el motivo de esta entrada, una primera toma de contacto con la API de Office 365. Para ello voy a desarrollar un proyecto MVC de ejemplo.

Creando y preparando el proyecto

Previo a empezar con el proyecto, es importante haber registrado en Azure AD una aplicación, para lo que recomiendo leer la entrada que os comentaba al principio del post.

En primer lugar vamos a crear un proyecto web de ASP.NET y seleccionaremos la plantilla MVC. Seleccionamos la plantilla porque ya lleva toda la configuración necesaria de autenticación que, en el caso de hacerlo a partir de un proyecto vacío, tendríamos que configurar a mano.

authentication

Cambiaremos el tipo de autenticación para seleccionar que queremos una autenticación contra Azure AD, y ahí indicaremos los parámetros necesarios.

dataorganizational

En el valor de App ID Url, seleccionaremos el mismo que el que indicamos cuando registramos nuestra aplicación en Azure AD y seleccionaremos la opción de sobreescribir si existe.

Una vez que se ha creado el proyecto, antes de continuar, tenemos que instalar de Nuget la Azure Directory Authentication Library (ADAL).

azure1adal

Y a continuación vamos a project->Add->Connected Services. Ahí nos pedirá registrar nuestra App y a continuación nos mostrará todos los servicios que tenemos disponibles de Office 365. Aquí podremos conceder distintos permisos a nuestra app con respecto a los servicios disponibles.

register permissions

Tras seleccionar los que queremos, vamos a properties y vamos a eliminar de las Urls de retorno la que no va con SSL, esto me ha dado bastante guerra, hasta que descubrí que había que hacerlo, porque tras la autenticación siempre me volvía a la URL con protocolo http y me daba error.

properties

Tras todos estos pasos, ya tenemos todo nuestro proyecto configurado para poder usar la API de Office 365 para los servicios que le hemos concedido permisos. Además si lo hemos hecho todo bien, veremos que si vamos a nuestro Azure AD, al apartado de aplicaciones y a su configuración, se ha configurado correctamente, y se han concedido los permisos necesarios y creado las claves correspondientes, además de que todo esto se habrá reflejado correctamente en nuestro web.config

Ahora llega el momento del código.

El código para acceder a la API de SharePoint

En mi caso, he metido todo el código dentro del método de la acción Index del controlador, al tratarse de un código de ejemplo, no he querido complicarlo mucho creando una clase de servicio y demás aspectos de «mejores prácticas», que se pueden o se deben hacer. Eso lo dejo para más adelante ;).

En el caso del ejemplo, lo que voy a hacer es mostrar en una lista, toda la información de los archivos que tengo en mi One Drive, ordenados de mayor a menor tamaño.

El código para trabajar con la API, puede resultar enrevesado y un poco engorroso al principio, pero al final, una vez que trabajas con él, te das cuenta que una parte importante, se repite siempre para todos los servicios y es común a todos, y que solo hay que cambiar la parte correspondiente a cada servicio concreto. Os pongo el código completo y os voy contando las partes más importantes.

public async Task<ActionResult> Index(string code)
{
     try
     {
          DiscoveryClient disco = Helpers.GetFromCache("DiscoveryClient&quot;) as DiscoveryClient;
          SharePointClient sharepointClient = Helpers.GetFromCache("SharePointClient") as SharePointClient;
          CapabilityDiscoveryResult filesDisco = Helpers.GetFromCache("CapabilityDiscoveryResult") as CapabilityDiscoveryResult;
          string authorized = Helpers.GetFromCache("authorized") as string;

          AuthenticationContext authContext = new AuthenticationContext(
             ConfigurationManager.AppSettings["ida:AuthorizationUri"] + "/common",
                   true);

          ClientCredential creds = new ClientCredential(
              ConfigurationManager.AppSettings["ida:ClientID"],
              ConfigurationManager.AppSettings["ida:Password"]);

          //1.- Looking for authentication code to get the discovery service
          if (code == null && disco == null)
          {
              Uri redirectUri = authContext.GetAuthorizationRequestURL(
                  discoResource,
                  creds.ClientId,
                  new Uri(Request.Url.AbsoluteUri.Split('?')[0]),
                  UserIdentifier.AnyUser,
                  string.Empty);

              return Redirect(redirectUri.ToString());
          }

          //2.- Getting the discovery client
          if (code != null && disco == null)
          {
              disco = new DiscoveryClient(new Uri(discoEndpoint), async () =>
              {

                  var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
                      code,
                      new Uri(Request.Url.AbsoluteUri.Split('?')[0]),
                      creds);

                  return authResult.AccessToken;
              });

              Helpers.SaveInCache("DiscoveryClient", disco);
          }

          //3.- Getting the capabilites to access to one drive files
          if (filesDisco == null)
          {
              filesDisco = await disco.DiscoverCapabilityAsync("MyFiles");
              Helpers.SaveInCache("CapabilityDiscoveryResult", filesDisco);
          }

          //4.- Looking for authentication to get sharepoint service
          if (filesDisco != null && authorized == null)
          {
              Helpers.SaveInCache("authorized", "");

              Uri redirectUri = authContext.GetAuthorizationRequestURL(
                  filesDisco.ServiceResourceId,
                  creds.ClientId,
                  new Uri(Request.Url.AbsoluteUri.Split('?')[0]),
                  UserIdentifier.AnyUser,
                  string.Empty);

              return Redirect(redirectUri.ToString());
          }

          //5.- Getting sharePoint Client
          if (sharepointClient == null)
          {
              sharepointClient = new SharePointClient(filesDisco.ServiceEndpointUri, async () =>
              {
                  var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
                      code,
                      new Uri(Request.Url.AbsoluteUri.Split('?')[0]),
                      creds);

                  return authResult.AccessToken;
              });

              Helpers.SaveInCache("SharePointClient", sharepointClient);
          }

          //6.- Reading files from sharepointClient
          var onedrivefiles = await sharepointClient.Files.ExecuteAsync();

          var filesOrdered = from files in onedrivefiles.CurrentPage
                             orderby files.Size descending
                             select files;

          List<OneDriveFile> modelList = new List<OneDriveFile>();

          //7.- Adding model list from files got from service
          foreach (var file in filesOrdered)
          {
              modelList.Add(new OneDriveFile
              {
                  Name = file.Name,
                  Size = file.Size,
                  Created = file.DateTimeCreated
              });
          }

          ViewBag.files = modelList;

          return View();
      }
      catch (Exception ex)
      {
          return View("Error");
      }
}
  1. Lo primero que haremos será es solicitar el token de autenticación en el caso de que no exista para poder acceder al servicio discovery
  2. El segundo paso, es obtener un discoveryClient a partir del token obtenido previamente y que nos servirá para solicitar las capacidades que necesitemos de la API de Office 365
  3. Tras esto, se solicitan las capacidades al servicio de discovery que habíamos cargado previamente.
  4. El siguiente paso, es solicitar el token de autenticación para el servicio correspondiente a la capacidad solicitada, si no está disponible. Hasta este punto, para todas las APIs de Office 365 es común, la única diferencia, es la capacidad que requeriremos en cada ocasión, lo que lo hace un buen candidato para encapsular correctamente, por eso decía antes que aunque pueden resultar engorrosos todos los pasos, el funcionamiento luego es muy repetitivo. En este caso, hemos solicitado la capacidad «MyFiles» para obtener la información de OneDrive.
  5. A continuación obtenemos un SharePointClient, para acceder ya al servicio de OneDrive
  6. En este momento, leemos los archivos y usando LinQ los ordenamos por tamaño
  7. Y ya para terminar los metemos en una lista del modelo de datos que hemos definido para mostrarlo en la vista posteriormente

Para el ejemplo he creado un modelo llamado OneDriveFile que os pongo también a continuación

public class OneDriveFile
{
    public string Name{ get; set;}
    public double Size{ get; set;}
    public DateTimeOffset Created {get; set;}
}

El resultado

Si ejecutamos el código, o lo depuramos, veremos, tras solicitar la autenticación contra el Azure AD, que nos muestra los archivos que tengo en mi One Drive de Office 365, y ordenados de mayor a menos tamaño

resultado

En mi caso la mayoría, de algunos eventos de desarrollo a los que he asistido y de los que aprendo casi todo :).

Antes de terminar, os dejo referencia de la información que me ha servido a mí para poder llevar a cabo este código, y que ha partido de un evento al que asistí con la gente de MadPoint, donde hicieron un ejemplo incluso más extenso que este sobre el uso de la API de Office 365. Yo básicamente he partido de ese código, y lo que he aprendido ha sido a hacer todas las configuraciones necesarias para que funcione, y modificar el mismo para que se ajuste exactamente al tipo de ejemplo que quería hacer, que si que se diferencia un poco del que se hizo en el evento. No obstante gracias por todo lo que aprendí allí. Os dejo los enlaces.

http://www.sinsharepointnohayparaiso.com/recursos-y-documentos

https://github.com/MadPoint/Eventos/tree/master/2015.02%20Prepara%20tus%20apps%20de%20SharePoint%20y%20Office%20365%20para%20el%20futuro

Y nada más por ahora, espero que os sirva este ejemplo, yo hasta que he configurado bien todos los aspectos, el registro de la app, los permisos, las URLs de retorno y todo lo necesario, he pasado algún tiempo entretenido.

Un saludo y hasta la próxima.

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.

RoomReservation: Mi primera SharePoint Hosted App

Muy buenas a todos,

Llevaba algunos días sin publicar nada en el blog, y es que he estado algo ocupado. Durante los meses de enero y febrero he estado trabajando en conocer el nuevo modelo de desarrollo de SharePoint, en qué consiste el nuevo modelo de Apps y que son y como se usan las API Cliente. He escrito algunos artículos sobre estos temas, relacionados con la API REST, CSOM y cómo crear SharePoint Hosted Apps. Pero para poder profundizar más en su conocimiento decidí crear una aplicación de SharePoint que tuviera en cuenta todo lo que había estado aprendiendo.

Por esto surge principalmente RoomReservation. RoomReservation es una SharePoint Hosted App que nos proporciona un sistema muy sencillo de reserva de salas. La aplicación, consiste en un formulario y un calendario. El formulario nos permite rellenar los campos básicos para hacer una reserva de salas, nos muestra la fecha, las horas y las salas disponibles. La utilidad de la aplicación, es que a medida que vamos completando los datos de fecha y hora las salas disponibles se van actualizando, permitiéndonos solo reservar aquellas salas que realmente están libres en el horario que estamos solicitando.

roomreservationapplication

Esta es la interfaz de la aplicación. Para que funcione correctamente, necesita dos lista, una lista personalizada con los recursos o salas disponibles y otra lista de tipo calendario donde se almacenarán las reservas realizadas.

El modelo de desarrollo que he seguido para la App ha sido el siguiente:

  • Todo el acceso a datos de SharePoint se ha hecho a través de API REST.
  • Las listas que son necesarias, son provisionadas remotamente por la propia aplicación, si tras instalarla, no existían ya. Para ello se ha usado también API REST, la aplicación evalúa si existen las listas, si no, automáticamente te redirige a una página donde tienes la opción de crear las listas.

install

Todo el código de la aplicación está en GitHub en el siguiente enlace:

https://github.com/jcroav/room-reservation

A partir de ahora comenzaré a utilizar más a menudo GitHub para subir mis desarrollos, e incluso ejemplos y demos que vaya publicando en mi blog.

Os animo a todos a que ayudéis RoomReservation o propongáis aspectos de mejora, serán siempre bienvenidos. Yo también intentaré aprovechar todo lo que he aprendido y lo que vaya a aprender próximamente para seguir mejorando esta App y desarrollando algunas nuevas.

¿Cuáles son los aspectos de mejora?

Como por supuesto no creo que sea perfecta, ni mucho menos, tengo ya previstas algunas áreas de mejora. Próximamente espero seguir trabajando en mejorar la interfaz gráfica, hacerla más adaptativa, añadir alguna funcionalidad extra y trabajar en los idiomas de la App, ya que ahora mismo solo está en inglés.

Pues nada más de momento, lo dicho, espero que os sirva como siempre, y que lo encontréis útil.

Un saludo y buena semana.