SharePoint Framework - Implementing Separation of Concerns (SoC)

Overview
 
SharePoint Framework client web parts are developed using TypeScript and any supporting JavaScript framework (for e.g. React, Angular, KnockOut, etc.). The SPFx solution provides a basic structure to start developing. However, we can take one step further to implement the best practices.
 
In this article, we will explore how we can implement the Separation of Concerns (SoC) principle in a SharePoint Framework solution.
 
Separation of Concerns (SoC) Overview
 
Separation of Concerns is a design principle for separating our program (or solution) into a distinct section, wherein, each section addresses a separate concern.
 
A code is split into sections with each one responsible for its own functionality (e.g. business logic, presentation logic, user interface, etc.).
SharePoint Framework - Implementing Separation of Concerns (SoC) 
Each section is independent and does not need to know the internals of other section. They only need to know how to communicate with each other by passing a certain information and get the desired result.
 
Advantages
  1. Each section is easier to maintain
  2. Each section can be easily unit tested
  3. Each section can be rewritten if needed, without affecting the other sections
In an SPFx solution, we can refer to each section as a Service.
 
Build SoC Scenario
 
In this article, we will reuse React Based OrgChart implemented in the previous article. Download the source code from the previous article to get started with implementing SoC.
 
The React component OrgChartViewer.tsx at \src\webparts\orgChartViewer\components\ has all the data access, business and presentation logic. We will start implementing by services to develop an independent section.
 
An OrgChartService class
  1. Under src, create a folder named "services".
  2. Add a file OrgChartService.ts file under it.

    SharePoint Framework - Implementing Separation of Concerns (SoC)

  3. We will move all the data related methods from UI to this service class
  4. Also, declare the public interface of the service
IDataService.ts
  1. import { IOrgChartItem, ChartItem } from './IOrgChartItem';  
  2.   
  3. export interface IDataService {  
  4.     getOrgChartInfo: (listName?: string) => Promise<any>;  
  5. }  
OrgChartService.ts
  1. import { ServiceScope, ServiceKey } from "@microsoft/sp-core-library";  
  2. import { IOrgChartItem, ChartItem } from './IOrgChartItem';  
  3. import { IDataService } from './IDataService';  
  4. import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';  
  5. import { PageContext } from '@microsoft/sp-page-context';  
  6.   
  7. export class OrgChartService implements IDataService {  
  8.     public static readonly serviceKey: ServiceKey<IDataService> = ServiceKey.create<IDataService>('orgChart:data-service', OrgChartService);  
  9.     private _spHttpClient: SPHttpClient;  
  10.     private _pageContext: PageContext;  
  11.     private _currentWebUrl: string;  
  12.   
  13.     constructor(serviceScope: ServiceScope) {  
  14.         serviceScope.whenFinished(() => {  
  15.             // Configure the required dependencies  
  16.             this._spHttpClient = serviceScope.consume(SPHttpClient.serviceKey);  
  17.             this._pageContext = serviceScope.consume(PageContext.serviceKey);  
  18.             this._currentWebUrl = this._pageContext.web.absoluteUrl;  
  19.         });  
  20.     }  
  21.   
  22.     public getOrgChartInfo(listName?: string): Promise<IOrgChartItem[]> {  
  23.       return new Promise<IOrgChartItem[]>((resolve: (itemId: IOrgChartItem[]) => void, reject: (error: any) => void): void => {  
  24.         this.readOrgChartItems(listName)  
  25.           .then((orgChartItems: IOrgChartItem[]): void => {  
  26.             resolve(this.processOrgChartItems(orgChartItems));  
  27.           });  
  28.       });  
  29.     }  
  30.   
  31.     private readOrgChartItems(listName: string): Promise<IOrgChartItem[]> {  
  32.       return new Promise<IOrgChartItem[]>((resolve: (itemId: IOrgChartItem[]) => void, reject: (error: any) => void): void => {  
  33.         this._spHttpClient.get(`${this._currentWebUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,URL,Parent/Id,Parent/Title&$expand=Parent/Id&$orderby=Parent/Id asc`,  
  34.         SPHttpClient.configurations.v1,  
  35.         {  
  36.           headers: {  
  37.             'Accept''application/json;odata=nometadata',  
  38.             'odata-version'''  
  39.           }  
  40.         })  
  41.         .then((response: SPHttpClientResponse): Promise<{ value: IOrgChartItem[] }> => {  
  42.           return response.json();  
  43.         })  
  44.         .then((response: { value: IOrgChartItem[] }): void => {  
  45.           resolve(response.value);  
  46.         }, (error: any): void => {  
  47.           reject(error);  
  48.         });  
  49.       });      
  50.     }  
  51.       
  52.     private processOrgChartItems(orgChartItems: IOrgChartItem[]): any {  
  53.         let orgChartNodes: Array<ChartItem> = [];  
  54.   
  55.         var count: number;  
  56.         for (count = 0; count < orgChartItems.length; count++) {  
  57.             orgChartNodes.push(new ChartItem(orgChartItems[count].Id, orgChartItems[count].Title, orgChartItems[count].Url, orgChartItems[count].Parent ? orgChartItems[count].Parent.Id : undefined));  
  58.         }  
  59.   
  60.         var arrayToTree: any = require('array-to-tree');  
  61.         var orgChartHierarchyNodes: any = arrayToTree(orgChartNodes);  
  62.         var output: any = JSON.stringify(orgChartHierarchyNodes[0]);  
  63.   
  64.         return JSON.parse(output);  
  65.     }  
  66. }  
What is ServiceKey and ServiceScope?
 
These classes allow for implementing the dependency injection. Instead of passing a reference to single dependency, we can pass scope as an argument to section and section calls consume() method to call the needed service.
 
The below line of code declares the service key.
  1. public static readonly serviceKey: ServiceKey<IOrgChartItem> = ServiceKey.create<IOrgChartItem>('orgChart:data-service', OrgChartService);  
The key will help to identify service within the scope. To ensure default implementation always exists, it is better to always call consume() inside a callback from serviceScope.whenFinished()
 
Implement Mock Data Service
 
When the web part is running on local SharePoint workbench, the mock data service can provide the mock data to the web part.
 
Add MockDataService.ts file under “\src\services” folder
  1. import { ServiceScope, ServiceKey } from "@microsoft/sp-core-library";  
  2. import { IDataService } from './IDataService';  
  3.   
  4. export class MockDataService implements IDataService {  
  5.     public static readonly serviceKey: ServiceKey<IDataService> = ServiceKey.create<IDataService>('orgChart:mock-service', MockDataService);  
  6.   
  7.     constructor(serviceScope: ServiceScope) {  
  8.     }  
  9.   
  10.     public getOrgChartInfo(): Promise<any> {  
  11.         const initechOrg: any =   
  12.           {  
  13.             id: 1,  
  14.             title: "ROOT",  
  15.             url: {Description: "Microsoft", Url: "http://www.microsoft.com"},  
  16.             children:[  
  17.               {  
  18.                 id: 2,  
  19.                 title: "Parent 1",  
  20.                 url: null,  
  21.                 parent_id: 1,  
  22.                 children:[  
  23.                   { id: 3, title: "Child 11", parent_id: 2, url: null },  
  24.                   { id: 5, title: "Child 12", parent_id: 2, url: null },  
  25.                   { id: 6, title: "Child 13", parent_id: 2, url: null }  
  26.                 ]  
  27.               },  
  28.               {  
  29.                 id: 7,  
  30.                 title: "Parent 2",  
  31.                 url: null,  
  32.                 parent_id: 1,  
  33.                 children:[  
  34.                   { id: 8, title: "Child 21", parent_id: 7, url: null },  
  35.                   { id: 9, title: "Child 22", parent_id: 7, url: null }  
  36.                 ]  
  37.               },  
  38.               {  
  39.                 id: 10,  
  40.                 title: "Parent 3",  
  41.                 url: null,  
  42.                 parent_id: 1,  
  43.                 children:[  
  44.                   { id: 11, title: "Child 31", parent_id: 10, url: null },  
  45.                   { id: 12, title: "Child 32", parent_id: 10, url: null }  
  46.                 ]  
  47.               }  
  48.             ]  
  49.           };  
  50.       
  51.         return new Promise<any>((resolve, reject) => {  
  52.           resolve(JSON.parse(JSON.stringify(initechOrg)));  
  53.         });  
  54.       }  
  55. }  
Update WebPart class to consume Service
  1. Open web part class OrgChartViewer.tsx under “\src\webparts\orgChartViewer\components\”.
  2. Update the class to consume the implemented service.
  1. import * as React from 'react';  
  2. import styles from './OrgChartViewer.module.scss';  
  3. import { IOrgChartViewerProps } from './IOrgChartViewerProps';  
  4. import { IOrgChartViewerState } from './IOrgChartViewerState';  
  5. import { IOrgChartItem, ChartItem } from '../../../services/IOrgChartItem';  
  6. import { IDataNode, OrgChartNode } from './OrgChartNode';  
  7. import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';  
  8. import { escape } from '@microsoft/sp-lodash-subset';  
  9. import { ServiceScope, Environment, EnvironmentType } from '@microsoft/sp-core-library';  
  10. import { OrgChartService } from '../../../services/OrgChartService';  
  11. import { MockDataService } from '../../../services/MockDataService';  
  12. import { IDataService } from '../../../services/IDataService';  
  13. import OrgChart from 'react-orgchart';  
  14.   
  15. export default class OrgChartViewer extends React.Component<IOrgChartViewerProps, IOrgChartViewerState> {  
  16.   private dataCenterServiceInstance: IDataService;  
  17.   
  18.   constructor(props: IOrgChartViewerProps, state: IOrgChartViewerState) {  
  19.     super(props);  
  20.   
  21.     this.state = {  
  22.       orgChartItems: []  
  23.     };  
  24.   
  25.     let serviceScope: ServiceScope = this.props.serviceScope;  
  26.   
  27.     switch (Environment.type) {               
  28.       case EnvironmentType.SharePoint:                
  29.       case EnvironmentType.ClassicSharePoint:   
  30.         // Based on the type of environment, return the correct instance of the IDataCenterService interface  
  31.         // Mapping to be used when webpart runs in SharePoint.  
  32.         this.dataCenterServiceInstance = serviceScope.consume(OrgChartService.serviceKey);     
  33.   
  34.         this.dataCenterServiceInstance.getOrgChartInfo(this.props.listName).then((orgChartItems: any) => {  
  35.           this.setState({  
  36.             orgChartItems: orgChartItems  
  37.           });  
  38.         });  
  39.   
  40.         break;  
  41.       // case EnvironmentType.Local:                  
  42.       // case EnvironmentType.Test:               
  43.       default:        
  44.         // Webpart is running in the local workbench or from a unit test.              
  45.         this.dataCenterServiceInstance = serviceScope.consume(MockDataService.serviceKey);  
  46.   
  47.         this.dataCenterServiceInstance.getOrgChartInfo().then((orgChartItems: any) => {  
  48.           this.setState({  
  49.             orgChartItems: orgChartItems  
  50.           });  
  51.         });  
  52.     }  
  53.   }  
  54.   
  55.   public render(): React.ReactElement<IOrgChartViewerProps> {  
  56.     return (  
  57.       <div className={ styles.orgChartViewer }>  
  58.         <div className={ styles.container }>  
  59.           <div className={ styles.row }>  
  60.             <div className={ styles.column }>  
  61.   
  62.               <OrgChart tree={this.state.orgChartItems} NodeComponent={this.MyNodeComponent} />  
  63.   
  64.             </div>  
  65.           </div>  
  66.         </div>  
  67.       </div>  
  68.     );  
  69.   }  
  70.   
  71.   private MyNodeComponent = ({ node }) => {  
  72.     if (node.url) {  
  73.       return (  
  74.         <div className="initechNode">  
  75.           <a href={ node.url.Url } className={styles.link} >{ node.title }</a>          
  76.         </div>  
  77.       );      
  78.     }  
  79.     else {  
  80.       return (  
  81.         <div className="initechNode">{ node.title }</div>  
  82.       );      
  83.     }      
  84.   }    
  85. }  
Test the WebPart
  1. On the command prompt, type “gulp serve”.
  2. Open SharePoint site.
  3. Navigate to /_layouts/15/workbench.aspx.
  4. Add the webpart to page.
  5. Edit the web part and add the list name (i.e. OrgChart) to web part property.

    SharePoint Framework - Implementing Separation of Concerns (SoC)

  6. The web part should display the data from a SharePoint list in an organization chart.

    SharePoint Framework - Implementing Separation of Concerns (SoC)

  7. Click on the nodes with URL to see test the page navigation.
  8. Open Local SharePoint workbench (https://localhost:4321/temp/workbench.html)
  9. Add the web part to the page.

    SharePoint Framework - Implementing Separation of Concerns (SoC) 
Summary
 
With the implementation of Separation of concerns (SoC), all sections are separated. The code is easier to maintain and upgrade without touching other sections. ServiceScope helps to build SoC in SharePoint Framework solutions.