Build PCF: Make Specified Attribute(s) ReadOnly on Power Apps Grid Control

Again, another request from my friend Trung Dung Nguyen. He asked me to explore Power Apps Grid Control and make some attributes to be “read-only”. For those who don’t know, Power Apps Grid control allows us to set some features like inline editor (like Editable Grid).

Power Apps Grid Control

To accomplish the task, the first thing I need to know is to understand the control itself. In the documentation, we can learn that we can leverage the control using PCF control (PCF that calls another PCF) which you can read here. Then, for the sample itself, we can check on this article.

Other than that resources, when I tried to google it. I found an article from our beloved PCF Queen – Diana which solved the same problem in this blog post and gives several ways to solved it that you can read here (one of the idea is being used in this blog post).

And, without further ado. Let’s see how I implement this:

Create The Table

First, I created the below table and try to fit all the data types that the control support:

Custom Table for Demo

Then to enable the Power Apps Grid Control, I set up this:

Enable Power Apps Grid Control

PCF Code

Again, if you follow the article that I shared above. We can see the sample PCF control on Microsoft/PowerApps-Samples that you can see here.

I will not share step by step to create the control, but I will share with you the main logic of the PCF.
The idea is to make the PCF configurable by the end user. So for that purpose, we will use Dataverse Environment Variable like the below screenshot:

Grid Customizer Environment Variable

We need to create Environment Variable with a display name “Grid Customizer“. Later on, in the PCF code, we will retrieve this config. For the JSON sample, I’ll use below string:

[{"entity":"tmy_alltypesattribute","attributes":["tmy_name", "tmy_email", "tmy_phone", "tmy_ticker", "tmy_url", 
"tmy_textarea", "tmy_lookup", "tmy_customer", "tmy_multiselectpicklist", "tmy_optionset", "tmy_duration", "tmy_language",
"tmy_timezone", "tmy_wholenumber", "tmy_currency", "tmy_decimal", "tmy_floatingpoint", "tmy_autonumber", "tmy_dateonly", 
"tmy_dateandtime", "tmy_image", "tmy_file", "tmy_richtext"]}]

For the CellEditorOverrides.tsx, here is the code:

import { CellEditorOverrides, CellEditorProps, GetEditorParams } from '../types';
import { stateData } from '../index';
export const cellEditorOverrides: CellEditorOverrides = {
    ["Text"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Email"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Phone"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Ticker"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["URL"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TextArea"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Lookup"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Customer"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Owner"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["MultiSelectPicklist"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["OptionSet"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TwoOptions"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Duration"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Language"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Multiple"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TimeZone"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Integer"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Currency"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Decimal"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["FloatingPoint"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["AutoNumber"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["DateOnly"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["DateAndTime"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Image"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["File"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Persona"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["RichText"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["UniqueIdentifier"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col)
}
function renderControl(props: CellEditorProps, col: GetEditorParams) {
    const columnName = col.colDefs[col.columnIndex].name.toLowerCase();
    if (stateData.Setting.attributes.length > 0 && stateData.Setting.attributes.indexOf(columnName) > -1) {
        col.stopEditing(true);
    }
    return null;
}

As you can see in the above code, I use a global variable name “stateData” that will store the information of the attributes that need to be “read-only”. Then for the implementation itself, whenever the editor is called and the attribute is supposed to be “read-only”, we will call the method “col.StopEditting(true)” which stops the editor to be editable. I think this is the easiest implementation that we can do so far.

UPDATED for OptionSet

Diana gives her expertise on the subject and asks about OptionSet. Hence, we need to little bit modify CellRendererOverrides.tsx:

import { CellEditorOverrides, CellEditorProps, GetEditorParams } from '../types';
import { stateData } from '../index';
export const cellEditorOverrides: CellEditorOverrides = {
    ["Text"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Email"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Phone"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Ticker"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["URL"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TextArea"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Lookup"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Customer"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Owner"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["MultiSelectPicklist"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["OptionSet"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TwoOptions"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Duration"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Language"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Multiple"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["TimeZone"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Integer"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Currency"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Decimal"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["FloatingPoint"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["AutoNumber"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["DateOnly"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["DateAndTime"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Image"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["File"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["Persona"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["RichText"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col),
    ["UniqueIdentifier"]: (props: CellEditorProps, col: GetEditorParams) => renderControl(props, col)
}
function renderControl(props: CellEditorProps, col: GetEditorParams) {
    const columnName = col.colDefs[col.columnIndex].name.toLowerCase();
    if (stateData.Setting.attributes.length > 0 && stateData.Setting.attributes.indexOf(columnName) > -1) {
        col.stopEditing(true);
    }
    return null;
}

If there’s a read-only config with the type for OptionSet, we need to change the renderer to only “Label”.

Then below is the code for index.ts:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { cellRendererOverrides } from "./customizers/CellRendererOverrides";
import { cellEditorOverrides } from "./customizers/CellEditorOverrides";
import { PAOneGridCustomizer, SettingModel } from "./types";
import * as React from "react";
let stateData: { Settings: SettingModel[], entity: string } = { Settings: [], entity: '' };
export class PGDReadonly implements ComponentFramework.ReactControl<IInputs, IOutputs> {
    /**
     * Empty constructor.
     */
    constructor() { }
    /**
     * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
     * Data-set values are not initialized here, use updateView.
     * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
     * @param _notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
     * @param _state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
     */
    public init(
        context: ComponentFramework.Context<IInputs>,
        _notifyOutputChanged: () => void,
        _state: ComponentFramework.Dictionary
    ): void {
        const contextObj: any = context;
        stateData.entity = contextObj.client._customControlProperties.pageType === 'EntityList' ?
            (contextObj.page?.entityTypeName ?? '') : (contextObj.client._customControlProperties.descriptor.Parameters?.TargetEntityType ?? '');
        const environmentVariableName = 'Grid Customizer';
        context.webAPI.retrieveMultipleRecords("environmentvariabledefinition", `?$filter=displayname eq '${environmentVariableName}'&$select=environmentvariabledefinitionid&$expand=environmentvariabledefinition_environmentvariablevalue($select=value)`)
            .then(result => {
                var current = result && result.entities ? result.entities[0] : {};
                var value = current['environmentvariabledefinition_environmentvariablevalue'] ? current['environmentvariabledefinition_environmentvariablevalue'][0].value : null;
                var models: SettingModel[] = value ? JSON.parse(value) : [];
                stateData.Settings = models.filter(e => e.entity == stateData.entity);
            });
        const eventName = context.parameters.EventName.raw;
        if (eventName) {
            const paOneGridCustomizer: PAOneGridCustomizer = { cellRendererOverrides, cellEditorOverrides };
            contextObj.factory.fireEvent(eventName, paOneGridCustomizer);
        }
    }
    /**
     * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
     * @param _context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
     * @returns ReactElement root react element for the control
     */
    public updateView(_context: ComponentFramework.Context<IInputs>): React.ReactElement {
        return React.createElement(React.Fragment);
    }
    /**
     * It is called by the framework prior to a control receiving new data.
     * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
     */
    public getOutputs(): IOutputs {
        return {};
    }
    /**
     * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
     * i.e. cancelling any pending remote calls, removing listeners, etc.
     */
    public destroy(): void {
        // Add code to cleanup control if necessary
    }
}
export { stateData };

As you can see, the important part of the above code is to retrieve the Environment Variable which the Display Name=”Grid Customizer“. Then we will store the information in the global variable.

Once the PCF is done, we can build and deploy the PCF to our testing environment.

Again, to test it, we need to configure the Control to call our PCF:

Set the Power Apps Grid Control to call the new PCF

You can click save + publish all the changes so we can test it.

Demo

Here is the demo:

Demo Power Apps Grid Control Read Only

You can check all the code in this GitHub repo.
Happy CRM-ing!

Author: temmyraharjo

Microsoft Dynamics 365 Technical Consultant, KL Power Platform User Community Leader, Student Forever, Test Driven Development, and Human Code enthusiast.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.