U2U Blog

for developers and other creative minds

TextBox

SharePoint Framework (SPFx): Using the new Microsoft Graph client from an application extension

Using the Microsoft Graph client from an Application Customizer Extension

In this post I will show you how to use the Microsoft Graph client from within an application extension. We will show the latest message from group conversations within an Office 365 group inside the header placeholder of it's SharePoint site. All these features are currently under preview. Documentation is available here: Release Notes Extensions Dev Preview Drop 1.

Start with a new project, using the Yeoman generator for SharePoint. Run the following from your command line tool:

cd c:\
md demo_graphextension
cd .\demo_graphextension\
yo @microsoft/sharepoint

Provide the wizard with the following details:

     _-----_
    |       |    .--------------------------.
    |--(o)--|    |      Welcome to the      |
   `---------´   |  SharePoint Client-side  |
    ( _´U`_ )    |    Solution Generator    |
    /___A___\    '--------------------------'
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

Let's create a new SharePoint solution.
? What is your solution name? demo-graphextension
? Which type of client-side component to create? Extension (Preview)
? Which type of client-side extension to create? Application Customizer (Preview)
? What is your Application Customizer (Preview) name? GroupConvNotifier
? What is your Application Customizer (Preview) description? ""

Let's first verify the extension actually works. Preview you know ;).

First run this command to make the debug files available on localhost:

gulp serve --nobrowser

To debug/test the extension we will have to use the modern UI in an actual SharePoint site. We will need the generated extension ID, so grab it from GroupConvNotifierApplicationCustomizer.manifest.json. Open one of your SharePoint Online sites and add the following to the url, replacing the id with the one you just copied:

?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"6136b9d4-dd9e-45c2-81cc-f4ec789bb53c":{"location":"ClientSideExtension.ApplicationCustomizer"}}

Confirm the load of the debug scripts and verify you get the following result: 



Let's now start with the code for our extension.

Getting the data

We will start with a service that will allow us to grab the latest conversation data from the group we are currently in. Add a folder services to the src folder. In there, add a file MSGraphService.ts and add a class.

export default class MSGraphService {

}

The first thing we will need is the current group id. For this we will need the context from the application customizer. We will pass the context into the constructor. The type ApplicationCustomizerContext you can find if you look at the BaseApplicationCustomizer class your Extension inherits from.

import the BaseApplicationCustomizer type at the top of the MSGraphService.ts file

import { ApplicationCustomizerContext } from "@microsoft/sp-application-base";

Inside the class, create a private field for the group id and set it from the constructor.

private _groupId = null;

constructor(public context:ApplicationCustomizerContext){
    this._groupId = this.context.pageContext.legacyPageContext.groupId;
}

Now that we have the context and the group id, we can get some actual Graph data. To the context object, Microsoft has now added the graphHttpClient object, which we can use to query for group data and reports. This will be extended in the future. Before, we had to use nasty solutions with popup's and/or iframes to get Microsoft Graph data!!

First, let's create a data structure that will be able to store the objects comming from Graph. Add a new folder interfaces to the src folder. In the interfaces folder, create a file IThread.ts. We will add two interfaces: one for the thread object and one for it's posts.

export interface IThread{
    id:string;
    topic:string;
    lastUpdate:Date;
    posts:IPost[];
}

export interface IPost{
    id: string;
    from: string;
    content: string;
}

import the interfaces inside the MSGraphService.ts file

import { IThread } from "../interfaces/IThread";

If you want to verify the data structure first, go to the Microsoft Graph Explorer and test the following queries to get to the query we need:

https://graph.microsoft.com/v1.0/groups/
https://graph.microsoft.com/v1.0/groups('{groupid}')
https://graph.microsoft.com/v1.0/groups('{groupid}')/threads
https://graph.microsoft.com/v1.0/groups('{groupid}')/threads?$select=id,topic,lastDeliveredDateTime&$top=1
https://graph.microsoft.com/v1.0/groups('{groupid}')/threads?$select=id,topic,lastDeliveredDateTime&$top=1&$expand=posts($select=from,body,receivedDateTime)

Notice that it is not possible to only take the latest post in one query, because sort and top is not supported in expand just yet. To avoid two REST calls, I'm grabbing the data in one go and will get the last post from code.

Import the necessary types for the graph client first:

import { GraphHttpClient, GraphClientResponse } from "@microsoft/sp-http";

Implement a method getLatestThreadPost that will get the group's latest conversation thread and it's post data based on our Graph URI.

public getLatestThreadPost():Promise<IThread>{        
    return this.context.graphHttpClient
        .get(`v1.0/groups/${this._groupId}/threads?$select=id,topic,lastDeliveredDateTime&$top=1&$expand=posts($select=from,body,receivedDateTime)`, GraphHttpClient.configurations.v1)
        .then((response:GraphClientResponse) => response.json())
        .then((jsonData) => {
            let tData = jsonData.value[0];
            console.log("Got conversation info");
            console.log(tData);
            return {
                id:tData.id,
                topic:tData.topic,
                lastUpdate:tData.lastDeliveredDateTime,
                posts: tData.posts.map((post) => {
                    return {
                        id : post.id,
                        from : post.from.emailAddress.name,
                        content : post.body.content
                    };
                })
            };
        })
        .catch((error) => {
            console.log("something went wrong");
            console.log(error);
            return null;
        });
}

That's it for the service! Notice that we do not need to generate any access tokens. The authorization is done by the graphHttpClient in the background, so great for us lazy developers! :) For the sake of the demo, we are assuming we have at least one thread and one post, of course we could add some validation here to check this first.

Rendering the data

Now that we have our service ready, let's show the data in the header placeholder of our Office365 Group SharePoint site.

We would like to get the latest data before we actually render the extension. For this we can override the OnInit method of our extension base class. Notice that the OnInit is already overridden in the generated code, so let's modify it. Open GroupConvNotifierApplicationCustomizer.ts.

First, import the necessary types:

import MSGraphService from "../../services/MSGraphService";
import { IThread, IPost } from "../../interfaces/IThread";

Now add two private fields to store our service and the latest data:

private _graphService: MSGraphService;
private _latestThreadData: IThread;

Next, override the OnInit method with the following code:

@override
public onInit(): Promise<void> {
  this._graphService = new MSGraphService(this.context);
  return new Promise<void>((resolve, reject) => {
    this._graphService.getLatestThreadPost().then((postData) => {
      this._latestThreadData = postData;
      resolve();
    });
  });
}

Notice this method will wait for our promise to resolve. The extension will wait for the OnInit method before rendering, ensuring our data is loaded. If you want to test the data load, add the following line to the onRender method:

console.log(this._latestThreadData);

Test this by attaching the same query string to the url of your Office365 Group Site. The result, if you open up your console, should look like this: 


To render the data, we will write a method renderHeader inside the GroupConvNotifierApplicationCustomizer class. Add the PlaceHolder type as an import:

import {
  BaseApplicationCustomizer,
  Placeholder
} from '@microsoft/sp-application-base';

First, let's add a private field for the Header placeholder to the class:

private _headerPlaceholder: Placeholder;

Change the onRender method to the following:

@override
public onRender(): void {
  console.log(this._latestThreadData);
  this.renderHeader();
}

Implement the renderHeader method as follows:

private renderHeader(): void {
  console.log('Rendering header!');
  // Handling the header placeholder
  if (!this._headerPlaceholder) {
    this._headerPlaceholder = this.context.placeholders.tryAttach(
      'PageHeader',
      {
        onDispose: this._onDispose
      }
    );

    // The extension should not assume that the expected placeholder is available.
    if (!this._headerPlaceholder) {
      console.error('The expected placeholder (PageHeader) was not found.');
      return;
    }

    if (this._latestThreadData) {
      let lastPost: IPost = this._latestThreadData.posts[this._latestThreadData.posts.length - 1];

      if (this._headerPlaceholder.domElement) {
        this._headerPlaceholder.domElement.innerHTML = `
            <div class="${styles.app}">
              <div class="ms-bgColor-themeTertiary ms-fontColor-white ${styles.header}">
                <i class="ms-Icon ms-Icon--Info"></i>
                &nbsp;${this._latestThreadData.topic}
                &nbsp;<i class="ms-Icon ms-Icon--Contact"></i>
                &nbsp;${lastPost.from}
                &nbsp;<i class="ms-Icon ms-Icon--Message"></i>
                &nbsp;${this.parseContent(lastPost.content)}
              </div>
            </div>`;
      }
    }
  }
}

The onDispose is required when attaching to a placeholder, so implement it:

private _onDispose(): void {
  console.log('Disposed header.');
}

Notice also that for the content of the post, which is typically HTML, we use a method parseContent. This method will strip the HTML tags out of the text and limit the maximum length of the content to 200. Implement it like this:

private parseContent(content:string):string{
    let regex = /(<([^>]+)>)/ig;
    content = content.replace(regex, "");
    if(content.length > 200) content = content.slice(0,200);
    return content;
}

As a final step, we will have to implement the styling used in our extension!
Most of the classes used are Office-UI-Fabric css classes, but some are custom. For these, create a new file GroupConvNotifier.module.scss inside the groupConvNotifier folder and add the following content:

.app {
  .header {
    height:40px; 
    text-align:center; 
    line-height:2.5; 
    font-weight:bold;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

import the styles at the top of the file:

import styles from './GroupConvNotifier.module.scss';


That's it! Now test again and check out that magnificent chat message inside the header!


Announcing the SharePoint Add-in “Export to Word”

Today, we’re glad to announce the FREE SharePoint Add-in “Export to Word”, developed by the team at U2U. This add-in fixes one of those issues that you’ll probably will have come across with in SharePoint, namely generating Word documents based on SharePoint list items. A bit like a mail merge for list items!

This functionality has never been out of the box available for regular SharePoint list items. You could achieve something like it on document libraries only, using Quick Parts in Word. But however, that only works for document libraries.

Now, what does the add-in allow you to do? It allows you to configure, per SharePoint list, one or more Word templates. You can even upload your own templates! The add-in then allows you to link fields from your selected list to the content of the template. Once your template is completely designed, you can just go to the SharePoint list and export any item to Word using your template. You can even export multiple items in bulk!

CreateNewTemplate_ori GeneratedDocuments_ori GeneratedDocuments2_ori

Do you want to know more about this add-in? Just go to the official site.
Do you want to install it from the Office Store? You can find the add-in here.

For a quick start guide on how to work with the add-in, have a look at the following video:

 

Do note that this is the first version of the Add-in, and we want your feedback to further extend and improve the add-in. You can contact us via the regular ways.

Consuming SharePoint CSOM from an Office 365 app

I’ve been a C# developer for more than five years now. When SharePoint 2013 was released, I started doing development for SharePoint 2013 and later also SharePoint Online. My focus was on developing web services (in combination with the SharePoint App model), native client and mobile applications using the Client Side Object Model.

When I was at the Barcelona TechEd conference in 2014, I decided to follow quite some breakout sessions about Office 365 and the “new” Office 365 APIs. It was around that time also that they released the new Office 365 Apps look and feel by means of the new “App Launcher” and the “My Apps” page.

clip_image002

Without having the knowledge about Apps for Office 365, I initially thought that these would be quite comparable to Apps for SharePoint. However, Apps for Office 365 are completely different.

Apps for Office 365

An app for Office 365 is conceptually:

  • An application that is running externally (i.e. on some website) or standalone (i.e. mobile or desktop). The application can be accessible from within the office 365 website, but also could be surfaced from within Word or Outlook.
  • Integrating somehow with Office 365 (this is not required).
  • Registered in the Azure Active Directory (AAD) that is running behind your Office 365 tenant.
  • Can use the same authentication mechanism as Office 365, resulting in single sign-on.

So before you can use your app in Office 365, you have to register it in Azure Active Directory. When registering you app in Azure AD, you also configure the permissions it gets to access the Office 365 services like mail, contacts, events and OneDrive for Business, but also SharePoint! Then I started thinking: “Does this mean that you can register an app in Azure AD and let it access SharePoint online?”.

clip_image004

Getting the access token

I was eager to discover whether it was possible to access SharePoint using the Client Side Object Model and the Azure Authentication mechanism. Jeremy Thake briefly mentioned in a blog post that it is possible to use the token you get from Azure as the Bearer token in you ClientContext requests.
So I created an ASP.NET MVC5 web application, registered it in Azure AD, set the permissions for Office 365 SharePoint Online (see above) and started experimenting.

You can get the access token as follows:

/// <summary>
/// Get the access token for the required resource
/// </summary>
/// <param name="clientID">The client ID of your app</param>
/// <param name="appKey">The app key</param>
/// <param name="resource">The resource you would like access to</param>
/// <returns></returns>
public static async Task<string> GetAccessToken(String clientID, String appKey, String resource)
{
    // Redeem the authorization code from the response for an access token and refresh token.
    ClaimsPrincipal principal = ClaimsPrincipal.Current;

    String authorizationUri = "https://login.windows.net";
    String nameIdentifier = principal.FindFirst(ClaimTypes.NameIdentifier).Value;
    String tenantId = principal.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;

    AuthenticationContext authContext = new AuthenticationContext(
        string.Format("{0}/{1}", authorizationUri, tenantId),
        new ADALTokenCache(nameIdentifier)
    );

    try
    {
        // Get the object identifier
        String objectId = principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

        // Authenticate
        AuthenticationResult result = await authContext.AcquireTokenSilentAsync(
            resource,
            new ClientCredential(clientID, appKey),
            new UserIdentifier(objectId, UserIdentifierType.UniqueId)
        );

        return result.AccessToken;
    }
    catch (AdalException exception)
    {
        //handle token acquisition failure
        if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
        {
            authContext.TokenCache.Clear();
        }
    }

    return null;
}

You obtain the values for the ClientID and AppKey from the application page in Azure AD. You typically store the values for these properties in the configuration file of your application.

clip_image006

Now what is the Resource you have to supply? Well the resource is as follows:

So in order to access a SharePoint, you can get the access token as follows:

String clientID = ConfigurationManager.AppSettings["ida:ClientId"] ?? ConfigurationManager.AppSettings["ida:ClientID"];
String appKey = ConfigurationManager.AppSettings["ida:AppKey"] ?? ConfigurationManager.AppSettings["ida:Password"];
String spSiteUrl = "https://tenant.sharepoint.com";

String accessToken = await GetAccessToken(clientID, appKey, spSiteUrl)

Accessing a SharePoint Online Site

In order to be able to use this token for your ClientContext, you need to set the Authorization Header on each webrequest the clientcontext sends out:

ClientContext clientContext = new ClientContext(spSiteUrl);
clientContext.ExecutingWebRequest +=
    (sender, e) =>
    {
        e.WebRequestExecutor.WebRequest.Headers["Authorization"] = "Bearer " + accessToken;
    };

You can now access this site through the clientcontext you created (taking into account the permissions you’ve set in Azure AD). In case you do not have the permissions to access something through the clientcontext, you’ll typically get a UnauthorizedaccessException.

Research Tracker application

Jeremy Thake often refers to a cool Office 365 project that combines different application types to access the same data in your SharePoint site. You can find these projects on GitHub.
One of these projects uses the SharePoint REST API to access a SharePoint site, create lists, manage list items. I cloned this project and extended it to also include the functionality by means of CSOM. You can also find this project on GitHub.

Exposing, Finding and Analyzing Dynamics CRM 4.0 Data with SharePoint 2007

Last Tuesday I hosted the CRM Technical Evening Session @ Microsoft Belgium. I spoke about (some of) the integration possibilities between SharePoint 2007 and Dynamics CRM 4.0.

Given the limited amount of time, the session’s scope was limited to:

  • a one way integration only: Getting our CRM data in SharePoint so we could expose, find & analyze it. Also, since the session was targeted at CRM partners, it didn’t cover any CRM customizations (like SiteMap, ISV.config or IFrames) in order to show the results back in Dynamics CRM
  • “Power”-User skills only: it wasn’t allowed to write a single line of .NET code.

The evening was packed with demos build around one simple scenario: We have a bunch of customer satisfaction survey results in Dynamics CRM: “the data”. By the end of the session, we wanted to have “information”: Are our customers satisfied? On what should we focus to improve our overall satisfaction? Should we improve friendliness or is there another aspect that influences the overall satisfaction?
In order to answer these questions we really went through the entire Microsoft stack of products: Dynamics CRM, SharePoint 2007, Office 2007, SQL Server Reporting Services 2008 (& the very handy new Report Builder 2.0) & SQL Server Analysis Services 2008.

Download the slides here.

SharePoint List Web Part for CRM 4.0 Released

I am so happy to announce that the List Web Part (LWP) for Dynamics CRM 4.0 is released to the public. You can download it here.

A few weeks ago I had the opportunity to participate in the early preview for the LWP and I admit: I was excited and very happy with the result. While testing the LWP my expectations were to have an alternative to the MOSS Business Data Catalog. And although it has not all the same possibilities (which is normal since it is just one web part), I feel that the LWP really lived up to my expectations. On top of that, it is very flexible, so you can adapt it to your own needs (select CRM view, select the CRM or SharePoint style, create connections to other web parts, …).

The big advantage is that no developer or BDC Application Definition file is required. All you need is the URL! So everyone can use it. Power Users/End users can configure them. To loan some words from another Microsoft campaign: Everyone Gets it! ;)

So, what are you waiting for? Download it and try it out!

I can’t wait to see if any of my feedback has made it into the final release. :)

TextBox