DEV Community

Cover image for How to build PCF dataset control using REACT
Rohan Goel
Rohan Goel

Posted on • Originally published at Medium

How to build PCF dataset control using REACT

In this tutorial, you’ll build an interactive PCF dataset control using React that can be integrated into the entity grid of Dynamics CRM.

The techniques you’ll learn in the tutorial will give you a deeper understanding of PCF controls in Dynamics CRM. Throughout this guide, we will cover everything from setting up your development environment to deploying and testing your custom control.

What are you building?

In this tutorial, you’ll build a control that will display the excel file attached to the notes within the CRM, offering users a more intuitive and organized way to access their excel attachments linked to note records.

You can see what it will look like when you’re finished here:

Image description

In the Timeline tab, there are three notes linked to the account, each with an attached Excel file.

In the Notes tab, our custom control is added that extract these excel files and lists them in the Select File dropdown, and upon selecting the file, the sheets of that excel file will be displayed in the Select Sheet dropdown.

When user selects a sheet, its data will be displayed in the grid as shown above, and as you noticed we’ll create a custom Download functionality as well, that will download the selected file in our system.

Setup for the tutorial

Before you start building PCF controls with React, ensure you have the necessary tools installed on your machine:

  1. Node.js: LTS version is recommended

  2. Visual Studio Code (VS Code): It’s recommended to use an editor like Visual Studio Code for a better coding experience.

  3. Microsoft Power Platform CLI: Use either Power Platform Tools for VS Code or Power Platform CLI for Windows to install Power Platform CLI.

Creating a new project

Once your environment is ready, create a new PCF dataset control project.

  1. Open visual studio code, and from the terminal navigate to the folder where you want to create the PCF control project.

  2. Run the following command from your terminal to create a new PCF control project:

pac pcf init --namespace SampleNamespace --name ReactDatasetControl --template dataset --framework react --run-npm-install
Enter fullscreen mode Exit fullscreen mode
  • --namespace specifies the namespace for your control.

  • --name specifies the name of your control.

  • --template specifies the type of control (e.g., field or dataset)

  • --framework (optional) specifies the framework for the control.

  • --run-npm-install installs the required node modules for the control.

Running the above pac pcf init command sets up a basic PCF control with all the required files and dependencies, making it ready for you to customize and deploy to PowerApps.

In this tutorial we’ll be using the XLSX library to work with excel files. Install them in your project using,

npm install xlsx
npm install --save-dev ajv
Enter fullscreen mode Exit fullscreen mode

Overview

Now that you’re set up, let’s get an overview of PCF Dataset Control!

Inspecting the starter code

In the explorer you’ll see three main sections:

Image description

  1. node_modules contains all the node packages required for the project.

  2. ReactDatasetControl project folder contains main files.

  3. eslintrc.json ,package.json are the configuration files of the project.

Let’s have a look at some of the key files.

ControlManifest.Input.xml

It’s where you define the configuration and properties of your PCF control. It includes information such as the control’s name, version, description, and the data types it will accept.

index.ts

It serves as the entry point for your PCF control’s business logic. It is where you define the behavior and interactions of your control.

Image description

Lifecycle Methods:

  • init: This method is called when the control is initialized. It is used to set up the control, including event handlers, and to render the initial UI.

  • updateView: This method is called whenever the control needs to be updated, for example, when the control's data or properties change. It is used to re-render the UI with the latest data.

  • getOutputs: This method returns the current value of the control's outputs, which can be used by other components or stored in the database.

  • destroy: This method is called when the control is removed from the DOM. It is used to clean up resources, such as event handlers, to prevent memory leaks.

In our control, the updateView method is called when the page loads. This method renders the HelloWorld component from the HelloWorld.tsx file.

Building the component

Let’s start by creating a App.tsx file inside the ReactDatasetControl project folder. This will consist the logic of our PCF control.

/* App.tsx */

import * as React from 'react';
import { IInputs } from "./generated/ManifestTypes";
import { DetailsList } from '@fluentui/react';

export function PCFControl({sampleDataSet} : IInputs) {

    const records = [
        {
            "First Name": "Saturo",
            "Last Name": "Gojo",
            "Domain": "Infinity Void"
        },
        {
            "First Name": "Sukuna",
            "Last Name": "Ryomen",
            "Domain": "Malevolent Shrine"
        }
    ];

    return <DetailsList items={records}/>
}
Enter fullscreen mode Exit fullscreen mode
  • Lines 1–3 brings all the necessary imports for the control.

  • Next line defines a function called PCFControl . The export JavaScript keyword makes this function accessible outside of this file.

  • In the PCFControl function a sampleDataSet property is passed that will consist the grid data/records to which our control will be attached.

  • The function returns a DetailsList control with the records data passed to it’s items property.

  • DetailsList is a Fluent UI react control created by Microsoft that is used to view data in List format.

index.ts

Open the file labeled as index.ts , import our PCFControl at the top of the file and let’s modify the updateView function to return our custom control.

import { PCFControl } from "./App"

// ...

public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement 
{
    const props: IInputs = { sampleDataSet: context.parameters.sampleDataSet };
    return React.createElement(PCFControl, props);
}

// ...
Enter fullscreen mode Exit fullscreen mode

context.parameters.sampleDataSet accesses the dataset property from the context parameter. It contains the grid data/records that our control will be bound to in later sections. Then we are returning a React element consisting of PCFControl component and passing dataset props as the properties to it.

Now that we have created a basic structure for our control, let’s build and execute our control to see how’s its looking. To view the control in your local browser execute the following commands in the terminal,

npm run build
npm start watch
Enter fullscreen mode Exit fullscreen mode

Now you can see your records in a tabular list format,

Image description

Using data through props

As a next step, we want to display the CRM Grid Data that we receive from the sampleDataSet property in the control instead of using the static data records.

Open App.tsx , and let’s first create a function called parseDatasetToJSON inside PCFControl function, that will parse the sampleDataSet property (consisting of grid data) and returns that data in an array of json objects.

// ...

function parseDatasetToJSON() 
{
    const jsonData = [];
    for(const recordID of sampleDataSet.sortedRecordIds) 
    {
        // Dataset record
        const record = sampleDataSet.records[recordID];
        const temp: Record<string, any> = {};

        // Loop through each dataset columns
        for(const column of sampleDataSet.columns) 
        {
            temp[column.name] = record.getFormattedValue(column.name)
        }

        jsonData.push(temp);
    }
    return jsonData;
}

// ...
Enter fullscreen mode Exit fullscreen mode

Now that we got a function to convert dataset to JSON, we want to call this function as soon as our sampleDataSet property is loaded or changed.

React provides a special functions called Hooks, like useState that let you store your data in a state type property variable and useEffect lets you call the functions on any of the state type property change.

Learn more about useState and useEffect from,

Import useState and useEffect at the top of the file, then initialize a state variable called notes that will store our JSON formatted dataset records.

When the sampleDataSet property is loaded/changed, useEffect function is called and the notes variable is updated with the new dataset records.

Then we are updating the DetailsList items property to use notes data instead of static data records.

import { useState, useEffect } from 'react';

// ...

const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON Objects

useEffect(() => {

    const notesRecords = parseDatasetToJSON();
    setNotes(notesRecords);

}, [sampleDataSet]); // On dataset property load or change

return <DetailsList items={notes}/>

// ...
Enter fullscreen mode Exit fullscreen mode

At this point your App.tsx code should look something like this:

import * as React from 'react';
import { useState, useEffect } from 'react';
import { IInputs } from "./generated/ManifestTypes";
import { DetailsList } from '@fluentui/react';

export function PCFControl({sampleDataSet} : IInputs) {

    function parseDatasetToJSON() 
    {
        const jsonData = [];
        for(const recordID of sampleDataSet.sortedRecordIds) 
        {
            // Dataset record
            const record = sampleDataSet.records[recordID];
            const temp: Record<string, any> = {};

            // Loop through each dataset columns
            for(const column of sampleDataSet.columns) 
            {
                temp[column.name] = record.getFormattedValue(column.name)
            }
            jsonData.push(temp);
        }
        return jsonData;
    }

    const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON Objects

    useEffect(() => {

        const notesRecords = parseDatasetToJSON();
        setNotes(notesRecords);

    }, [sampleDataSet]); // On dataset property load or change

    return <DetailsList items={notes}/>

}
Enter fullscreen mode Exit fullscreen mode

Now build your project, and you should see something like this,

Image description

On load, PCF Dataset control provide this dummy dataset. To provide our custom dataset, from the bottom right highlighted section, select any excel file that will contain a list of columns and rows. On select PCF Dataset control maps this excel file to the sampleDataSet property.

Image description

Understanding CRM Entity: Notes

In this tutorial we are creating a PCF dataset control to be used on the OOB Notes entity of Dynamics CRM. Let’s take a brief moment to go through some key columns in the Notes entity that will be later used in our control.

The Notes entity in Dynamics CRM is used to store and manage attachments or comments associated with other entities, like accounts, contacts, or opportunities. It’s often used for keeping track of files or important notes related to records.

Here’s a brief overview of the key columns within the Notes entity:

  1. File Name (filename): This column stores the name of the file attached to the note. It’s a string field and will be used to populate file dropdown in our control.

  2. Document Body (documentbody): This column contains the actual content of the attached file encoded in Base64 format string. It’s where the file’s data is stored, making it a critical part of the attachment handling.

Now that we have a better understanding on various fields in note entity, let’s continue with creating our control.

Adding File Dropdown

We’ll now create a File dropdown, which will display the list of files attached with the note records.

Note: In CRM, an entity record can have multiple note records linked to it, where each note record can have only one file attached in it.

Let’s create a function called createFileOptions that will take notes records as a parameter and returns an array of options for the dropdown.

// ...

function createFileOptions(notes: Array<Record<string, any>>)
{
    const options: IDropdownOption[] = [];

    for(const [index, note] of notes.entries())
    {
        const option = { key: index, text: note["filename"] ?? "No File" };
        options.push(option);
    }
    return options;
}

// ...
Enter fullscreen mode Exit fullscreen mode

This function will generate an array of IDropdownOption objects, with each object containing a text property that holds the value of the filename field from the note records.

Remember: notes is a state variable that we created using the parseDatasetToJSON function and each note record will contain two main properties filename and documentbody

Next, using this function, we’ll store this options data in a fileOptions state type variable, and will pass this options into a Fluent UI DropDown control to be added above the DetailsList control in the return statement.

import { DetailsList, Dropdown, IDropdownOption, Stack } from '@fluentui/react';

// ...

const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects 

useEffect(() => {

    const notesRecords = parseDatasetToJSON();
    const fileOptionsRecords = createFileOptions(notesRecords);

    setNotes(notesRecords);
    setFileOptions(fileOptionsRecords);

}, [sampleDataSet]); // On dataset change

return (
    <Stack>
        <Dropdown placeholder="Select File" options={fileOptions}/>
        <DetailsList items={notes}/>
    </Stack>
);

// ...
Enter fullscreen mode Exit fullscreen mode

At this point your App.tsx code should look something like this:

import * as React from 'react';
import { useState, useEffect } from 'react';
import { IInputs } from "./generated/ManifestTypes";
import { DetailsList, Dropdown, IDropdownOption, Stack } from '@fluentui/react';

export function PCFControl({sampleDataSet} : IInputs) {

    function parseDatasetToJSON() 
    {
        const jsonData = [];
        for(const recordID of sampleDataSet.sortedRecordIds) 
        {
            // Dataset record
            const record = sampleDataSet.records[recordID];
            const temp: Record<string, any> = {};

            for(const column of sampleDataSet.columns) 
            {
                temp[column.name] = record.getFormattedValue(column.name)
            }
            jsonData.push(temp);
        }
        return jsonData;
    }

    function createFileOptions(notes: Array<Record<string, any>>)
    {
        const options: IDropdownOption[] = [];

        for(const [index, note] of notes.entries())
        {
            const option = { key: index, text: note["filename"] ?? "No File" }
            options.push(option);
        }
        return options;
    }

    const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
    const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects

    useEffect(() => {

        const notesRecords = parseDatasetToJSON();
        const fileOptionsRecords = createFileOptions(notesRecords);

        setNotes(notesRecords);
        setFileOptions(fileOptionsRecords);

    }, [sampleDataSet]); // On dataset change

    return (
        <Stack>
            <Dropdown placeholder="Select File" options={fileOptions}/>
            <DetailsList items={notes}/>
        </Stack>
    );
}
Enter fullscreen mode Exit fullscreen mode

Build your project and now you will a dropdown being added on the top of your list consisting filename values from your dataset file.

Got an error during build saying Unexpected any. Specify a different type @typescript-eslint/no-explicit-any ?

Well that because typescript is an strongly typed language and it expects us to define the variable types during compile time to prevent any run-time errors.

To fix this issue, go into your .eslintrc.json file and add this setting to the rules property, this will allow us to use variables of type any. Then build your project again.

"@typescript-eslint/no-explicit-any": "off"
Enter fullscreen mode Exit fullscreen mode

Now since we are using the filename field from the note records, ensure that you have added the filename column in your dataset. Remember your dataset file represents the note entity records in CRM.

For reference here’s the dataset.csv file we are using for this tutorial,

Image description

Image description

Ignore the styling of controls for now, we’ll add them in later sections.

Now when the user selects a file from the dropdown, we want to get the data of that file which as mentioned will be stored in the documentbody field of the note record.

To implement that, let’s create a function called handleSelectFile that will be called when the user selects a file in the dropdown.

On select of a file, this function will retrieve that file data from the documentbody field, and then will convert this data which is in Base64 string format into to an excelWorkbook object using the XLSX library.

Note: The excelWorkbook object will store the currently selected file data.

import * as XLSX from 'xlsx';

// ...

function handleSelectFile(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption)
{
    if(option === undefined) return; // Return if no option is selected

    const note = notes[option.key as number]; // Get note record using index
    const base64Data = note["documentbody"] ?? ""; // Get file data of that note record
    const workbook = XLSX.read(base64Data, { type: 'base64', cellDates: true }); // Converts base64 data to excel workbook object
    setExcelWorkbook(workbook);

    console.log(workbook);
}

const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
const [excelWorkbook, setExcelWorkbook] = useState<XLSX.WorkBook>(XLSX.utils.book_new()); // Excel workbook object

// ...
Enter fullscreen mode Exit fullscreen mode

Then add this function in the onChange property of the dropdown, that will call this function whenever a file is selected in the dropdown.

return (
    <Stack>
        <Dropdown placeholder="Select File" options={fileOptions} onChange={handleSelectFile}/>
        <DetailsList items={notes}/>
    </Stack>
);
Enter fullscreen mode Exit fullscreen mode

Before validating the updates, let’s first add a column called documentbody in our sample dataset file that will contain the base64 string values of an excel file. To convert an excel file to base64 string can use this online tool.

Note: The documentbody value represents the excel file that will be attached to our note records in CRM

Here’s the sample file that we used, in which cell F2 contains a base64 encoded string of an excel file.

Image description

Now when we build our project and select the file Sales.xlsx, that file documentbody value will be retrieved and will be converted to the excel workbook object and will be logged into the console.

Image description

Adding Sheet Dropdown

Till this point when have managed to get the excelWorkbook object in our code when the user selects a file in the dropdown. In the further steps, we now want to get all the sheets that are present inside this workbook and display them in a separate sheet dropdown.

So now we’ll create the sheet dropdown control that will display the list of sheets present inside our excelWorkbook object. When the user selects a sheet, we’ll convert this sheet rows/records into the json array format and display it in using our DetailsList control.

For that, let’s first create a function called createSheetOptions that will parse through the workbook object and creates the list of options containing sheet names, similar to what we did for the file dropdown options.

// ...

function createSheetOptions(workbook: XLSX.WorkBook)
{
    const options: IDropdownOption[] = [];
    for(const [index, sheetName] of workbook.SheetNames.entries())
    {
        const option = { key: index, text: sheetName };
        options.push(option);
    }
    return options;
}

// ...
Enter fullscreen mode Exit fullscreen mode

Next, using this function, we’ll store this sheet options in a sheetOptions variable, and will display these options using a Dropdown control.

Remember, we were calling the createFileOptions when the dataset is loaded or changed in our control from the useEffect function. Now when should we call the createSheetOptions ?

Correct! It should be called as soon as the user selects a file i.e. inside handleSelectFile function. By this whenever the user select a file, our sheetOptions will be updated based on the selected file.

Let’s update the handleSelectFile function to achieve the same,

// ...

function handleSelectFile(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption)
{
    if(option === undefined) return; // Return if no option is selected

    const note = notes[option.key as number]; // Get note record using index
    const base64Data = note["documentbody"] ?? ""; // Get file data
    const workbook = XLSX.read(base64Data, { type: 'base64', cellDates: true }); // Converts base64 data to excel workbook object
    setExcelWorkbook(workbook);

    const sheetOptionsRecords = createSheetOptions(workbook);
    setSheetOptions(sheetOptionsRecords);
}

const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
const [excelWorkbook, setExcelWorkbook] = useState<XLSX.WorkBook>(XLSX.utils.book_new()); // Excel workbook object
const [sheetOptions, setSheetOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects

// ...
Enter fullscreen mode Exit fullscreen mode

Now let’s add a new dropdown control in our return statement, that will display these sheetOptions to the user.

return (
    <Stack>
        <Stack horizontal>
            <Dropdown placeholder="Select File" options={fileOptions} onChange={handleSelectFile}/>
            <Dropdown placeholder="Select Sheet" options={sheetOptions} />
        </Stack>
        <DetailsList items={notes}/>
    </Stack>
);
Enter fullscreen mode Exit fullscreen mode

We got the sheets in our dropdown, for the next step, we want whenever the user selects the sheet in the dropdown that sheet rows/records should be displayed in the DetailsList control.

To achieve that, let’s create a function called handleSelectSheet that will be called whenever a user selects the sheet in the dropdown.

This function will get the currently selected sheet from the excelWorkbook object stored in our code, and will convert that sheet record/rows into a json array. Then we will store this json data in a state variable called rows which can be passed to our DetailsList control.

function handleSelectSheet(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption)
{
    if(option === undefined) return; // Return if no option is selected

    const sheet = excelWorkbook.Sheets[option.text as string]; // Get sheet record using SheetName
    const rowRecords: Record<string, any>[] = XLSX.utils.sheet_to_json(sheet, {raw: false}); // Sheet Records in JSON Array
    setRows(rowRecords);
}

const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
const [excelWorkbook, setExcelWorkbook] = useState<XLSX.WorkBook>(XLSX.utils.book_new()); // Excel workbook object
const [sheetOptions, setSheetOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
const [rows, setRows] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
Enter fullscreen mode Exit fullscreen mode

Now add this function in the onChange property of the sheet dropdown that will call this function when a sheet is selected, and update the items property of the DetailsList control to use the rows data that consist our selected sheet records.

return (
    <Stack>
        <Stack horizontal>
            <Dropdown placeholder="Select File" options={fileOptions} onChange={handleSelectFile}/>
            <Dropdown placeholder="Select Sheet" options={sheetOptions} onChange={handleSelectSheet} />
        </Stack>
        <DetailsList items={rows}/>
    </Stack>
);
Enter fullscreen mode Exit fullscreen mode

Adding Download Button

Before we go ahead and test our control, let’s first add the final part of our PCF control functionality i.e. the Download file functionality.

Whenever a user clicks on the download button, it will download the currently selected file (stored in excelWorkbook object) in the user’s system.

We can implement this by simply executing the XLSX.writeFile() method on the onClick of Button control. This method will take the workbook object as the first parameter and the name of the file to be downloaded as the second.

Let’s create a PrimaryButton control along with the other dropdown controls, and execute a function called handleDownload using the onClick property of the button control.

// ...

function handleDownload()
{
    XLSX.writeFile(excelWorkbook, 'download.xlsx');
}

// ...

return (
    <Stack>
        <Stack horizontal>
            <Dropdown placeholder="Select File" options={fileOptions} onChange={handleSelectFile}/>
            <Dropdown placeholder="Select Sheet" options={sheetOptions} onChange={handleSelectSheet} />
            <PrimaryButton text="Download" allowDisabledFocus onClick={handleDownload}/>
        </Stack>
        <DetailsList items={rows}/>
    </Stack>
);
Enter fullscreen mode Exit fullscreen mode

Now when you click on the download button, the selected file will be downloaded in your system.

At this point your App.tsx code should look something like this:

import * as React from 'react';
import { useState, useEffect } from 'react';
import { IInputs } from "./generated/ManifestTypes";
import { DetailsList, DetailsListLayoutMode, Dropdown, IDropdownOption, PrimaryButton, Stack } from '@fluentui/react';
import * as XLSX from 'xlsx';
import { text } from 'stream/consumers';

export function PCFControl({sampleDataSet} : IInputs) {

    function parseDatasetToJSON() 
    {
        const jsonData = [];
        for(const recordID of sampleDataSet.sortedRecordIds) 
        {
            // Dataset record
            const record = sampleDataSet.records[recordID];
            const temp: Record<string, any> = {};

            for(const column of sampleDataSet.columns) 
            {
                temp[column.name] = record.getFormattedValue(column.name)
            }
            jsonData.push(temp);
        }
        return jsonData;
    }

    function createFileOptions(notes: Array<Record<string, any>>)
    {
        const options: IDropdownOption[] = [];

        for(const [index, note] of notes.entries())
        {
            const option = { key: index, text: note["filename"] ?? "No File" };
            options.push(option);
        }
        return options;
    }

    function createSheetOptions(workbook: XLSX.WorkBook)
    {
        const options: IDropdownOption[] = [];
        for(const [index, sheetName] of workbook.SheetNames.entries())
        {
            const option = { key: index, text: sheetName };
            options.push(option);
        }
        return options;
    }

    function handleSelectFile(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption)
    {
        if(option === undefined) return; // Return if no option is selected

        const note = notes[option.key as number]; // Get note record using index
        const base64Data = note["documentbody"] ?? ""; // Get file data
        const workbook = XLSX.read(base64Data, { type: 'base64', cellDates: true }); // Converts base64 data to excel workbook object
        setExcelWorkbook(workbook);

        const sheetOptionsRecords = createSheetOptions(workbook);
        setSheetOptions(sheetOptionsRecords);
    }

    function handleSelectSheet(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption)
    {
        if(option === undefined) return; // Return if no option is selected

        const sheet = excelWorkbook.Sheets[option.text as string]; // Get sheet record using SheetName
        const rowRecords: Record<string, any>[] = XLSX.utils.sheet_to_json(sheet, {raw: false}); // Sheet Records in JSON Array
        setRows(rowRecords);
    }

    function handleDownload()
    {
        XLSX.writeFile(excelWorkbook, 'download.xlsx');
    }

    const [notes, setNotes] = useState<Array<Record<string, any>>>([]); // Array of JSON objects
    const [fileOptions, setFileOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
    const [excelWorkbook, setExcelWorkbook] = useState<XLSX.WorkBook>(XLSX.utils.book_new()); // Excel workbook object
    const [sheetOptions, setSheetOptions] = useState<IDropdownOption[]>([]); // Array of IDropdownOption objects
    const [rows, setRows] = useState<Array<Record<string, any>>>([]); // Array of JSON objects

    useEffect(() => {
        const notesRecords = parseDatasetToJSON();
        const fileOptionsRecords = createFileOptions(notesRecords);

        setNotes(notesRecords);
        setFileOptions(fileOptionsRecords);

    }, [sampleDataSet]); // On dataset change

    return (
        <Stack>
            <Stack horizontal>
                <Dropdown placeholder="Select File" options={fileOptions} onChange={handleSelectFile}/>
                <Dropdown placeholder="Select Sheet" options={sheetOptions} onChange={handleSelectSheet} />
                <PrimaryButton text="Download" allowDisabledFocus onClick={handleDownload}/>
            </Stack>
            <DetailsList items={rows}/>
        </Stack>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now when you build the control, in the files dropdown, you’ll see all the files listed by the filename column (it represents the name of the file that is attached to the note records in CRM).

When you select a file, in the code the documentbody value (it represent the actual file that is attached to the note record in CRM) of that file will be converted into an Excel workbook, and its sheets will be displayed in the sheets dropdown.

After selecting a sheet, the records from that sheet will be converted into an json array and will be displayed in the list.

Note: To convert an excel file to base64 string can use this online tool.

For reference this is the excel dataset file we used for this tutorial,

Image description

Image description

Add styling to controls

Great work coming this far in the tutorial…

Now that we have implemented the core functionality of this PCF control, let’s add some styling to our control for a better user experience.

Create a new file ControlStyles.tsx in your project folder and add the following style properties in that file.

/* ControlStyles.tsx */

import { IStackTokens, IStackStyles, IDetailsListStyles, IDropdownStyles } from "@fluentui/react";

export const stackTokens: IStackTokens = { 
    childrenGap: 10 
};

export const stackStyles: IStackStyles = {
    root: {
        padding: 10,
        width: '100%',
        marginBottom: 20,
    },
};

export const detailsListStyles: IDetailsListStyles = {
    root: {
        overflowX: 'auto'
    }, 
    contentWrapper: {
        overflowY: 'auto', 
        width: 'max-content', 
        height: 450
    },
    focusZone : {},
    headerWrapper: {} 
}

export const dropDownStyles : Partial<IDropdownStyles> = {
    root : {
        width: 'auto', 
        minWidth: 200
    }
}
Enter fullscreen mode Exit fullscreen mode

Then import these style properties in our main App.tsx file and add these properties in our Fluent UI controls.

// ...

import {stackTokens, stackStyles, detailsListStyles, dropDownStyles} from './ControlStyles'

// ...

return (
    <Stack tokens={stackTokens} styles={stackStyles}>
        <Stack horizontal tokens={stackTokens} styles={stackStyles}>
            <Dropdown 
                placeholder="Select File" 
                options={fileOptions} 
                onChange={handleSelectFile} 
                styles={dropDownStyles}
            />
            <Dropdown 
                placeholder="Select Sheet" 
                options={sheetOptions} 
                onChange={handleSelectSheet} 
                styles={dropDownStyles}
                defaultSelectedKey='0' 
            />
            <PrimaryButton text="Download" allowDisabledFocus onClick={handleDownload}/>
        </Stack>
        <DetailsList 
            items={rows} 
            styles={detailsListStyles}
            data-is-scrollable={false}
            layoutMode={DetailsListLayoutMode.fixedColumns}
        />
    </Stack>
);
Enter fullscreen mode Exit fullscreen mode

Build your project and you can see your styled PCF control in action,

Image description

Final wrap-up

When you attach your PCF control to a grid, the control subscribes to the data set provided by that grid. The fields and records that are configured to be displayed in the grid are passed to your PCF control dataset property, allowing it to render or process this data in a customized manner.

However, some particular columns like documentbody in note entity is not available to be directly added to the grid because it’s a binary field, and Dynamics CRM does not allow binary fields to be displayed in grid views due to performance and usability concerns.

So to access the documentbody and filename columns data, we can do so by programmatically adding those columns in our dataset. Let’s add these columns in our sampleDataSet property from the updateView method in index.ts file.

// ...

public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement 
{

    if (context.parameters.sampleDataSet.addColumn) 
    {
        context.parameters.sampleDataSet.addColumn("documentbody");
        context.parameters.sampleDataSet.addColumn("filename");
        context.parameters.sampleDataSet.refresh();
    }

    const props: IInputs = { sampleDataSet: context.parameters.sampleDataSet };
    return React.createElement(PCFControl, props);
}

// ...
Enter fullscreen mode Exit fullscreen mode

Deploying PCF Control

It’s time to see our PCF control in action from the Power Apps.

Execute the following commands in the terminal to deploy your PCF control in Power Apps environment,

  • Build the PCF control.
npm run build
Enter fullscreen mode Exit fullscreen mode
  • Create a connection with your power apps environment,
pac auth create --environment "{Your Environment Name}"
Enter fullscreen mode Exit fullscreen mode
  • Deploy the control to your environment,
pac pcf push
Enter fullscreen mode Exit fullscreen mode

After successful deployment, go to make.powerapps.com:

  • Select Your Environment: Choose the environment where you deployed the control.

  • Navigate to Solutions: Go to Solutions > Default Solution > Custom controls.

  • Check Your Control: Your PCF control should appear in the list.

Image description

Adding control on Grid

For this tutorial, we’ll attach our control on the notes grid in the account table form.

Note: You can add this control on any entity form which is enabled to have attachments linked with it.

  • Select the related table as Notes on the sub-grid.

Image description

  • Key filters you should add in your notes view, adding these filters in the view, will ensure to return only those note records to our dataset, that are having an excel file attached to them.

Image description

  • Then go to Get More Components and search for the ReactDatasetControl that we just created and add it to the list.
  • Add that control to the grid and Save & Publish the form.

Image description

  • Open any account record that is having notes with excel attachments linked to it, and you should be able to see your files in the control.

Image description

Wrapping up

Congratulations! You’ve created a custom PCF dataset control that:

  • Display notes excel attachments within model driven apps,

  • Lists each individual sheet within the excel file,

  • Downloads the file within the system.

  • Leverages Fluent UI React controls, which are built by Microsoft for the development of modern Dynamics applications.

Nice work! I hope you now feel like you have a decent grasp on React and PCF controls in dynamics.

If you want to further enhance your React and PCF controls skills, here are some ideas for improvements that you could make to this control:

  1. Add error handling logic to eliminate the need of view filters.
  2. Add sort, filter and search feature in the grid DetailsList control.
  3. Add support to read other types of files like txt, pdf, docx etc.
  4. Add CRUD features in the grid.

Feel free to share other cool features that can be added or your doubts regarding this tutorial in the comments section.

Here’s the GitHub repository for this project.

Happy Coding…

Top comments (0)