
As promised, I’ll continue the series of Power Apps grid API with a few use-cases. Today I’m having a look at one of the most requested grid extensibility feature: disabling. There are different kind of disabling requirements, and I would differentiate between:
- disabling a complete grid
- disabling complete columns
- disabling specific cells
The last two ones could be treated the same, but because of the bug of OnRecordSelect not being triggered sometimes, I treat them different.
Then, there is also a difference if we need to work on a:
- grid in the home area (sitemap) or related grid
- subgrid
I’ll try to go into details for each case, having also a look at extended requirements where we need to make some requests to get the data we need for the decision. The API doesn’t provide async events, so we need to have a solution to work around that.
At the end there is a decision flowchart, to help you navigate through all cases.
If you are interested in only a specific case, here is the table of content:
- Disabling the complete grid
- Disabling the complete nested grid
- Disabling columns
- Disabling specific cells on subgrids
- Conclusion: what to use and when
For this blog let’s consider the following tables: Project, TimeEntries and Milestone. Each Project can have more TimeEntries and more Milestones. Each TimeEntry can be related to a Milestone from the list of the Milestones corresponding to this Project.

Disabling the complete grid
The first option to disable a complete grid is to customize it as “not editable”

Also, there is a case where the grid will be automatically disabled for you: if the form is disabled, the subgrid will be automatically be disabled.
Advertisement
Privacy Settings
But what if you need to change this dynamically, based on some other attributes? We can do that using the gridControl.setDisabled(true)
123 | const formContext = executionContext.getFormContext(); const gridControl = formContext.ui.controls.get( "Subgrid_TimeEntries" ); gridControl.setDisabled( true ); |
But of course, you probably will have a dependency to set it disabled or not. In that case we need to set the disabled state also when the form attribute changes.
Use-Case: Let’s consider that I have a form for the table “Project”, and there we have the subgrid for “TimeEntries”. The subgrid should be editable only if the project already started
(Project.StartDate< today).
Implementation: We register to form.onLoad, and there we disable the subgrid. You will end up with some similar code:
window.Project_Subgrid_TimeEntries = ( function (){ function disableGridByStartDate(grid, form){ const dateFromAttribute = form?.getAttribute( "diana_startdate" ); const dateFromValue = dateFromAttribute?.getValue(); const isDisabled = dateFromAttribute== null || (dateFromValue != null && dateFromValue > new Date()); grid?.setDisabled(isDisabled); } function onLoad(executionContext){ const formContext = executionContext.getFormContext(); const gridControl = formContext.ui.controls.get( "Subgrid_TimeEntries" ); disableGridByStartDate(gridControl, formContext); //addOnChange on form attribute const dateFromAttribute = formContext?.getAttribute( "diana_startdate" ); if (dateFromAttribute){ dateFromAttribute.addOnChange(()=>{ disableGridByStartDate(gridControl, formContext); //need to refresh the grid here gridControl.refresh(); }); } } return { onLoad } })(); |
In form.OnLoad we’ll register Project_Subgrid_TimeEntries.onLoad (similar with the starting blog of this series, where we’ve went through the grid APIs).
Notice that in onChange event is not enough to call grid.setDisabled(true). We need to call also the grid.refresh() after that, otherwise we won’t see the change. Now the grid gets disabled/enabled every time I change the StartDate (in the past/future).
Visual feedback about the disabled state is very subtle
What the user will see is very subtle that probably nobody knows that is not editable until they try to edit a cell. As a comparison, this is the grid in editable mode:


And now let’s see how looks a grid set as disabled:



So a trained eye would notice that the grid is disabled right before trying to edit it, but in my opinion the most of the users would try to edit, and when they double-click to go into edit mode, the grid will navigate to the record form instead. Probably not the best user-experience. Honestly, I always get unpleasant surprised when it doesn’t work as I expect. But disabling the grid does offer a way to protect the data, since the user won’t be able to edit it. And probably we can train the users to detect the small feedback.
https://www.youtube.com/embed/9D2vmzvcT64?feature=oembedA short demo on disabling and enabling the complete grid
Working asynchronous
Of course here we don’t have issues. If the condition to disable/enable the grid is not available on the form, we can use the form onLoad and/or onChange events to retrieve the data and disable/enable the grid after that.
Disabling the complete nested grid
I am not aware of a way to get access to the nested grid using code. So probably it’s not possible to disable the nested grid using code. The only way I know is by setting the corresponding property in the customizing:

Visual feedback much better
Of course.. the property is called “disable editing” while the one for the main grid was called “Enable editing”. For the parent grid we don’t have the customizing option to set it on disabled. There is only the “editable or not” option (but oddly we can use code to set it on disabled (setDisabled), and that has the same effect as the “Not editable” customizing option). Not sure if it’s really different..
But the nested grids can be set on disabled. And the the visual feedback is much better: the cells are grayed out:

Disabling columns
In this case, we don’t have to disable the complete grid. Instead, we need to disable only specific columns. In this case we don’t differentiate on the data on the row, we just need to complete disable the one or more columns.
Some columns are disabled by default
Some columns are disabled by default. There are two types of “disabling”:
- there are some columns grayed out. This seems to be the case with the columns which are not writable on the Dataverse side, like FormulaColumn or CreatedOn in my screenshot below. When the user hovers over these cells, the “uneditable” icon is shown on the right side of the cell.
- some columns are disabled but the user sees that only on hover over those cells (like State or Status columns in my screenshot). The same “uneditable” icon is shown.

But the other columns are editable. What if we want to lock them? We need some code to do that.
Disabling columns on subgrids
If we need to disable the columns of a subgrid, one approach would be to use the “OnRecordSelect” event. Later in this blog we will use that. But since the OnRecordSelect is not always triggered before the cells could be edited, and the visual representation if shown only on trying to edit the cell, we will try now another approach: by using the grid.onLoad event (event described in the grid API blog).
Advertisement
Privacy Settings
Use-Case: Let’s consider the tables as before: we have a form for “Project”, there a subgrid with the reported “Time Entries”. The project could be of different types, and only some projects allow milestones. That’s why we have a field on the form “Has milestones”. If the value is “no”, the Milestone column in the subgrid should be disabled.
Implementation: We have a similar code structure as before, using the form.onLoad to attach the “grid.onLoad” event. The “disableColumn” function gets the rows loaded through the executionContext, and each row gets the attribute “diana_milestone” and sets it on disabled using rowAttribute’s “setDisabled” method.
The controls for an attribute is a collection, but in the row context we cannot have more controls for the same attribute (as it can happen on the form), so we use always the first control (attribute.controls.get(0)).
window.Project_Subgrid_TimeEntries = ( function (){ function disableColumn(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); const form = gridExecutionContext.getFormContext(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const isEnabled = form.getAttribute( "diana_hasmilestones" )?.getValue() ?? true ; const rows = grid.getGrid().getRows().get(); //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(!isEnabled); } } //using form.onLoad to attach the grid.onLoad event const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); } return { onLoad } })(); |
Using this script the “Milestone” column is locked, but the user sees the “uneditable” icon only on hover, or when he/she tries to edit the cell.
When the user scrolls down, more rows are loaded, another grid.onLoad event is triggered, and the function disableColumn with disable the cells also for the new loaded rows.

Disabling columns of a subgrid with dependency on form fields
The example of disabling the columns works fine. But disabling depends on the form attribute “HasMilestone”. If the user changes the value to “yes”, the Milestone column in the subgrid should be enabled again.
For that we need to attach the an onChange event to the “HasMilestone” attribute on the form. We can do that in form.OnLoad. All we have to do in the “onChange” event is to refresh the grid. That fill cause a little “flash” on the subgrid, but we’ve changed something on the form, not inside the grid itself.. so it should be ok.
window.Project_Subgrid_TimeEntries = ( function (){ function disableColumn(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); const form = gridExecutionContext.getFormContext(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const isEnabled = form.getAttribute( "diana_hasmilestones" )?.getValue() ?? true ; const rows = grid.getGrid().getRows().get(); //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(!isEnabled); } } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); form.getAttribute( "diana_hasmilestones" ).addOnChange(()=>{ grid.refresh(); }); } return { onLoad } })(); |
Disable subgrid columns async- dependency on related data
In the pervious chapter we had a dependency on a form attribute. But what if we need to make a request first in order to get the data the disabling depends on? Let’s see a few solutions.
Solution 1. Disabling columns async in grid.OnLoad
One approach could be to use the grid.OnLoad event to make the request and disable the cell only after that.
Limitation: But the problem with that: the grid renders the cells, and the cells are not shown as disabled until the user tries to edit them. I’ll simulate the async process with a timeout. The promise is resolved after the grid.OnLoad event ends. The cell gets the disabled state, but the cells are not rendered anymore, so the user sees this only on trying to edit. So this is probably not the best option.

The implementation: The code I’ve used:
window.Project_Subgrid_TimeEntries = ( function (){ let disabledValue = false ; function gridOnLoad(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const rows = grid.getGrid().getRows().get(); window.setTimeout(() => { //emulates async request disabledValue = true ; //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(disabledValue); } }, 5000); } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(gridOnLoad); } return { onLoad } })(); |
Solution 2. Disabling async in form.OnLoad works better
The second way, would be to make the async request in form.OnLoad, and cache the needed data. In form.OnLoad I simulate the request with a timeout. Notice that I need to call the grid.refresh() for the case where the grid rendered before the promise is back.
Advertisement
Privacy Settings
The implementation: The code I’ve used:
window.Project_Subgrid_TimeEntries = ( function (){ let disabledValue = false ; function disableColumn(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const rows = grid.getGrid().getRows().get(); //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(disabledValue); } } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); window.setTimeout(() => { disabledValue = true ; grid?.refresh(); }, 5000); } return { onLoad } })(); |
Limitation: This works pretty well. The cells are disabled also onHover. The problem with is that the request might be slow. If the grid renders before the promise is resolved, the cells are editable for a short while. So the better approach is to set the cells on disabled, and enable it only if need after that. That causes a little flash.
Another issue: we need to load the data in form.onLoad, even if the grid is not used, and maybe on another tab.
Solution 3. Disabling async in form.OnLoad impoved
To improve the previous version, we could hide the grid control and show it only after the promise is completed. In that case no render is made until everything is prepared.
Here is the code I’ve used:
window.Project_Subgrid_TimeEntries = ( function (){ let disabledValue = false ; function disableColumn(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const rows = grid.getGrid().getRows().get(); //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(disabledValue); } } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); grid.setVisible( false ); window.setTimeout(() => { disabledValue = true ; grid.setVisible( true ); }, 5000); } return { onLoad } })(); |
Since we don’t need a grid refresh (no image flickering) and the data can be loaded when we are ready, this could be the best option.
Solution 4. Disabling columns async based on “async form.OnLoad”
Another way to work with async requests before disabling, would be to use the asynchronous form.OnLoad. For that we need first to enable the “Async onload hander” in the app settings.

Then we don’t need a grid.refresh(), and the data is safe, since the controls are not rendered until the promise is resolved.
Advertisement
Privacy Settings
Implementation: Notice that I return the promise from form.OnLoad so the form rendering waits for the promise.
window.Project_Subgrid_TimeEntries = ( function (){ let disabledValue = false ; function disableColumn(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); if (!grid) { console.error( "cound't find grid in onLoad" ); } const rows = grid.getGrid().getRows().get(); //disable the column "diana_milestone" for (const row of rows){ const attribute = row.data.entity.attributes.get( "diana_milestone" ); attribute.controls.get(0).setDisabled(disabledValue); } } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); const {promise, resolve} = Promise.withResolvers(); window.setTimeout(() => { disabledValue = true ; resolve(); }, 5000); return promise; } return { onLoad } })(); |
Solution 5. Using tab “onVisible”: tabStateChanged
This is a small change to the version 3, which can be applied if the subgrid is not placed on the first tab. We hide the grid on form.onLoad and show it only when the tab is visible and the data is retrieved. The code is similar with the solution “3”, but the onLoad looks like this
//we start with undefined. Then we know when the request was made and the disabled value is set let disabledValue = undefined; const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); grid.addOnLoad(disableColumn); grid.setVisible( false ); //after hiding the gridControl, we attach to OnTabStateChhange event const tab = form.ui.tabs.get( "tab_timeentries" ); tab.addTabStateChange(() => { if (disabledValue != null ) return ; //is the value was not already retrieved window.setTimeout(() => { //make only now the async request disabledValue = true ; grid.setVisible( true ); }, 5000); }); } |
The advantage of this solution is that we make the requests only if the tab is activated. If the user doesn’t visit the tab, the data is not fetched.
Disable columns of a grid in the main area (sitemap)?
Here things get harder, since we don’t have the grid.OnLoad event, as we did for subgrids.
I was thinking that it could be a solution to add copies of the column as FormulaColumn, That could work. But it has some serious cons: like bloating the table with technical columns, and loosing some features for some column types (like links for lookups). But the main issue with this approach is that the user is able to add columns to the view .. and could add the original column which is not disabled. So this is not an option.
Implementation using OnRecordSelect: The only way we can disable the columns for the main grid is by using the OnRecordSelect event.
The script to disable “Milestone” columns looks like this:
window.Project_Subgrid_TimeEntries = ( function (){ function onRecordSelect(selectExecutionContext){ const row = selectExecutionContext.getEventSource(); const attribute = row?.attributes?.get( "diana_milestone" ); if (!attribute) return ; attribute.controls?.get(0)?.setDisabled( true ); } return { onRecordSelect } })(); |
To register the script we need the switch to “classic”, select the Entity, and choose the “Events” tab:

Limitation: the cells are shown as disabled as soon we’ve visited the row at least once. If we try to edit the cell, the grid realizes it’s locked and shows the symbol from now on.

The visual feedback is not the biggest problem. As I’ve shown in the other blog, the PowerApps grid allows the users to navigate by keys, and edit cells that way without triggering OnRecordSelect. In the example below:
- Click on Name on Row 2 and edit the name
- Click Enter
- User “Arrow Down” to navigate to the Name in another row
- Type the value for the name on another row, sets the row in edit mode without triggering the “OnRecordSelect”. You see that the lookup “Milestone” is in edit mode, since the shown text is “Select lookup”
- Use “TAB” to navigate to Milestone. You are able to edit the Milestone , since is not the selected row.


Async limitation
Disabling the columns for main area (sitemap) has a few limitations. The first limitation is the one we just saw: cell editing is possible without selection the row.
Advertisement
Privacy Settings
Another one is that we don’t have an onLoad event to retrieve related data.
For instance we cannot disable columns depending on other columns that are not shown in the view (it could be a column from the same table or from a related lookup). The OnRecordSelect won’t wait, so at least the first selection of the row might provide the data needed for disable too late.
Disabling specific cells on subgrids
So we’ve saw how to disable a complete column. But what if we need to disable the cells only on some rows, depending on the row data?
Disabling specific cells in grid.OnLoad doesn’t work
The first attempt would be to use the same idea as in disabling the complete columns:
- load the related data in form.onLoad
- use the grid.onLoad event to disable the cells
Unfortunately this doesn’t work. I have the feeling that attribute.controls always access the same instance of the control and that accessing a specific instance of the control works only if we’ve selected the row. It seems to me that the value applied is only the one for the last row.
This is the code I’ve tried out and didn’t work (my cells were still editable):
window.Project_Subgrid_TimeEntries = ( function (){ let allRelatedData = {}; const retrieveRelatedDataByRowIds = async (id) => { //as shown above } function getMilestoneDisabledState(attribute){ //as shown above } function gridOnLoad(gridExecutionContext){ const grid = gridExecutionContext.getEventSource(); if (!grid) { console.error( "cound't find grid in onLoad" ); } for (const row of grid.getGrid().getRows().get()){ const attributeMilestone = row.data.entity.attributes.get( "diana_milestone" ); const milestoneLocked = getMilestoneDisabledState(attributeMilestone); for (const attribute of row.data.entity.attributes.get()){ attribute.controls.get(0).setDisabled(milestoneLocked); } } } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); const grid = form.ui.controls.get( "Subgrid_TimeEntries" ); retrieveRelatedDataByRowIds(form.data.entity.getId()); grid.addOnLoad(gridOnLoad); } return { onLoad } })(); |
Disabling cells on subgrids async – dependency on related data
This time we need more related data, since every row might have another dependency. In a lot of cases we might need to retrieve the related data first, which we can do in form.onLoad (since we saw that the grid.onLoad is too late to apply changes).
Use-Case: In this part I would like to disable all cells of a row, if the attribute “Milestone” is inactive (Milestone.statecode == 1). So we need to retrieve the state of all Milestones associated with the project, and cache that, so we can use it when the events are triggered
Implementation: I will need some help functions. For instance to retrieve data and to check if the row should be disabled:
window.Project_Subgrid_TimeEntries = ( function (){ //used to cache the related data let allRelatedData = {}; const retrieveRelatedDataByRowIds = async (id) => { const fetchXml = [ "<fetch mapping='logical'>" , "<entity name='diana_milestone'>" , "<attribute name='diana_milestoneid'/><attribute name='diana_name'/><attribute name='statecode'/>" , "<filter type='and'>" , `<condition attribute= 'diana_project' operator= 'eq' value= '${id}' />`, "</filter>" , "</entity></fetch>" ].join( "" ); const result = await Xrm.WebApi.retrieveMultipleRecords( "diana_milestone" , "?fetchXml=" +fetchXml); const newData = result.entities.reduce((acc, entity) => { acc[entity.diana_milestoneid] = entity.statecode; return acc; }, {}); allRelatedData = newData; console.log( "allRelatedData" , allRelatedData); } function getMilestoneDisabledState(attribute){ const milestioneValue = attribute.getValue(); if (milestioneValue == null || milestioneValue.length == 0){ return ; } const milestioneid = milestioneValue[0]?.id?.toLowerCase()?.replace( "{" , "" )?.replace( "}" , "" ); return allRelatedData[milestioneid] == 1; } })(); |
Disabling works through OnRecordSelect
So we need to use the OnRecordSelect event to disable the cell. This one works (except for the case when the user navigates by key, since the OnRecordSelect is not triggered in that case, as we saw before)
Advertisement
Privacy Settings
Implementation:
window.Project_Subgrid_TimeEntries = ( function (){ let allRelatedData = {}; const retrieveRelatedDataByRowIds = async (id) => { //as shown above } function getMilestoneDisabledState(attribute){ //as shown anbove } const onLoad = (executionContext) => { const form = executionContext.getFormContext(); retrieveRelatedDataByRowIds(form.data.entity.getId()); } function onRecordSelect(eventContext){ const rowEntity = eventContext.getEventSource(); const attribute = rowEntity?.attributes?.get( "diana_milestone" ); if (!attribute) return ; const milestoneLocked = getMilestoneDisabledState(attribute); for (const attribute of rowEntity.attributes.get()){ attribute.controls.get(0).setDisabled(milestoneLocked); } } return { onLoad, onRecordSelect } })(); |
Of course I need to register both the “form onLoad” and the “grid onRecordSelect” event using the customizing (as described in the grid API blog)
Limitation: Since this is based onRecordSelect, the uses sees the disabled state only after the row is selected at least once.
Disabling cells in main grid (home area / sitemap / related grid / subgrid)
In case we have all the needed data inside the view, we just need to use the OnRecordSelect and disable the controls.
Use-Case: In the following example I’m using the column “Name”. If the value is “PCF” I’ll disable the milestone.
Implementation: We need the OnRecordSelect event to disable the cells. Since the dependency is on a column from the view, we need also to register the OnChange event for the Name.
The code:
window.Project_Subgrid_TimeEntries = ( function (){ function onRecordSelect(selectExecutionContext){ const row = selectExecutionContext.getEventSource(); const attribute = row?.attributes?.get( "diana_milestone" ); if (!attribute) return ; const nameAttribute = row.attributes.get( "diana_name" ); if (!nameAttribute){ console.error( "Couldn't find diana_name attribute" ); return ; } const milestoneLocked = nameAttribute.getValue() == "PCF" ; attribute.controls?.get(0)?.setDisabled(milestoneLocked); } function onNameChange(executionContext){ const row = executionContext.getFormContext(); const attributeMilestone = row.data.entity.attributes.get( "diana_milestone" ); if (!attributeMilestone) return ; const milestoneLocked = row.data.entity.attributes.get( "diana_name" ).getValue() == "PCF" ; attributeMilestone.controls.get(0).setDisabled(milestoneLocked); } return { onRecordSelect, onNameChange } })(); |
The result:


Limitations: If the data is not available in the view, I am not aware of a way to retrieve data async beforehand, so we cannot use the API to disable the cells. For that case we can only use the customizer control for Power Apps grid (PCF for Power Apps Grid).
The limitations for main grid vs subgrid
As we saw, in the subgrid we can use the form onLoad event to retrieve related data. In the home area/sitemap we don’t have that event. As a workaround we could try to add the related data in the view, to have it available when the row is selected. But the user can remove the columns and the disable rule cannot be evaluated anymore.


Conclusion: what to use and when
It looks pretty hard to remember which rule to be used in which case. I’ve tried to make a flowchart to help you decide which solution to be used.
Advertisement
Privacy Settings
For a subgrid I follow these rules:

For a grid in the home area, we have some serious limitations. There we could only choose between:
- disable in the customizing
- use OnRecordSelect (maybe in combination with record OnChange)
Know the limitations! Where this is no solution available or the solution is not good enough, we will need a customizer control. I have an older blog about disabling using the customizer control (PCF). More on this to come soon.
Maybe you have a better solution for a special use-case. I would like to hear about that, to learn from each other.
About the Author

Diana Birkelbach, MVP
Dianamics PCF Lady | Microsoft MVP | Blogger | Power Platform Community Super User | 👩💻 Dynamics 365 & PowerPlatform Developer | ORBIS SE
Reference:
Birkelbach, D (2025). Power Apps Grid API – Disabling. Available at: Power Apps Grid API – Disabling – Dianamics PCF Lady [Accessed: 15th January 2025].