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:
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:
Node.js: LTS version is recommended
Visual Studio Code (VS Code): It’s recommended to use an editor like Visual Studio Code for a better coding experience.
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.
Open visual studio code, and from the terminal navigate to the folder where you want to create the PCF control project.
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
--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
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:
node_modules contains all the node packages required for the project.
ReactDatasetControl project folder contains main files.
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.
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}/>
}
Lines 1–3 brings all the necessary imports for the control.
Next line defines a function called
PCFControl
. Theexport
JavaScript keyword makes this function accessible outside of this file.In the
PCFControl
function asampleDataSet
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’sitems
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);
}
// ...
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
Now you can see your records in a tabular list format,
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;
}
// ...
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,
- https://react.dev/learn/reacting-to-input-with-state
- https://react.dev/learn/synchronizing-with-effects
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}/>
// ...
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}/>
}
Now build your project, and you should see something like this,
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.
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:
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.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;
}
// ...
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 theparseDatasetToJSON
function and each note record will contain two main propertiesfilename
anddocumentbody
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>
);
// ...
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>
);
}
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"
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,
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
// ...
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>
);
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.
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.
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;
}
// ...
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
// ...
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>
);
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
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>
);
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>
);
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>
);
}
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,
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
}
}
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>
);
Build your project and you can see your styled PCF control in action,
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);
}
// ...
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
- Create a connection with your power apps environment,
pac auth create --environment "{Your Environment Name}"
- Deploy the control to your environment,
pac pcf push
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.
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.
- 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.
- Then go to
Get More Components
and search for theReactDatasetControl
that we just created and add it to the list. - Add that control to the grid and Save & Publish the form.
- 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.
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:
- Add error handling logic to eliminate the need of view filters.
- Add sort, filter and search feature in the grid DetailsList control.
- Add support to read other types of files like txt, pdf, docx etc.
- 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)