From 41cd080658c6a3fbfe650b6c0d3107720e8d2a75 Mon Sep 17 00:00:00 2001 From: Joshua Wakefield Date: Wed, 19 Aug 2026 14:23:05 -0400 Subject: [PATCH] Fix "That Wasn't Supposed to Happen" on grid Excel export for LavaDataObject-backed grids Rock 16 made AttendanceListOccurrence inherit LavaDataObject (upstream ecef40fd9b, issue #5917), whose public this[string key] indexer reflects as a property named Item. The grid's DataSource-mode Excel export reads every property with prop.GetValue( item, null ) and throws TargetParameterCountException on the indexer, failing the export on every group (ExceptionLog 1573206-1579801, 2026-08-18). FilterDynamicObjectPropertiesCollection already strips LavaDataObject base properties, but only on the Fluid branch; our RockLiquid (DotLiquid) engine setting takes the branch that never did. The v16 line ended (1.16.13.1) with the gap still present; v17 removed RockLiquid entirely, which is why this is unreported upstream. - Grid.cs: skip indexer properties in the export property filter - Grid.cs: strip LavaDataObject base properties on the RockLiquid branch, mirroring the Fluid branch (also repairs Merge Template and Launch Workflow via GetEntitySetFromGridSourceList) - LavaField.cs: exclude indexer properties when building the custom Lava column property dictionary, which otherwise throws at render for any custom column on a LavaDataObject-backed grid --- Rock/Web/UI/Controls/Grid/Grid.cs | 17 +++++++++++++++++ Rock/Web/UI/Controls/Grid/LavaField.cs | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Rock/Web/UI/Controls/Grid/Grid.cs b/Rock/Web/UI/Controls/Grid/Grid.cs index f09691a97cc..9586e245b35 100644 --- a/Rock/Web/UI/Controls/Grid/Grid.cs +++ b/Rock/Web/UI/Controls/Grid/Grid.cs @@ -2444,6 +2444,15 @@ protected void Actions_ExcelExportClick( object sender, EventArgs e ) continue; } + // Skip indexer properties. They cannot be read without an index argument, so the + // prop.GetValue( item, null ) call below would throw a TargetParameterCountException. + // LavaDataObject exposes a public "this[string key]" indexer, so any grid whose data + // source type inherits from it lands here. + if ( prop.GetIndexParameters().Length > 0 ) + { + continue; + } + props.Add( prop ); } @@ -2748,6 +2757,14 @@ private List FilterDynamicObjectPropertiesCollection( Type dataSou var dropProperties = typeof( RockDynamic ).GetProperties().Select( a => a.Name ); additionalMergeProperties = additionalMergeProperties.Where( a => !dropProperties.Contains( a.Name ) ).ToList(); } + // If this is a LavaDataObject class, don't include any of the properties that are inherited from + // LavaDataObject. This mirrors the non-RockLiquid branch below. Without it, LavaDataObject's + // "this[string key]" indexer is treated as an exportable "Item" column. + else if ( typeof( LavaDataObject ).IsAssignableFrom( dataSourceObjectType ) ) + { + var dropProperties = typeof( LavaDataObject ).GetProperties().Select( a => a.Name ); + additionalMergeProperties = additionalMergeProperties.Where( a => !dropProperties.Contains( a.Name ) ).ToList(); + } } else { diff --git a/Rock/Web/UI/Controls/Grid/LavaField.cs b/Rock/Web/UI/Controls/Grid/LavaField.cs index 791ee334ddb..756552743ae 100644 --- a/Rock/Web/UI/Controls/Grid/LavaField.cs +++ b/Rock/Web/UI/Controls/Grid/LavaField.cs @@ -243,7 +243,9 @@ private Dictionary ToGridItemsDictionary( GridViewRow gridViewRo /// The data item. private void PopulateDataItemPropertiesDictionary( object dataItem ) { - var dataItemProperties = dataItem.GetType().GetProperties().Where( a => a.GetGetMethod() != null && !a.GetGetMethod().IsVirtual ).ToArray(); + // Exclude indexer properties (e.g. LavaDataObject's "this[string key]"): they cannot be read + // without an index argument, so GetValue( dataItem, null ) on them throws a TargetParameterCountException. + var dataItemProperties = dataItem.GetType().GetProperties().Where( a => a.GetGetMethod() != null && !a.GetGetMethod().IsVirtual && a.GetIndexParameters().Length == 0 ).ToArray(); this.DataItemPropertiesDictionary = new Dictionary(); // add MergeFields based on the associated ColumnHeaderText of each property of the dataitem (without spaces or special chars)