Lo sviluppo di una Client Side Web Part tramite lo SharePoint Framework può essere fatto per la maggior parte del tempo in locale grazie al Local Workbanch: una sorta di sanbox che simula le modern pages di SharePoint Online e che ci permette quindi di testare le nostre web part senza essere collegati ad internet o senza avere un tenant Office 365 su cui installare le proprie soluzioni. Abbiamo parlato del Local Workbanch durante uno dei post della mia serie introduttiva allo SharePoint Framework.
Però un momento.. e i dati?
Chi è abituato a lavorare con SharePoint ed ha sviluppato web part nel corso degli ultimi anni, ha sicuramente interagito con il suo contesto effettuando operazioni su liste o sfruttando uno o più dei servizi disponibili (la ricerca, lo user profile, ecc..). Lavorando in locale queste cose vengono meno e utilizzando lo SharePoint Framework è diventata subito una buona pratica quella di creare un mock di questi dati/servizi, così da poter staccare totalmente la logica di accesso ai dati di SharePoint dal resto della logica propria della web part (logiche di business, UI, validazione, ecc...).
In questo post vedremo un metodo per creare questi mock, sfruttando le interfacce offerte da Typescript ed una particolare implementazione del pattern Factory.
Capire in che contesto si trova la propria Client Side Web Part
Per capire se la web part viene eseguita all'interno del Local Workbanch o all'interno di un contesto SharePoint valido, abbiamo in aiuto due classi Typescript presenti all'interno delle librerie dello SharePoint Framework. Si tratta delle classi Environment ed EnvironmentType. Vediamo come possiamo utilizzarle con un breve esempio.
import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; //import omessi per facilitare la lettura export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> { public render(): void { if(Environment.type == EnvironmentType.Local) { //sono nel local workbanch } else if(Environment.type == EnvironmentType.SharePoint) { //sono in un contesto SharePoint } else if(Environment.type == EnvironmentType.ClassicSharePoint) { //sono in un contesto SharePoint, ma in una web part page } else if(Environment.type == EnvironmentType.Test) { //sono in uno unit test } } }
Come potete vedere, la classe EnvironmentType si comporta come se fosse una enum C# e ci permette di applicare logiche differenti in base alla posizione della web part. La web part può infatti essere inserita all'interno del Local Workbanch, all'interno di una modern page di SharePoint, all'interno di una web part page di SharePoint (le vecchie pagine) oppure all'interno di uno unit test e noi possiamo implementare comportamenti differenti in base a questa informazione.
Implementare l'interfaccia per il proprio Data Access Layer
Per far sì che la nostra web part non debba cambiare il suo comportamento in base al contesto, è necessario definire un'interfaccia contenente tutti i metodi che si occuperanno di ritornare i dati (finti o preso da SharePoint). Lo stesso per tutte le interfacce utili a definire i dati da ritornare.
Nel nostro esempio, voglio semplicemente stampare a video il classico elenco di liste.
export interface IDataManager { SPContext: IWebPartContext; GetLists(): Promise<Model.IList[]>; }
Nel nostro caso, voglio fare in modo di ritornare dei dati finti nel caso in cui la web part giri all'interno del Local Workbanch mentre invece, nel momento in cui la sposto all'interno di un sito SharePoint vero e proprio, vorrei che leggesse i dati dal sito corrente (chiamando le REST API di SharePoint e tutto il resto).
Ecco quindi le due implementazioni di tale interfaccia, una contenente dei dati fake e l'altra contenente una chiamata alle API REST di SharePoint Online effettuata tramite la libreria PNP Core JS.
Implementazione mock
import * as Model from '../models/Model'; import { IDataManager } from './Managers'; import { IWebPartContext } from '@microsoft/sp-webpart-base'; export class MockDataManager implements IDataManager { public SPContext: IWebPartContext; private _serverLoadDelay: number = 1000; public GetLists(): Promise<Model.IList[]> { return new Promise<Model.IList[]>((resolve, reject) => { //simulate server load... this.Sleep(this._serverLoadDelay).then(() => { var results = [ { Title: "Contact list 1", Id: "f6785ba2-30a3-4b2d-a756-9371d416ae67" }, { Title: "Contact list 2", Id: "cc475be1-b08b-49c4-9776-b26c31d18216" }, { Title: "Contact list 3", Id: "3c15be57-41c8-45be-9456-abe04fbe00ed" } ]; resolve(results); }); }); }; private Sleep (delay) { return new Promise((resolve) => { setTimeout(resolve, delay); }); } }
Implementazione chiamata alle API di SharePoint
import { IDataManager } from './Managers'; import * as Model from '../models/Model'; import { IWebPartContext } from '@microsoft/sp-webpart-base'; import pnp from 'sp-pnp-js'; export class SPDataManager implements IDataManager { public SPContext: IWebPartContext; public GetLists(): Promise<Model.IList[]> { return new Promise<Model.IList[]>((resolve, reject) => { var results: Model.IList[] = []; pnp.sp.web.lists.select("Id", "Title").get().then((lists) => { lists.forEach(list => { results.push({ Id: list.Id, Title: list.Title }); }); resolve(results); }); }); }; }
Entrambe le implementazioni ritornano la stessa struttura di dati e offrono le stesse proprietà, questo ci permette di costruire il resto della logica della web part e la relativa interfaccia grafica astraendoci da come questi dati vengono recuperati.
All'interno del manager che produce dati finti ho aggiunto anche una Promise che simula alcuni secondi di caricamento dati dal server. Questo piccolo accorgimento risulta veramente molto utile per implementare tutte le logiche di visualizzazione dati in maniera asincrona all'interno della nostra web part.
Utilizzare il pattern Factory per recuperare i dati in base al contesto corrente
Bene, ora che abbiamo implementato i due manager, vediamo come possiamo scegliere il manager corretto in base al contesto.
So che ci basterebbe una serie di "if" come abbiamo fatto nel primo estratto di codice presente in questo post, ma la soluzione più elegante è quella di utilizzare una particolare implementazione del pattern Factory. Ecco come:
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library'; import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { IHelloWorldWebPartProps } from './IHelloWorldWebPartProps'; import * as Managers from './managers/Managers'; import { Dictionary } from 'sp-pnp-js'; export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> { private _managers = new Dictionary( [EnvironmentType.Local.toString(), EnvironmentType.SharePoint.toString()], [new Managers.MockDataManager(), new Managers.SPDataManager()] ); private _dataManger: Managers.IDataManager; public onInit(): Promise<void> { this._dataManger = this._managers.get(Environment.type.toString()); this._dataManger.SPContext = this.context; return super.onInit(); }; public render(): void { this._dataManger.GetLists().then((results) => { var html = "<ul>"; results.forEach(element => { html += "<li>" + element.Title + "</li>"; }); html += "</ul>"; this.domElement.innerHTML = html; }); } }
Come avrete capito, ho omesso tutte le parti del sorgente che non erano importanti per facilitare la lettura. Trovate l'intera soluzione in questo repository Github.
Se volete imparare a muovere i primi passi con lo SharePoint Framework, fate riferimento alla serie di post che ho scritto sull'argomento.