Tipos de géneros de libros - Anexo: Conexiones PnP
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
Hola a todo@s, continuando con la serie de entradas en el blog La librería, en esta entrada se pretende enseñar otra forma de comunicarse con SharePoint online a través de las librerías PnP que se ofrecen para usar en React.js.
He de informar que mientras estaba desarrollando y documentado sobre las comunicaciones con SharePoint online a través de los servicios REST , descubrí esta API de PnP y que ahora la agrego como anexo a lo ya programado del ejercicio para tener más formas de programar nuestro componente SPfx.
En el siguiente enlace Use with SharePoint Framework web parts de Microsoft es donde hablan de esta API de desarrollo, como utilizarlo y a través de otros enlaces que se ofrecen, se aportan más ejemplos para su utilización.
Gracias a que se ha implementado el componente SPfx, con un patrón de diseño Factoría abstracta, la implementación de este tipo de componente para establecer las comunicaciones con SharePoint online no supone mucho cambio con respecto a lo implementado hasta el momento en el ejercicio.
Para establecer este tipo de comunicación en nuestro proyecto SPfx, es necesario pasar por los siguientes apartados para entenderlo mejor su funcionamiento:
Instalación del componente PnP.
Conceptos y estructuración del componente SPfx.
Definición de la clase PnPWebApiCalls.
Instalación del componente PnP
Para la utilización de los componentes PnP en el proyecto SPfx, es necesario instalar una serie de paquetes npm. El comando para instalar los componentes necesarios es el siguiente:
npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp --save
Conceptos y estructuración del componente SPfx
He de informar que, para poder usar los componentes y funciones de la API de PnP en el componente SPfx, no es necesario declarar ningún objeto de esta API de desarrollo. La verdad es que no se como definir la estructura o definición del componente que se maneja, pero parece que se utiliza como un objeto estático el cual solo requiere configurar el contexto del webpart de nuestro componente SPfx.
Como se acaba de mencionar, este componente usar el contexto del componente SPfx. Este contexto es del tipo WebPartContext, que se encuentra definido en la librería @microsoft/sp-webpart-base para su importación.
import { WebPartContext } from '@microsoft/sp-webpart-base';
Debido a que se quiere utilizar esta nueva API de comunicación con SharePoint online, se ha tenido de reestructurar ciertos clases y definiciones del componente SPfx, para adaptarlo a las nuevas necesidades. Además, hay que tener en cuenta que cuando se definió el componente SPfx que se está utilizando, se eligió como framework de trabajo React, por lo que el contexto del tipo de objeto WebPartContext no está cargado inicialmente.
A continuación, se exponen las modificaciones realizadas en las diferentes componentes y clases que se utilizan actualmente en el componente SPfx.
Interfaz IManagerTypesOfBookGenresProps
El interfaz IManagerTypesOfBookGenresProps es el encargado de definir las propiedades iniciales del componente React principal, que en este ejercicio es ManagerTypesOfBookGenres, en el componente SPfx.
Se aplican los cambios necesarios, para poder establecer el nuevo modelo de comunicación con SharePoint online, quedando definido el interfaz de la siguiente manera.
import { WebPartContext} from '@microsoft/sp-webpart-base';
export interface IManagerTypesOfBookGenresProps {
webPartContext: WebPartContext;
enabledPnP:boolean;
}
De esta definición, hay que destacar los siguientes aspectos:
Las propiedades spHttpClient y pageContext se han eliminado, ya que ambas propiedades vienen definidas dentro de la propiedad nueva webPartContext.
La propiedad enabledPnP es una propiedad que se pretende utilizar para indicar al componente SPfx, con qué mecanismos (API REST o API PnP) se va a comunicar con SharePoint online.
Clase ManagerTypesOfBookGenresWebPart
Con el motivo de la modificación del interfaz IManagerTypesOfBookGenresProps que se usa en el componente React principal, la clase ManagerTypesOfBookGenresWebPart se ve afectadas en muchos, pero sencillos aspectos:
En el panel de configuraciones del WebPart.
En la información que se pasa inicialmente al componente React principal.
En la definición del interfaz IManagerTypesOfBookGenresWebPartProps perteneciente los props que se usa en la clase ManagerTypesOfBookGenresWebPart.
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'ManagerTypesOfBookGenresWebPartStrings';
import ManagerTypesOfBookGenres from './components/ManagerTypesOfBookGenres';
import { IManagerTypesOfBookGenresProps } from './components/IManagerTypesOfBookGenresProps';
export interface IManagerTypesOfBookGenresWebPartProps {
enabledPnP: boolean;
}
export default class ManagerTypesOfBookGenresWebPart extends BaseClientSideWebPart<IManagerTypesOfBookGenresWebPartProps> {
public render(): void {
const element: React.ReactElement<IManagerTypesOfBookGenresProps> = React.createElement(
ManagerTypesOfBookGenres,
{
webPartContext: this.context,
enabledPnP: this.properties.enabledPnP
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.GroupSettings,
groupFields: [
PropertyPaneToggle('enabledPnP', {
label: strings.PropertiesWebPartPanel.MethodPnP,
checked: false
})
]
}
]
}
]
};
}
}
De las modificaciones realizadas sobre esta clase, hay que destacar los siguientes aspectos:
-
Se ha definido una configuración a nivel de Webpart, con el que se pretende indicar que mecanismos de conexión se quiere establecer con SharePoint online.
Al introducir esta nueva configuración al Webpart, se tiene que modificar la definición del interfaz IManagerTypesOfBookGenresWebPartProps para poder indicar en el proceso del método Render de donde se obtiene el valor que se va a pasar al componente React ManagerTypesOfBookGenres.
-
En el método Render, en la definición y parametrización inicial de los props que se envían al componente React ManagerTypesOfBookGenres, se han eliminado las propiedades spHttpClient y pageContext siendo remplazas por la propiedad webPartContext que contiene en su definición la misma información que contenían las propiedades eliminadas.
A la vez, se agrega la propiedad enabledPnP que se utilizara para saber cómo se debe establecer las comunicaciones con SharePoint online.
Ficheros de recursos
Al introducir las configuraciones en el panel del Webpart, se han tenido que definir nuevos literales, necesitando modificar las definiciones de los ficheros de recursos implicados para una correcta visualización de la información.
-
En el fichero mystrings.d.ts que contiene la definición IManagerTypesOfBookGenresWebPartStrings se han definido los literales relacionados con la información nueva del panel de configuración.
declare interface IManagerTypesOfBookGenresWebPartStrings { PropertyPaneDescription: string; GroupSettings: string; PropertiesWebPartPanel: { MethodPnP: string; } … … } -
En los ficheros de recursos es-es.js y en-us.js, se han establecido los literales correspondientes al idioma.
define([], function () { return { "PropertyPaneDescription": "En este panel emergente del web part SPfx ‘Gestor de tipos de géneros de libros’, se configuran sus características para su correcto funcionamiento.", "GroupSettings": "Configuraciones de elementos web", "PropertiesWebPartPanel": { "MethodPnP": "¿Quiere usar las conexiones PnP a SharePoint?" }, … … } });define([], function () { return { "PropertyPaneDescription": "In this pop-up panel of the web part SPfx 'Manager of types of genres of books', its characteristics are configured for its correct functioning.", "GroupSettings": "Web Part settings", "PropertiesWebPartPanel": { "MethodPnP": "Do you want to use PnP connections to SharePoint?" }, … … } });
Clase SPfxWebApiCalls
Aun que se podía seguir pasando al constructor de la clase SPfxWebApiCalls las propiedades spHttpClient y pageContext, para poder seguir estableciendo las comunicaciones con SharePoint online a través de los servicios REST, se ha tomado la decisión de cambiar los parámetros del constructor y obtener estas propiedades dentro de la lógica de negocio del constructor.
De esta forma, se normalizan las clases que se manejan en las comunicaciones con SharePoint online.
…
…
import { WebPartContext } from '@microsoft/sp-webpart-base';
export class SPfxWebApiCalls extends WebApiCalls {
…
…
constructor(webPartContext: WebPartContext) {
super();
this._spHttpClient = webPartContext.spHttpClient;
this._pageContext = webPartContext.pageContext;
}
…
…
}
Clase PnPWebApiCalls
La clase PnPWebApiCalls es la clase destinada comunicarse con SharePoint online utilizando la API de PnP, para la gestión de los tipos de géneros de libros.
Hay que informar que por ahora se va a definir la clase y su constructor. Más adelante en la entrada se explicará cómo realizar las comunicaciones con SharePoint online.
import { WebApiCalls } from './WebApiCalls';
import { IItemGenre } from './../Entities/IItemGenre';
import { IListItemsGenre } from './../Entities/IListItemsGenre';
import { WebPartContext } from '@microsoft/sp-webpart-base';
export class PnPWebApiCalls extends WebApiCalls {
constructor(webPartContext: WebPartContext) {
super();
}
protected executeGetRepositoryGenres(): Promise<IListItemsGenre> {
throw new Error("Method not implemented.");
}
protected executeGetRepositoryGenreById(id: number): Promise<IItemGenre> {
throw new Error("Method not implemented.");
}
protected executeAddRepositoryGenres(newItem: IItemGenre): Promise<IListItemsGenre> {
throw new Error("Method not implemented.");
}
protected executeExistGenreInRepository(title: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
protected executeUpdateRepositoryGenres(updateItem: IItemGenre): Promise<IListItemsGenre> {
throw new Error("Method not implemented.");
}
protected executeDeleteRepositoryGenres(id: number): Promise<IListItemsGenre> {
throw new Error("Method not implemented.");
}
}
Componente ManagerTypesOfBookGenres
Con los cambios realizados hasta el momento, ya solo falta adaptar en el componente ManagerTypesOfBookGenres los nuevos parámetros procedentes de los props y configurar la lógica de negocio según sus valores.
En realidad, solo hay que modificar el método componentWillMount, más concretamente en la instancia de un objeto de la clase SPfxWebApiCalls. La nueva implementación quedaría de la siguiente manera.
public componentWillMount() {
let auxClassWebApiCalls: IWebApiCalls = null;
if (Environment.type === EnvironmentType.Local) {
auxClassWebApiCalls = new MockWebApiCalls();
}
else if (Environment.type === EnvironmentType.SharePoint ||
Environment.type === EnvironmentType.ClassicSharePoint) {
if (this.props.enabledPnP) {
auxClassWebApiCalls = new PnPWebApiCalls(this.props.webPartContext);
}
else {
auxClassWebApiCalls = new SPfxWebApiCalls(this.props.webPartContext);
}
}
auxClassWebApiCalls.getRepositoryGenres().then((_response: IListItemsGenre) => {
if (_response != null) {
let auxOptimizedProcesses = this.state.optimizedProcesses;
auxOptimizedProcesses.renderedItems = _response.value;
this.setState({
classWebApiCalls: auxClassWebApiCalls,
optimizedProcesses: auxOptimizedProcesses
});
}
});
}
De este método, hay que destacar los siguientes aspectos:
Se ha adaptado la declaración del SPfxWebApiCalls a su nuevo constructor, el cual recibe ahora el contexto del Webpart.
Dependiendo de la configuración establecida en el Webpart, se crea un objeto de tipo SPfxWebApiCalls o de tipo PnPWebApiCalls.
Definición de la clase PnPWebApiCalls
La clase PnPWebApiCalls tiene que implementar los mismos métodos que las clases SPfxWebApiCalls y MockWebApiCalls, al heredar las propiedades y métodos de la clase WebApiCalls. Eso sí, su implementación no es igual y puede disponer de más métodos privados para su propio uso.
He de informar que, para usar esta API, es necesario realizar más operativas, para realizar la operativas que si se aplican sobre los servicios REST para establecer las comunicaciones con SharePoint online. La forma de trabajar es muy similar a trabajar con la API del modelo de objetos de cliente de SharePoint, en el que se tiene que definir los objetos y sus propiedades antes de realizar la operativa ExecuteQuery. La diferencia esta en que los objetos y propiedades no los pruebes procesar por lotes o por lo menos yo no lo he conseguido.
Constructor de la clase
Debido a que es necesario incorporar las configuraciones necesarias para poder realizar las comunicaciones con SharePoint online a través de la API de PnP, se ha definido un constructor al que se le pasa como parámetro el contexto del Webpart del componente SPfx.
…
…
const _selectFields: string = "Id,Title";
export class PnPWebApiCalls extends WebApiCalls {
private _webPartContext: WebPartContext;
constructor(webPartContext: WebPartContext) {
super();
this._webPartContext = webPartContext;
sp.setup({
spfxContext: this._webPartContext
});
}
…
…
}
De este método, hay que destacar los siguientes aspectos:
El contexto del Webpart del componente SPfx, se guarda por si acaso en una propiedad interna de la clase.
Para configurar la API de PnP, solo es necesario establecer en sus configuraciones, el contexto con el que funciona el componente SPfx. A partir de aquí, es la propia API de PnP, en realizar todo lo necesario para establecer las comunicaciones con SharePoint online.
Se ha definido una constante interna, con los campos del repositorio que se quiere obtener en las consultas que se realicen.
Método executeGetRepositoryGenres
En este método destinado a la obtención de todos los tipos de géneros de libros y en todos los demás de la clase, se puede observar que se anidan operativas asíncronas para obtener los resultados deseado.
He de recordar que, con esta API, es necesario solicitar los objetos y sus valores en diferentes peticiones, provocando al final que las operativas en la clase, este anidadas.
protected async executeGetRepositoryGenres(): Promise<IListItemsGenre> {
let result = this.getList().then(resolveList => {
let resolveListItems = resolveList.items.select(_selectFields)
.orderBy("Title", true)
.get().then(resolveItems => {
let listItems: IListItemsGenre = { value: resolveItems };
return listItems;
});
return resolveListItems;
});
return Promise.resolve(result);
}
De este método, hay que destacar los siguientes aspectos:
Se utiliza un método privada propio de la clase, destinado a la obtención de la lista sobre la que se realizan todas las operativas del método.
Con la obtención de la lista, se procede a configurar los parámetros de la consulta que se ejecuta con el método Get que proporciona la API.
-
Los datos devueltos de la consulta realizada sobre el repositorio no son del tipo IListItemsGenre, como las otras clases implementadas del interfaz IWebApiCalls, sino que devuelve directamente un array del tipo IItemGenre.
Es necesario trasformar los resultados al tipo definido en el método.
Método executeGetRepositoryGenreById
Promise<IItemGenre> {
let result = this.getList().then(resolveList => {
let resultItem = resolveList.items.getById(id)
.select(_selectFields)
.get().then((resolveItems: IItemGenre) => {
return resolveItems;
});
return resultItem;
});
return Promise.resolve(result);
}
He de informar que, es método destinado a obtener un elemento por el identificador que tiene en la lista, tiene la particularidad de que el resultado obtenido de la operativa getById, no se tiene un objeto del tipo IItemGenre, sino que es necesario volver a realizar el método Get, para obtener el elemento con el tipo necesario del método.
Método executeExistGenreInRepository
Este método es muy similar al método executeGetRepositoryGenres pero con la salvedad que se filtra los datos devueltos por el título del tipo de genero de libro.
protected async executeExistGenreInRepository(title: string): Promise<boolean> {
let result = this.getList().then(resolveList => {
let resolveFilter = resolveList.items.filter("".concat("Title eq '", title, "'"))
.get().then(resolveItems => {
return resolveItems.length > 0 ? true : false;
});
return resolveFilter;
});
return Promise.resolve(result);
}
Método executeAddRepositoryGenres
protected async executeAddRepositoryGenres(newItem: IItemGenre): Promise<IListItemsGenre> {
let result = this.getList().then(resolveList => {
let _newItem = { Title: newItem.Title };
let resultAdd = resolveList.items.add(_newItem)
.then(() => {
return this.executeGetRepositoryGenres();
});
return resultAdd;
});
return result;
}
De este método, para agregar un nuevo tipo de genero de libro, hay que destacar los siguientes aspectos:
Hay que definir un objeto con las propiedades iniciales que se aportan en el proceso de crear un elemento nuevo en la lista.
Con el nuevo elemento creado en la lista, se procede a devolver un listado de los tipos de géneros de libros registrados, para actualizar el interfaz de usuario.
Método executeUpdateRepositoryGenres
protected async executeUpdateRepositoryGenres(updateItem: IItemGenre): Promise<IListItemsGenre> {
let result = this.getList().then(resolveList => {
let _updateItem = { Title: updateItem.Title };
let resultUpdate = resolveList.items.getById(updateItem.Id)
.update(_updateItem).then(() => {
return this.executeGetRepositoryGenres();
});
return resultUpdate;
});
return result;
}
De este método para actualizar un tipo de genero de libro, hay que destacar los siguientes aspectos:
La necesidad de realizar una consulta previa por el identificador del tipo de genero de libro.
Definir un objeto con las propiedades que se quieren actualizar, que son aportada al proceso de actualización del elemento previamente obtenido por su identificador.
Actualizado el elemento en la lista, se procede a devolver un listado de los tipos de géneros de libros registrados, para actualizar el interfaz de usuario.
Método executeDeleteRepositoryGenres
protected async executeDeleteRepositoryGenres(id: number): Promise<IListItemsGenre> {
let result = this.getList().then(resolveList => {
let resultUpdate = resolveList.items.getById(id)
.delete().then(() => {
return this.executeGetRepositoryGenres();
});
return resultUpdate;
});
return result;
}
De este método para eliminar un tipo de genero de libro, hay que destacar los siguientes aspectos:
La necesidad de realizar una consulta previa por el identificador del tipo de genero de libro.
Eliminado el tipo de genero de libro, se procede a devolver un listado de los tipos de géneros de libros registrados, para actualizar el interfaz de usuario.
Método getList
Este método privado, se utiliza en los demás métodos de la clase, para obtener la lista donde se realizan las operativas de la clase.
private async getList(): Promise<List> {
const web = await sp.site.getRootWeb();
const serverRelativeUrl = await web.select("ServerRelativeUrl").get().then(resolveWeb => {
return Promise.resolve(resolveWeb.ServerRelativeUrl);
});
return Promise.resolve(web.getList("".concat(serverRelativeUrl, "/", Constants.Lists.ByUrlRelative.Genres)));
}
De este método, hay que destacar los siguientes aspectos:
-
Para la obtención de la lista, se realiza a través del método getList del sitio web, al cual se le pasa la url relativa de la lista con respecto a la url del tenant.
Quiero informar que, se puede usar el título de la lista para obtener el objeto de lista en la API de PnP. Una cosa que me ha enseñado mi trayectoria en SharePoint es que mejor desarrollar usando las url relativas de las listas y bibliotecas que usar sus títulos, ya que a veces a los clientes les da por cambiar el título de las listas, y eso implica una mayor gestión al cambio, si los métodos de obtención de listas se basan en el título.
Una vez más se ha tenido que realizar varias consultas sobre un mismo objeto para disponer de todo lo necesario, para obtener la lista.
Clase Constants
Como consecuencia de obtener las listas por url relativas, se ha tenido que ampliar las constantes relacionadas con las listas, para almacenar las url relativas de la definición de las listas.
export const Constants = {
ControlId: {
FieldTitle: "Title"
},
Lists: {
ByName: {
Genres: "Genres"
},
ByUrlRelative:{
Genres:"Lists/Genres"
}
}
};
Conclusiones sobre el API de PnP
Una vez implementado los mecanismos para utilizar la API de PnP y de realizar diferentes pruebas, he llegado a la conclusión que al igual que si se hace las conexiones con SharePoint online a través de los servicios REST, existen también luces y sombras a la hora de desarrollar.
Como parte positiva de la API destacaría los siguientes aspectos:
La posibilidad de usar url relativas para la obtención de listas, evitando o gestionando de una forma más eficiente la gestión al cambio.
Para mí, la forma de implementar la lógica de negocio me transmite la sensación de que queda mejor explicado el código fuente con respecto a la API de los servicios REST.
No es necesario implementar cabeceras a las operativas de alta, baja y modificaciones, como es necesario usando los servicios REST.
Aun que es necesario obtener el elemento que se pretende eliminar, no es necesario almacenar identificadores como en los procesos de los servicios REST.
En la parte negativa, destacaría los siguientes aspectos:
El no poder realizar peticiones por lote como en el modelo de objeto cliente de SharePoint, es frustrante, ya que hay que anidar llamadas asíncronas hasta obtener los resultados deseados y puede complicar más el desarrollo.
Tras realizar múltiples operativas con esta API, me ha dado la sensación de que los procesos tardan más tiempos que si se realizan con los servicios REST. Entiendo que esto puede ser por no poder procesar las operativas por lotes.
La decisión final y como buen consultor que soy, es que toméis la opción que mejor se desenvuelva en vuestros proyectos, ya que lo único que pretendía con esta entrada del blog, es enseñar otra forma de establecer las comunicaciones con SharePoint online.
- Obtener enlace
- X
- Correo electrónico
- Otras aplicaciones
