Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
In this tutorial, you'll create a canvas app dataset code component, deploy it, add it to a screen, and test the component using Visual Studio Code. The code component displays a paged, scrollable dataset grid that provides sortable and filterable columns. It also allows the highlighting of specific rows by configuring an indicator column. This is a common request from app makers and can be complex to implement using native canvas app components. Code components can be written to work on both canvas and model-driven apps. However, this component is written to specifically target use within canvas apps.
In addition to these requirements, you'll also ensure the code component follows best practice guidance:
Note
Before you start, make sure you've installed all the prerequisite components.
You can download the complete sample from PowerApps-Samples/component-framework/CanvasGridControl/.
Create a new folder to use for your code component. For example, C:\repos\CanvasGrid
.
Open Visual Studio Code and then File > Open Folder and select the CanvasGrid
folder. If you've added the Windows Explorer extensions during installation of Visual Studio Code, you can use the Open with Code context menu option inside the folder. You can also load any folder into Visual Studio Code using code .
at the command prompt when the current directory is set to that location.
Inside a new Visual Studio Code PowerShell terminal (Terminal > New Terminal), use the pac pcf init command to create a new code component project:
pac pcf init --namespace SampleNamespace --name CanvasGrid --template dataset
or using the short form:
pac pcf init -ns SampleNamespace -n CanvasGrid -t dataset
This adds a new pcfproj
and related files to the current folder, including a packages.json
that defines the modules needed. To install the required modules, use npm install:
npm install
Note
If you receive the message, The term 'npm' is not recognized as the name of a cmdlet, function, script file, or operable program.
, make sure you've installed all the prerequisites, specifically node.js (LTS version is recommended).
The template includes an index.ts
file along with various configuration files. This is the starting point of your code component and contains the lifecycle methods described in Component implementation.
You'll be using Microsoft Fluent UI and React for creating UI, so you must install these as dependencies. Use the following at the terminal:
npm install react react-dom @fluentui/react
This adds the modules to the packages.json
and installs them into the node_modules
folder. You won't commit node_modules
into source control since all the required modules can be restored using npm install
.
One of the advantages of Microsoft Fluent UI is that it provides a consistent and highly accessible UI.
The template used by pac pcf init installs the eslint
module to your project and configures it by adding an .eslintrc.json
file. Eslint
now requires configuring for TypeScript and React coding styles. More information: Linting - Best practices and guidance for code components.
The CanvasGrid\ControlManifest.Input.xml
file defines the metadata describing the behavior of the code component. The control attribute will already contain the namespace and name of the component.
Tip
You may find the XML easier to read by formatting it so that attributes appear on separate lines. Find and install an XML formatting tool of your choice in the Visual Studio Code Marketplace: Search for xml formatting extensions.
The examples below have been formatted with attributes on separate lines to make them easier to read.
You must define the records that the code component can be bound to, by adding the following inside the control
element, replacing the existing data-set
element:
The records data-set will be bound to a data source when the code component is added to a canvas app. The property-set indicates that the user must configure one of the columns of that dataset to be used as the row highlight indicator.
Tip
You can specify multiple dataset elements. This could be useful if you wanted to search one dataset but show a list of records using a second.
In addition to the dataset, you can provide the following input properties:
HighlightValue
- Allows the app maker to provide a value to be compared against the column defined as the HighlightIndicator
property-set
. When the values are equal, the row should be highlighted.HighlightColor
- Allows the app maker to select a color to use to highlight rows.Tip
When creating code components for use in canvas apps, it's recommended to provide input properties for the styling of common aspects of your code components.
In addition to the input properties, an output property named FilteredRecordCount
will be updated (and triggers the OnChange
event) when the rows count is changed because of a filter action applied inside the code component. This is helpful when you want to show a No Rows Found
message inside the parent app.
Note
In the future, code components will support custom events so that you can define a specific event rather than using the generic OnChange
event.
To define these three properties, add the following to the CanvasGrid\ControlManifest.Input.xml
file, below the data-set
element:
<property name="FilteredRecordCount"
display-name-key="FilteredRecordCount_Disp"
description-key="FilteredRecordCount_Desc"
of-type="Whole.None"
usage="output" />
<property name="HighlightValue"
display-name-key="HighlightValue_Disp"
description-key="HighlightValue_Desc"
of-type="SingleLine.Text"
usage="input"
required="true"/>
<property name="HighlightColor"
display-name-key="HighlightColor_Disp"
description-key="HighlightColor_Desc"
of-type="SingleLine.Text"
usage="input"
required="true"/>
Save this file and then, at the command-line, use:
npm run build
Note
If you get an error like this while running npm run build
:
[2:48:57 PM] [build] Running ESLint...
[2:48:57 PM] [build] Failed:
[pcf-1065] [Error] ESLint validation error:
C:\repos\CanvasGrid\CanvasGrid\index.ts
2:47 error 'PropertyHelper' is not defined no-undef
Open index.ts file and add this: // eslint-disable-next-line no-undef
, directly above the line:
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;
The run npm run build
again.
After the component is built, you'll see that:
An automatically generated file CanvasGrid\generated\ManifestTypes.d.ts
is added to your project. This is generated as part of the build process from the ControlManifest.Input.xml
and provides the types for interacting with the input/output properties.
The build output is added to the out
folder. The bundle.js
is the transpiled JavaScript that runs inside the browser, and the ControlManifest.xml
is a reformatted version of the ControlManifest.Input.xml
file that's used during deployment.
Note
Do not modify the contents of the generated
and out
folders directly. They'll be overwritten as part of the build process.
When the code component uses React, there must be a single root component that's rendered within the updateView method. Inside the CanvasGrid
folder, add a new TypeScript file named Grid.tsx
, and add the following content:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import {
ScrollablePane,
ScrollbarVisibility
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';
type DataSet = ComponentFramework.PropertyHelper.DataSetApi.EntityRecord & IObjectWithKey;
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
}
const onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
if (props && defaultRender) {
return (
<Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
{defaultRender({
...props,
})}
</Sticky>
);
}
return null;
};
const onRenderItemColumn = (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord,
index?: number,
column?: IColumn,
) => {
if (column && column.fieldName && item) {
return <>{item?.getFormattedValue(column.fieldName)}</>;
}
return <></>;
};
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);
const items: (DataSet | undefined)[] = React.useMemo(() => {
setIsLoading(false);
const sortedRecords: (DataSet | undefined)[] = sortedRecordIds.map((id) => {
const record = records[id];
return record;
});
return sortedRecords;
}, [records, sortedRecordIds, hasNextPage, setIsLoading]);
const gridColumns = React.useMemo(() => {
return columns
.filter((col) => !col.isHidden && col.order >= 0)
.sort((a, b) => a.order - b.order)
.map((col) => {
const sortOn = sorting && sorting.find((s) => s.name === col.name);
const filtered =
filtering &&
filtering.conditions &&
filtering.conditions.find((f) => f.attributeName == col.name);
return {
key: col.name,
name: col.displayName,
fieldName: col.name,
isSorted: sortOn != null,
isSortedDescending: sortOn?.sortDirection === 1,
isResizable: true,
isFiltered: filtered != null,
data: col,
} as IColumn;
});
}, [columns, sorting]);
const rootContainerStyle: React.CSSProperties = React.useMemo(() => {
return {
height: height,
width: width,
};
}, [width, height]);
return (
<Stack verticalFill grow style={rootContainerStyle}>
<Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
></DetailsList>
</ScrollablePane>
{(itemsLoading || isComponentLoading) && <Overlay />}
</Stack.Item>
</Stack>
);
});
Grid.displayName = 'Grid';
Note
The file has the extension tsx
which is a TypeScript file that supports XML style syntax used by React. It's compiled into standard JavaScript by the build process.
This section includes commons on the design of the Grid.tsx
component.
This is a React functional component, but equally, it could be a class component. This is based on your preferred coding style. Class components and functional components can also be mixed in the same project. Both function and class components use the tsx
XML style syntax used by React. More information: Function and Class Components
When importing the ChoiceGroup
Fluent UI components using path-based imports, instead of:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
Stack
} from "@fluentui/react";
This code uses:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Stack } from '@fluentui/react/lib/Stack';
This way, your bundle size will be smaller, resulting in lower capacity requirements and better runtime performance.
An alternative would be to use tree-shaking.
This code:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
Uses destructuring assignment. In this way, you extract the attributes required to render from the props, rather than prefixing them with props.
each time they're used.
The code also uses React.memo to wrap the functional component so that it won't render unless any of the input props have changed.
React.useMemo is used in several places to ensure that the item array created is only mutated when the input props options
or configuration
change. This is a best practice of function components that reduces unnecessary renders of the child components.
DetailsList
in a Stack
is wrapped because, later you'll add a footer element with the paging controls.Sticky
component is used to wrap the header columns (using onRenderDetailsHeader
) so that they remain visible when scrolling the grid.setKey
is passed to the DetailsList
along with initialFocusedIndex
so that when the current page changes, the scroll position and selection will be reset.onRenderItemColumn
is used to render the cell contents. It accepts row item and uses getFormattedValue to return the display value of the column. The getValue method returns a value that you could use to provide an alternative rendering. The advantage of getFormattedValue
is that it contains a formatted string for columns of non-string types such as dates and lookups.gridColumns
block is mapping the object shape of the columns provided by the dataset context, onto the shape expected by the DetailsList
columns prop. Since this is wrapped in the React.useMemo hook, the output will only change when the columns
or sorting
props change. You can display the sort and filter icons on the columns where the sorting and filtering details provided by the code component context matches the column being mapped. The columns are sorted using the column.order
property to ensure that they're in the correct order on the grid as defined by the app maker.isComponentLoading
in our React component. This is because when the user selects sorting and filtering actions, you can grey out the grid as a visual cue until the sortedRecordIds
are updated and the state is reset. There's an additional input prop called itemsLoading
which is mapped to the dataset.loading property provided by the dataset context. Both flags are used to control the visual loading cue that's implemented using the Fluent UI Overlay
component.The next step is to make changes to the index.ts
file to match properties defined in Grid.tsx.
To the header of index.ts
, replace the existing imports with the following:
import {IInputs, IOutputs} from './generated/ManifestTypes';
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;
type DataSet = ComponentFramework.PropertyTypes.DataSet;
Note
The import of initializeIcons
is required because this code uses the Fluent UI icon set. You call initializeIcons
to load the icons inside the test harness. Inside canvas apps, they're already initialized.
Add the following fields to the CanvasGrid
class:
export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
/**
* Empty constructor.
*/
constructor() {
}
Add the following to init
:
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement): void {
// Add control initialization code
}
The init
function is called when the code component is first initialized on an app screen. You store a reference to the following:
notifyOutputChanged
: This is the callback provided that you call to notify the canvas app that one of the properties has changed.container
:This is the DOM element to which you add your code component UI.resources
:This is used to retrieve localized strings in the current user's language.The context.mode.trackContainerResize(true)) is used so that updateView
will be called when the code component changes size.
Note
Currently, there's no way to determine if the code component is running inside the test harness. You need to detect if the control-dimensions
div
element is present as an indicator.
Add the following to updateView
:
public updateView(context: ComponentFramework.Context<IInputs>): void {
// Add code to update control view
}
You can see that:
init
function.Grid
component is defined inside Grid.tsx
and is imported at the top of the file.allocatedWidth
and allocatedHeight
will be provided by the parent context whenever they change (for example, the app resizes the code component or you enter full screen mode), since you made a call to trackContainerResize(true) inside the init
function.dataset
string.updatedProperties
array is not populated, so you can use the isTestHarness
flag you set in the init
function to short-circuit the logic that sets the sortedRecordId
and records
. You maintain a reference to the current values until they change, so that you don't mutate these when passed to the child component unless a re-render of the data is required.hasPreviousPage
is false.Lastly, you need to tidy up when the code component is destroyed:
Ensure all the files are saved and at the terminal use:
npm start watch
You need to set the width and height to see the code component grid that's populated using the sample three records. You can then export a set of records into a CSV file from Dataverse and then load it into the test harness using Data Inputs > Records panel:
Here is some comma separated sample data you can save to a .csv file and use:
address1_city,address1_country,address1_stateorprovince,address1_line1,address1_postalcode,telephone1,emailaddress1,firstname,fullname,jobtitle,lastname
Seattle,U.S.,WA,7842 Ygnacio Valley Road,12150,555-0112,someone_m@example.com,Thomas,Thomas Andersen (sample),Purchasing Manager,Andersen (sample)
Renton,U.S.,WA,7165 Brock Lane,61795,555-0109,someone_j@example.com,Jim,Jim Glynn (sample),Owner,Glynn (sample)
Snohomish,U.S.,WA,7230 Berrellesa Street,78800,555-0106,someone_g@example.com,Robert,Robert Lyon (sample),Owner,Lyon (sample)
Seattle,U.S.,WA,931 Corte De Luna,79465,555-0111,someone_l@example.com,Susan,Susan Burk (sample),Owner,Burk (sample)
Seattle,U.S.,WA,7765 Sunsine Drive,11910,555-0110,someone_k@example.com,Patrick,Patrick Sands (sample),Owner,Sands (sample)
Seattle,U.S.,WA,4948 West Th St,73683,555-0108,someone_i@example.com,Rene,Rene Valdes (sample),Purchasing Assistant,Valdes (sample)
Redmond,U.S.,WA,7723 Firestone Drive,32147,555-0107,someone_h@example.com,Paul,Paul Cannon (sample),Purchasing Assistant,Cannon (sample)
Issaquah,U.S.,WA,989 Caravelle Ct,33597,555-0105,someone_f@example.com,Scott,Scott Konersmann (sample),Purchasing Manager,Konersmann (sample)
Issaquah,U.S.,WA,7691 Benedict Ct.,57065,555-0104,someone_e@example.com,Sidney,Sidney Higa (sample),Owner,Higa (sample)
Monroe,U.S.,WA,3747 Likins Avenue,37925,555-0103,someone_d@example.com,Maria,Maria Campbell (sample),Purchasing Manager,Campbell (sample)
Duvall,U.S.,WA,5086 Nottingham Place,16982,555-0102,someone_c@example.com,Nancy,Nancy Anderson (sample),Purchasing Assistant,Anderson (sample)
Issaquah,U.S.,WA,5979 El Pueblo,23382,555-0101,someone_b@example.com,Susanna,Susanna Stubberod (sample),Purchasing Manager,Stubberod (sample)
Redmond,U.S.,WA,249 Alexander Pl.,86372,555-0100,someone_a@example.com,Yvonne,Yvonne McKay (sample),Purchasing Manager,McKay (sample)
Note
There's only a single column shown in the test harness regardless of the columns you provide in the loaded CSV file. This is because the test harness only shows property-set
when there is one defined. If no property-set
is defined, then all of the columns in the loaded CSV file will be populated.
Although the Fluent UI DetailsList
allows selecting records by default, the selected records are not linked to the output of the code component. You need the Selected
and SelectedItems
properties to reflect the chosen records inside a canvas app, so that related components can be updated. In this example, you allow selection of only a single item at a time so SelectedItems
will only ever contain a single record.
Add the following to the imports inside Grid.tsx
:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import {
ScrollablePane,
ScrollbarVisibility
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';
To the GridProps
interface, inside Grid.tsx
, add the following:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
}
Inside the Grid.tsx
function component, update the destructuring of the props
to add the new prop setSelectedRecords
.
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
} = props;
Directly below that, add:
const forceUpdate = useForceUpdate();
const onSelectionChanged = React.useCallback(() => {
const items = selection.getItems() as DataSet[];
const selected = selection.getSelectedIndices().map((index: number) => {
const item: DataSet | undefined = items[index];
return item && items[index].getRecordId();
});
setSelectedRecords(selected);
forceUpdate();
}, [forceUpdate]);
const selection: Selection = useConst(() => {
return new Selection({
selectionMode: SelectionMode.single,
onSelectionChanged: onSelectionChanged,
});
});
The React.useCallback and useConst hooks ensure that these values do not mutate between renders and cause unnecessary child component rendering.
The useForceUpdate hook ensures that when selection is updated, the component is re-rendered to reflect the updated selection count.
The selection
object created to maintain the state of the selection is then passed into the DetailsList
component:
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
></DetailsList>
You need to define the new setSelectedRecords
callback inside index.ts
and pass it to the Grid
component. Near the top of CanvasGrid
class, add the following:
export class CanvasGrid
implements ComponentFramework.StandardControl<IInputs, IOutputs>
{
notifyOutputChanged: () => void;
container: HTMLDivElement;
context: ComponentFramework.Context<IInputs>;
sortedRecordsIds: string[] = [];
resources: ComponentFramework.Resources;
isTestHarness: boolean;
records: {
[id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
};
currentPage = 1;
filteredRecordCount?: number;
Note
The method is defined as an arrow function to bind it to the current this
instance of the code component.
The call to setSelectedRecordIds informs the canvas app that the selection has changed so that other components referencing SelectedItems
and Selected
will be updated.
Finally, add the new callback to the input props of the Grid
component in the updateView
method:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
}),
this.container
);
There's a pattern in canvas apps where if a gallery or grid has an item selection invoked (for example, selecting a chevron icon), it raises the OnSelect
event. You can implement this pattern using the openDatasetItem method of the dataset.
As before, you add an additional callback prop on the Grid
component by adding the following to the GridProps
interface inside Grid.tsx
:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<
string,
ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
}
Again, you must add the new prop to the destructuring of the props:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
} = props;
The DetailList
has a callback prop called onItemInvoked
which, in turn, you pass your callback to:
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
></DetailsList>
Add the onNavigate
method to the index.ts
just below the setSelectedRecords
method:
onNavigate = (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
): void => {
if (item) {
this.context.parameters.records.openDatasetItem(item.getNamedReference());
}
};
This simply invokes the openDatasetItem
method on the dataset record so that the code component will raise the OnSelect
event. The method is defined as an arrow function to bind it to the current this
instance of the code component.
You need to pass this callback into the Grid
component props inside the updateView
method:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
}),
this.container
);
When you save all files, the test harness will reload. Use Ctrl
+ Shift
+ I
(or F12
) and use Open File (Ctrl
+ P
) searching for index.ts
and you can place a breakpoint inside the onNavigate
method. Double-click on a row (or highlight it with the cursor keys and pressing Enter
) will hit the breakpoint because the DetailsList
invokes the onNavigate
callback.
There is a reference to _this
because the function is defined as an arrow function and has been transpiled into a JavaScript closure to capture the instance of this
.
Before you go any further, you need to add resource strings to the code component so that you can use localized strings for messages such as paging, sorting, and filtering. Add a new file CanvasGrid\strings\CanvasGrid.1033.resx
and use the Visual Studio resource editor or Visual Studio Code with an extension to enter the following:
Name | Value |
---|---|
Records_Dataset_Display |
Records |
FilteredRecordCount_Disp |
Filtered Record Count |
FilteredRecordCount_Desc |
The number of records after filtering |
HighlightValue_Disp |
Highlight Value |
HighlightValue_Desc |
The value to indicate a row should be highlighted |
HighlightColor_Disp |
Highlight Color |
HighlightColor_Desc |
The color to highlight a row using |
HighlightIndicator_Disp |
Highlight Indicator Field |
HighlightIndicator_Desc |
Set to the name of the field to compare against the Highlight Value |
Label_Grid_Footer |
Page {0} ({1} Selected) |
Label_SortAZ |
A to Z |
Label_SortZA |
Z to A |
Label_DoesNotContainData |
Does not contain data |
Label_ShowFullScreen |
Show Full Screen |
Tip
It's not recommended to edit resx
files directly. Instead, use either Visual Studio's resource editor or an extension for Visual Studio Code. Find a Visual Studio Code extension: Search Visual Studio Marketplace for a resx editor
The data for this file can also be set by opening the CanvasGrid.1033.resx
file in Notepad and copying the XML content below:
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Records_Dataset_Display" xml:space="preserve">
<value>Records</value>
</data>
<data name="FilteredRecordCount_Disp" xml:space="preserve">
<value>Filtered Record Count</value>
</data>
<data name="FilteredRecordCount_Desc" xml:space="preserve">
<value>The number of records after filtering</value>
</data>
<data name="HighlightValue_Disp" xml:space="preserve">
<value>Highlight Value</value>
</data>
<data name="HighlightValue_Desc" xml:space="preserve">
<value>The value to indicate a row should be highlighted</value>
</data>
<data name="HighlightColor_Disp" xml:space="preserve">
<value>Highlight Color</value>
</data>
<data name="HighlightColor_Desc" xml:space="preserve">
<value>The color to highlight a row using</value>
</data>
<data name="HighlightIndicator_Disp" xml:space="preserve">
<value>Highlight Indicator Field</value>
</data>
<data name="HighlightIndicator_Desc" xml:space="preserve">
<value>Set to the name of the field to compare against the Highlight Value</value>
</data>
<data name="Label_Grid_Footer" xml:space="preserve">
<value>Page {0} ({1} Selected)</value>
</data>
<data name="Label_SortAZ" xml:space="preserve">
<value>A to Z</value>
</data>
<data name="Label_SortZA" xml:space="preserve">
<value>Z to A</value>
</data>
<data name="Label_DoesNotContainData" xml:space="preserve">
<value>Does not contain data</value>
</data>
<data name="Label_ShowFullScreen" xml:space="preserve">
<value>Show Full Screen</value>
</data>
</root>
You have resource strings for the input
/output
properties and the dataset
and associated property-set
. These will be used in Power Apps Studio at design time based on the maker's browser language. You can also add label strings that can be retrieved at runtime using getString. More information: Implementing localization API component.
Add this new resource file to the ControlManifest.Input.xml
file inside the resources
element:
If you want to allow the user to sort and filter using grid column headers, the Fluent UI DetailList
provides an easy way of adding context menus to the column headers.
First, add onSort
and onFilter
to the GridProps
interface inside Grid.tsx
to provide callback functions for sorting and filtering:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<
string,
ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (
item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
) => void;
}
Then, add these new props along with the resources
reference (so you can retrieve localized labels for sorting and filtering) to the props destructuring:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
} = props;
You need to add some imports to the top of Grid.tsx
so that you can use the ContextualMenu
component provided by Fluent UI. You can use path-based imports to reduce the size of the bundle.
import { ContextualMenu, DirectionalHint, IContextualMenuProps } from '@fluentui/react/lib/ContextualMenu';
Now add the context menu rendering functionality to Grid.tsx
just below the line
const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);
:
You'll see that:
contextualMenuProps
state controls the visibility of the context menu that's rendered using the Fluent UI ContextualMenu
component.resources.getString
to show labels on the context menu that can be localized.React.useCallback
hook, similar to React.useMemo
, ensures that the callbacks are only mutated when the dependent values change. This optimizes the rendering of child components.Add these new context menu functions to the column select and context menu events. Update the const gridColumns
to add the onColumnContextMenu
and onColumnClick
callbacks:
const gridColumns = React.useMemo(() => {
return columns
.filter((col) => !col.isHidden && col.order >= 0)
.sort((a, b) => a.order - b.order)
.map((col) => {
const sortOn = sorting && sorting.find((s) => s.name === col.name);
const filtered =
filtering &&
filtering.conditions &&
filtering.conditions.find((f) => f.attributeName == col.name);
return {
key: col.name,
name: col.displayName,
fieldName: col.name,
isSorted: sortOn != null,
isSortedDescending: sortOn?.sortDirection === 1,
isResizable: true,
isFiltered: filtered != null,
data: col,
} as IColumn;
});
}, [columns, sorting]);
For the context menu to be shown, you need to add it to the rendered output. Add the following directly underneath the DetailsList
component in the returned output:
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
</ScrollablePane>
Now that you've added the sorting and filtering UI, you need to add the callbacks to index.ts
to actually perform the sort and filter on the records bound to the code component. Add the following to index.ts
just below the onNavigate
function:
onSort = (name: string, desc: boolean): void => {
const sorting = this.context.parameters.records.sorting;
while (sorting.length > 0) {
sorting.pop();
}
this.context.parameters.records.sorting.push({
name: name,
sortDirection: desc ? 1 : 0,
});
this.context.parameters.records.refresh();
};
onFilter = (name: string, filter: boolean): void => {
const filtering = this.context.parameters.records.filtering;
if (filter) {
filtering.setFilter({
conditions: [
{
attributeName: name,
conditionOperator: 12, // Does not contain Data
},
],
} as ComponentFramework.PropertyHelper.DataSetApi.FilterExpression);
} else {
filtering.clearFilter();
}
this.context.parameters.records.refresh();
};
You'll see that:
Lastly, you can pass these two callbacks into the Grid
rendering call:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
}),
this.container
);
Note
At this point, you can no longer test using the test harness because it doesn't provide support for sorting and filtering. Later, you can deploy using pac pcf push and then add to a canvas app for testing. If you wish, you can skip to that step to see how the code component looks inside canvas apps.
Since the grid can now filter records internally, it's important to report back to the canvas app how many records are displayed. This is so that you can show a 'No Records' type message.
Tip
You could implement this internally within the code component, however it's recommended that as much user interface is left up to the canvas app since it will give the app maker more flexibility.
You have already defined an output property called FilteredRecordCount
in the ControlManifest.Input.xml
. When the filtering takes place and the filtered records are loaded, the updateView
function will be called with string dataset
in the updatedProperties array. If the number of records has changed, you need to make a call to notifyOutputChanged
so that the canvas app knows it must update any controls that use the FilteredRecordCount
property. Inside the updateView
method of index.ts
, add the following just above the ReactDOM.render
and below allocatedHeight
:
This updates the filteredRecordCount
on the code component class you defined earlier when it's different from the new data received. After notifyOutputChanged
is called, you need to ensure the value is returned when getOutputs
is called, so update the getOutputs
method to be:
For large datasets, canvas apps will split the records across multiple pages. You can add a footer that shows page navigation controls. Each button will be rendered using a Fluent UI IconButton
, which you must import.
Add this to the imports inside Grid.tsx
:
import { IconButton } from '@fluentui/react/lib/Button';
The following step will add capabilities to load the format for the page indicator label from the resource strings ("Page {0} ({1} Selected)"
) and format using a simple stringFormat
function. This function could equally be in a separate file and shared between your components for convenience:
In this tutorial, add it at the top of Grid.tsx
, right below type DataSet ...
.
function stringFormat(template: string, ...args: string[]): string {
for (const k in args) {
template = template.replace("{" + k + "}", args[k]);
}
return template;
}
In Grid.tsx
, add the following Stack.Item
below the existing Stack.Item
that contains the ScrollablePane
:
return (
<Stack verticalFill grow style={rootContainerStyle}>
<Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
{contextualMenuProps && <ContextualMenu {...contextualMenuProps} />}
</ScrollablePane>
{(itemsLoading || isComponentLoading) && <Overlay />}
</Stack.Item>
</Stack>
);
You'll see that:
Stack
ensures that the footer will stack below the DetailsList
. The grow
attribute is used to make sure that the grid expands to fill the available space."Page {0} ({1} Selected)"
) and format using the stringFormat
function you added in the previous step.alt
text for accessibility on the paging IconButtons
.Next, you must add the missing loadFirstPage
, loadNextPage
, and loadPreviousPage
callback props.
To the GridProps
interface, add the following:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
onSort: (name: string, desc: boolean) => void;
onFilter: (name: string, filtered: boolean) => void;
}
Add these new props to the props destructuring:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
} = props;
Add these callbacks to index.ts
below the onFilter
method:
loadFirstPage = (): void => {
this.currentPage = 1;
this.context.parameters.records.paging.loadExactPage(1);
};
loadNextPage = (): void => {
this.currentPage++;
this.context.parameters.records.paging.loadExactPage(this.currentPage);
};
loadPreviousPage = (): void => {
this.currentPage--;
this.context.parameters.records.paging.loadExactPage(this.currentPage);
};
Then update the Grid
rendering call to include these callbacks:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
onSort: this.onSort,
onFilter: this.onFilter,
}),
this.container
);
Code components offer the ability to show in full screen mode. This is especially useful on small screen sizes or where there's limited space for the code component within a canvas app screen.
To launch the full screen mode, you can use the Fluent UI Link
component. Add it to the imports at the top of Grid.tsx
:
import { Link } from '@fluentui/react/lib/Link';
To add a full screen link, you add the following to the existing Stack
that contains the paging controls.
Note
Be sure to add this to the nested Stack
, and not the root Stack
.
<Stack horizontal style={{ width: '100%', paddingLeft: 8, paddingRight: 8 }}>
<IconButton
alt="First Page"
iconProps={{ iconName: 'Rewind' }}
disabled={!hasPreviousPage}
onClick={loadFirstPage}
/>
<IconButton
alt="Previous Page"
iconProps={{ iconName: 'Previous' }}
disabled={!hasPreviousPage}
onClick={loadPreviousPage}
/>
<Stack.Item align="center">
{stringFormat(
resources.getString('Label_Grid_Footer'),
currentPage.toString(),
selection.getSelectedCount().toString(),
)}
</Stack.Item>
<IconButton
alt="Next Page"
iconProps={{ iconName: 'Next' }}
disabled={!hasNextPage}
onClick={loadNextPage}
/>
</Stack>
You'll see that:
Add the onFullScreen
and isFullScreen
props to the GridProps
interface inside Grid.tsx
to provide callback functions for sorting and filtering:
export interface GridProps {
width?: number;
height?: number;
columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
sortedRecordIds: string[];
hasNextPage: boolean;
hasPreviousPage: boolean;
totalResultCount: number;
currentPage: number;
sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
resources: ComponentFramework.Resources;
itemsLoading: boolean;
highlightValue: string | null;
highlightColor: string | null;
setSelectedRecords: (ids: string[]) => void;
onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
onSort: (name: string, desc: boolean) => void;
onFilter: (name: string, filtered: boolean) => void;
loadFirstPage: () => void;
loadNextPage: () => void;
loadPreviousPage: () => void;
}
Add these new props to the props destructuring:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
loadFirstPage,
loadNextPage,
loadPreviousPage,
} = props;
To provide these new props, inside index.ts
, add the following callback method below loadPreviousPage
:
onFullScreen = (): void => {
this.context.mode.setFullScreen(true);
};
The call to setFullScreen causes the code component to open the full screen mode and adjust the allocatedHeight
and allocatedWidth
accordingly because of the call to trackContainerResize(true)
in the init
method. Once the full screen mode is open, updateView
will be called, updating the rendering of the component with the new size. The updatedProperties
contains fullscreen_open
or fullscreen_close
, depending on the transition that is happening.
To store the state of the full screen mode, add a new isFullScreen
field to the CanvasGrid
class inside index.ts
:
export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
notifyOutputChanged: () => void;
container: HTMLDivElement;
context: ComponentFramework.Context<IInputs>;
sortedRecordsIds: string[] = [];
resources: ComponentFramework.Resources;
isTestHarness: boolean;
records: {
[id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
};
currentPage = 1;
filteredRecordCount?: number;
Add the following to the updateView
method to track the state:
public updateView(context: ComponentFramework.Context<IInputs>): void {
const dataset = context.parameters.records;
const paging = context.parameters.records.paging;
const datasetChanged = context.updatedProperties.indexOf("dataset") > -1;
const resetPaging =
datasetChanged &&
!dataset.loading &&
!dataset.paging.hasPreviousPage &&
this.currentPage !== 1;
if (resetPaging) {
this.currentPage = 1;
}
Now you can pass the callback and isFullScreen
field into the Grid
rendering props:
ReactDOM.render(
React.createElement(Grid, {
width: allocatedWidth,
height: allocatedHeight,
columns: dataset.columns,
records: this.records,
sortedRecordIds: this.sortedRecordsIds,
hasNextPage: paging.hasNextPage,
hasPreviousPage: paging.hasPreviousPage,
currentPage: this.currentPage,
totalResultCount: paging.totalResultCount,
sorting: dataset.sorting,
filtering: dataset.filtering && dataset.filtering.getFilter(),
resources: this.resources,
itemsLoading: dataset.loading,
highlightValue: this.context.parameters.HighlightValue.raw,
highlightColor: this.context.parameters.HighlightColor.raw,
setSelectedRecords: this.setSelectedRecords,
onNavigate: this.onNavigate,
onSort: this.onSort,
onFilter: this.onFilter,
loadFirstPage: this.loadFirstPage,
loadNextPage: this.loadNextPage,
loadPreviousPage: this.loadPreviousPage,
}),
this.container
);
Now you're ready to add the conditional row highlighting functionality. You've already defined the HighlightValue
and HighlightColor
input properties, and the HighlightIndicator
property-set
. The property-set
allows the maker to choose a field to use to compare with the value they provide in HighlightValue
.
Custom row rendering in the DetailsList
requires some additional imports. There are already some types from @fluentui/react/lib/DetailsList
, so add IDetailsListProps
, IDetailsRowStyles
and DetailsRow
to that import statement in Grid.tsx
:
import {
DetailsList,
ConstrainMode,
DetailsListLayoutMode,
IColumn,
IDetailsHeaderProps
} from '@fluentui/react/lib/DetailsList';
Now, create the custom row renderer by adding the following just below the const rootContainerStyle
block:
const onRenderRow: IDetailsListProps['onRenderRow'] = (props) => {
const customStyles: Partial<IDetailsRowStyles> = {};
if (props && props.item) {
const item = props.item as DataSet | undefined;
if (highlightColor && highlightValue && item?.getValue('HighlightIndicator') == highlightValue) {
customStyles.root = { backgroundColor: highlightColor };
}
return <DetailsRow {...props} styles={customStyles} />;
}
return null;
};
You'll see that:
HighlightIndicator
alias using:item?.getValue('HighlightIndicator')
.HighlightIndicator
field matches the value of the highlightValue
provided by the input property on the code component, you can add a background color to the row.DetailsRow
component is used by the DetailsList
to render the columns you defined. You don't need to change the behavior other than the background color.Add some additional props for highlightColor
and highlightValue
that will be provided by the rendering inside updateView
. You've already added to the GridProps
interface, so you just need to add them to the props destructuring:
export const Grid = React.memo((props: GridProps) => {
const {
records,
sortedRecordIds,
columns,
width,
height,
hasNextPage,
hasPreviousPage,
sorting,
filtering,
currentPage,
itemsLoading,
setSelectedRecords,
onNavigate,
onSort,
onFilter,
resources,
loadFirstPage,
loadNextPage,
loadPreviousPage,
onFullScreen,
isFullScreen,
} = props;
Pass the onRenderRow
method into the DetailsList
props:
<DetailsList
columns={gridColumns}
onRenderItemColumn={onRenderItemColumn}
onRenderDetailsHeader={onRenderDetailsHeader}
items={items}
setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
initialFocusedIndex={0}
checkButtonAriaLabel="select row"
layoutMode={DetailsListLayoutMode.fixedColumns}
constrainMode={ConstrainMode.unconstrained}
selection={selection}
onItemInvoked={onNavigate}
></DetailsList>
Now that you've implemented all the features, you must deploy the code component to Microsoft Dataverse for testing.
Inside your Dataverse environment, ensure there's a publisher created with a prefix of samples
:
This could also be your own publisher, provided you update the publisher prefix parameter in the call to pac pcf push below. More information: Create a solution publisher.
Once you've saved the publisher, you're ready to authorize the CLI against your environment so that we can push the compiled code component. At the command-line, use:
pac auth create --url https://myorg.crm.dynamics.com
Replace myorg.crm.dynamics.com
with the URL of your own Dataverse environment.
Sign in with an administrator/customizer user when prompted. The privileges provided by these user roles are needed to deploy any code components to Dataverse.
To deploy your code component, use:
pac pcf push --publisher-prefix samples
Note
If you receive the error, Missing required tool: MSBuild.exe/dotnet.exe. Please add MSBuild.exe/dotnet.exe in Path environment variable or use 'Developer Command Prompt for VS
, you must install either Visual Studio 2019 for Windows & Mac or Build Tools for Visual Studio 2019, being sure to select the '.NET build tools' workload as described in the prerequisites.
Once completed, this process will have created a small temporary solution named PowerAppTools_samples in your environment, and the CanvasGrid
code component will be added to this solution. You can move the code component into your own solution later if necessary. More information: Code Component Application Lifecycle Management (ALM).
To use code components inside canvas apps, you must enable the Power Apps component framework for canvas apps on the environment you're using.
a. Open the Admin center (admin.powerplatform.microsoft.com) and navigate to your environment. b. Navigate to Settings > Product > Features . Ensure Power Apps component framework for canvas apps is turned On:
Create a new canvas app using the Tablet layout.
From the Insert panel, select Get more components.
Select the Code tab on the Import components pane.
Select the CanvasGrid
component.
Select Import. The code component will now appear under Code components on the Insert panel.
Drag the CanvasGrid
component onto the screen and bind to the Contacts
table in Microsoft Dataverse.
Set the following properties on the CanvasGrid
code component using the properties panel:
1
- This is the value that statecode
has when the record is inactive.#FDE7E9
- This is the color to use when the record is inactive.HighlightIndicator
= "statecode"
- This is the field to compare against. This will be on the Advanced panel in the DATA section.
Add a new TextInput
component and name it txtSearch
.
Update the CanvasGrid.Items
property to be Search(Contacts,txtSearch.Text,"fullname")
.
As you type in the Text Input, you'll see that the contacts are filtered in the grid.
Add a new Text label and set the text to be "No records found". Position the label on top of the Canvas Grid.
Set the Visible property of the Text label to be CanvasGrid1.FilteredRecordCount=0
.
This means that when there are no records matching the txtSearch
value, or if a column filter is applied using the context menu that returns no records (for example, Full Name does not contain data), the label will be displayed.
Add a Display Form (from the Input group in the Insert panel).
Set the form DataSource
to the Contacts
table and add some form fields.
Set the form Item
property to CanvasGrid1.Selected
.
You should now see that when you select items on the grid, the form displays the item selected.
Add a new Screen to the canvas app called scrDetails
.
Copy the form from the previous screen and paste it onto the new screen.
Set the CanvasGrid1.OnSelect
property to be Navigate(scrDetails)
.
When you invoke the grid row select action, you should now see that the app navigates to the second screen with the item selected.
You can easily debug your code component while it's running inside the canvas app by opening Developer Tools using Ctrl+Shift+I
.
Select Ctrl+P
and type Grid.tsx
or Index.ts
. You can then set a break point and step through your code.
If you need to make further changes to your component, you don't need to deploy each time. Instead, use the technique described in Debug code components to create a Fiddler AutoResponder to load the file from your local file system while npm start watch
is running.
The AutoResponder would look similar to the following:
REGEX:(.*?)((?'folder'css|html)(%252f|\/))?SampleNamespace\.CanvasGrid[\.\/](?'fname'[^?]*\.*)(.*?)$
C:\repos\CanvasGrid\out\controls\CanvasGrid\${folder}\${fname}
You'll also need to enable the filters to add the Access-Control-Allow-Origin
header. More information: Debugging after deploying into Microsoft Dataverse.
You'll need to Empty cache and hard refresh on your browser session for the AutoResponder file to be picked up. Once loaded, you can simply refresh the browser since Fiddler will add a cache control header to the file to prevent it from being cached.
Once you're happy with your changes, you can increment the patch version in the manifest, and then redeploy using pac pcf push.
So far, you've deployed a development build, which is not optimized and will run slower at runtime. You can choose to deploy an optimized build using pac pcf push by editing the CanvasGrid.pcfproj
. Underneath the OutputPath
, add the following: <PcfBuildMode>production</PcfBuildMode>
<PropertyGroup>
<Name>CanvasGrid</Name>
<ProjectGuid>a670bba8-e0ae-49ed-8cd2-73917bace346</ProjectGuid>
<OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
</PropertyGroup>
Application lifecycle management (ALM) with Microsoft Power Platform
Power Apps component framework API reference
Create your first component
Debug code components
Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreTraining
Learning path
Use advance techniques in canvas apps to perform custom updates and optimization - Training
Use advance techniques in canvas apps to perform custom updates and optimization