Skip to content

MudDataGrid: Fix nested group expansion for multilevel grouping (#11725)#11729

Merged
danielchalmers merged 3 commits intoMudBlazor:devfrom
M4ntax:fix/MudDataGrid_multilevel_group_expand_fix
Aug 13, 2025
Merged

MudDataGrid: Fix nested group expansion for multilevel grouping (#11725)#11729
danielchalmers merged 3 commits intoMudBlazor:devfrom
M4ntax:fix/MudDataGrid_multilevel_group_expand_fix

Conversation

@M4ntax
Copy link
Contributor

@M4ntax M4ntax commented Jul 28, 2025

This PR fixes an issue with multi-level group expansion in MudDataGrid. Previously, expanding or collapsing a nested group node expanded/collapsed all subgroups with the same key, regardless of the parent group.
Now, the expansion state is tracked by the full group hierarchy—expanding a specific subgroup only affects that particular group node in its parent, not others with the same key in different branches.

This PR also renames the method ToggleGroupExpandAsync to ToggleGroupExpand for clarity, and updates its parameters:

  • The title parameter has been renamed to columnName for better clarity.
  • The GroupDefinition<T> groupDef parameter was removed, since the target column can now be found directly by columnName without needing the selector.

It is also suggested to make the method public. This would make it possible to toggle specific group nodes programmatically, not just via UI buttons. For instance, in our project, we currently have to use reflection to invoke this method because it is not publicly accessible.

There are no visual changes to the UI apart from correct expand/collapse behavior for grouped nodes.

Checklist:

  • The PR is submitted to the correct branch (dev).
  • My code follows the style of this project.
  • I've added relevant tests or tested on existing ones.

Fixes #11725

@M4ntax M4ntax changed the title MudDataGrid: Fix nested group expansion for multilevel grouping #11725 MudDataGrid: Fix nested group expansion for multilevel grouping (#11725) Jul 28, 2025
@codecov
Copy link

codecov bot commented Jul 28, 2025

Codecov Report

❌ Patch coverage is 97.67442% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.31%. Comparing base (86f0de2) to head (c85c901).
⚠️ Report is 46 commits behind head on dev.

Files with missing lines Patch % Lines
...MudBlazor/Components/DataGrid/MudDataGrid.razor.cs 96.55% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #11729      +/-   ##
==========================================
- Coverage   91.35%   91.31%   -0.04%     
==========================================
  Files         466      467       +1     
  Lines       14697    14709      +12     
  Branches     2853     2867      +14     
==========================================
+ Hits        13426    13432       +6     
- Misses        632      635       +3     
- Partials      639      642       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

/// <param name="columnName">The name of the grouped column.</param>
/// <param name="key">The group key identifying the specific group to expand or collapse.</param>
/// <param name="expanded">Whether the group should be expanded (true) or collapsed (false).</param>
public void ToggleGroupExpand(string columnName, object? key, bool expanded)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to expose this method.
Can you maintain the scope internal?

internal is accessible in test.

Copy link
Contributor Author

@M4ntax M4ntax Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback!

As I mentioned in the PR description, just to clarify our use case: in our project, we need to control group expansion programmatically. At the moment, since ToggleGroupExpand is not publicly accessible, we have to rely on reflection to invoke it, which is not ideal or maintainable in the long run. As a MudDataGrid user, I see real value in making this method part of the public API – toggling a specific group node programmatically is a legitimate requirement for many scenarios and should not be considered “internal-only” functionality.

Restricting GroupHierarchyKeysCollection to internal would also cause practical issues for us, as this would effectively block us from being able to use ToggleGroupExpand altogether. If this class is internal, then the GroupDefinition.HierarchyKeys property would also effectively become internal, which would prevent client code from accessing composite keys for advanced scenarios, or passing them to ToggleGroupExpand. This would greatly limit extensibility and force us into more reflection-based workarounds.

Additionally, having GroupHierarchyKeysCollection and ToggleGroupExpand available in the public API would enable advanced scenarios such as determining the precise subgroup within a GroupTemplate, and would provide a better user experience when building the content of a GroupTemplate in DataGridGroupRow, since it can be useful to know exactly which subgroup you are in.

In summary, exposing this type and method makes the library much more flexible for power users. We would greatly appreciate reconsidering making them public.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, thank for the clarification.


#nullable enable

namespace MudBlazor.Components.DataGrid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer flat namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


#nullable enable

namespace MudBlazor.Components.DataGrid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace should be MudBlazor, like the class GroupDefinition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


namespace MudBlazor.Components.DataGrid
{
public class GroupHierarchyKeysCollection(IList<object?> list) : ReadOnlyCollection<object?>(list)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to expose this class.
Can make this new class internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an answer in the comment section above

{
public class GroupHierarchyKeysCollection(IList<object?> list) : ReadOnlyCollection<object?>(list)
{
public string HierarchyPath => string.Join('>', this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this work when the key is of a type that don't override ToString.
A more viable solution would be to store the keys in an array and compare each key one by one.

Copy link
Contributor Author

@M4ntax M4ntax Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the implementation based on your suggestion. GroupHierarchyKeysCollection now overrides Equals and GetHashCode to compare and hash keys element by element. Also added unit tests for GroupHierarchyKeysCollectionto cover these changes.

@github-actions github-actions bot added bug Unexpected behavior or functionality not working as intended refactor Reorganizes code and has no changes to the API or functionality in the main library labels Jul 28, 2025
@M4ntax M4ntax force-pushed the fix/MudDataGrid_multilevel_group_expand_fix branch from 1f76f9c to d643a7d Compare July 29, 2025 12:57
@M4ntax M4ntax requested a review from vernou July 29, 2025 13:06
@versile2
Copy link
Contributor

Since we specifically refer to Row Detail View as hierarchy I feel the naming conventions might need to be adjusted. In addition with a quick overview you did a good job in most spots with public API pieces of adding appropriate XML documentation, but any piece that is to be public needs 100% xml documentation including class. Finally would it not make more sense to make the GroupKey public at this point and instead of passing object? pass GroupKey to the new method?

Overall good work.

@M4ntax
Copy link
Contributor Author

M4ntax commented Jul 29, 2025

@versile2, thank you for your review and suggestions! What do you think about renaming GroupHierarchyKeysCollection to GroupKeyPath? Regarding GroupKey, I'm not sure there is a significant difference for users between passing a GroupKey object or passing the individual parameters to the method. In essence, the user would just need to create the GroupKey instance themselves outside of ToggleGroupExpand. Making GroupKey public would really only make sense if the client needs to have read-only access to the internal Dictionary<GroupKey, bool> _groupExpansionsDict (but honestly, that feels like a separate issue altogether :) ); otherwise, this does not seem necessary in my opinion.

@versile2
Copy link
Contributor

@versile2, thank you for your review and suggestions! What do you think about renaming GroupHierarchyKeysCollection to GroupKeyPath? Regarding GroupKey, I'm not sure there is a significant difference for users between passing a GroupKey object or passing the individual parameters to the method. In essence, the user would just need to create the GroupKey instance themselves outside of ToggleGroupExpand. Making GroupKey public would really only make sense if the client needs to have read-only access to the internal Dictionary<GroupKey, bool> _groupExpansionsDict (but honestly, that feels like a separate issue altogether :) ); otherwise, this does not seem necessary in my opinion.

GroupKeyPath works for me. or GroupingKeysCollection is probably fine.

I misread your code previously and thought you were using the GroupKey in the GroupingKeysCollection. You are correct, no reason to expose it and keep it simple. When you've got all your changes pushed to the PR tag me I'll add a full review.

return true;
}

public override int GetHashCode()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you consider to use HashCode?
The documentation has a example where the hash is computed from a collection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

</MudDataGrid>

@code {
private MudDataGrid<Model> _dataGrid = null!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed unused field

@sonarqubecloud
Copy link

@M4ntax
Copy link
Contributor Author

M4ntax commented Jul 30, 2025

@versile2 Added changes based on your suggestions

@M4ntax M4ntax requested a review from vernou July 30, 2025 06:58
@M4ntax
Copy link
Contributor Author

M4ntax commented Aug 1, 2025

@vernou, I've made the suggested changes. Please let me know if there's anything else I can do.

@versile2 versile2 requested a review from Copilot August 1, 2025 13:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a bug in multi-level group expansion for the MudDataGrid component. Previously, expanding/collapsing a nested group would affect all subgroups with the same key across different parent groups. Now, the expansion state is properly tracked using the full group hierarchy path.

  • Introduces GroupKeyPath class to track the complete path through nested group levels
  • Refactors the ToggleGroupExpandAsync method to ToggleGroupExpand with improved parameters and public visibility
  • Updates group expansion logic to use hierarchical key paths instead of simple keys

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
MudDataGrid.razor.cs Core logic changes for hierarchical group tracking and method refactoring
GroupKeyPath.cs New class representing a read-only collection of group keys forming a unique path
GroupDefinition.cs Adds KeyPath property to track group hierarchy
DataGridGroupRow.razor.cs Updates group expansion call to use new method signature
GroupKeyPathTests.cs Unit tests for the new GroupKeyPath class
DataGridGroupingTests.cs Integration test for multilevel group expansion behavior
DataGridMultilevelGroupingNestedGroupExpansionTest.razor Test component for verifying nested group expansion

// if it has a key we see if it differs from the definition Expanded State and update accordingly
// if it doesn't we add it if the new state doesn't match the definition
var col = RenderedColumns.FirstOrDefault(x => x.GroupBy == groupDef.Selector);
var col = RenderedColumns.FirstOrDefault(x => x.PropertyName == columnName);
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method now searches for columns by PropertyName instead of using the GroupBy selector. This could fail to find the correct column if PropertyName doesn't match the columnName parameter, especially for columns with custom selectors.

Suggested change
var col = RenderedColumns.FirstOrDefault(x => x.PropertyName == columnName);
// Try to find the column by group selector or property name
var col = RenderedColumns.FirstOrDefault(x =>
(x.GroupBy != null && x.GroupBy.ToString() == columnName) ||
x.PropertyName == columnName);

Copilot uses AI. Check for mistakes.
Title = innerGroup.Title,
Parent = newGroupDefinition,
Grouping = innerGroup.Grouping,
KeyPath = new GroupKeyPath(innerGroup.KeyPath),
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new GroupKeyPath from innerGroup.KeyPath assumes KeyPath implements IList<object?>, but it's a ReadOnlyCollection. This will likely cause a runtime error since ReadOnlyCollection doesn't implement IList<object?>.

Suggested change
KeyPath = new GroupKeyPath(innerGroup.KeyPath),
KeyPath = new GroupKeyPath(innerGroup.KeyPath.ToList()),

Copilot uses AI. Check for mistakes.
/// <remarks>
/// Two <see cref="GroupKeyPath"/> instances are equal if they contain the same elements in the same order.
/// </remarks>
public class GroupKeyPath(IList<object?> list) : ReadOnlyCollection<object?>(list)
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor parameter should validate that the input list is not null to prevent runtime exceptions when creating instances.

Copilot uses AI. Check for mistakes.
/// <param name="columnName">The name of the grouped column.</param>
/// <param name="key">The group key identifying the specific group to expand or collapse.</param>
/// <param name="expanded">Whether the group should be expanded (true) or collapsed (false).</param>
public void ToggleGroupExpand(string columnName, object? key, bool expanded)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer this method signature :

/// <summary>
/// Expands or collapses the specified group.
/// </summary>
/// <param name="groupKeys">The group keys identifying the specific group to expand or collapse.</param>
/// <param name="expanded">Whether the group should be expanded (true) or collapsed (false).</param>
public void ExpandGroup(GroupKeyPath groupKeys, bool expanded)

@versile2?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's not so much Expand, or Collapse, or Toggle, It should be SetGroupExpansion or something along those lines. bool expanded feeds how you want it, so it's not truly a toggle. It can expand, and it can collapse though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like SetGroupExpansion.

@vernou
Copy link
Member

vernou commented Aug 3, 2025

@vernou, I've made the suggested changes. Please let me know if there's anything else I can do.

I tested the PR, it's work fine.
Thank for your work.


Just I dislike the new type name GroupKeyPath. Too many word is confusing.
I would prefer the name GroupKeys.

Also this type is just a immutable collection with override HasCode and Equals, so maybe it can be a utility type.
@versile2?

@versile2
Copy link
Contributor

versile2 commented Aug 4, 2025

@vernou, I've made the suggested changes. Please let me know if there's anything else I can do.

I tested the PR, it's work fine. Thank for your work.

Just I dislike the new type name GroupKeyPath. Too many word is confusing. I would prefer the name GroupKeys.

Also this type is just a immutable collection with override HasCode and Equals, so maybe it can be a utility type. @versile2?

I don't have an opinion on the naming here, but there is an internal type GroupKey I'm sure could collide if it's too close. It is also removable although it would take more than a few minutes.

@vernou
Copy link
Member

vernou commented Aug 4, 2025

I prefer GroupKeys, but you can keep the actual name.
@M4ntax, as you want.

@danielchalmers
Copy link
Member

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively resolves an issue with multi-level group expansion in MudDataGrid by introducing a GroupKeyPath to uniquely identify nested groups. The changes are well-structured, including the addition of a new GroupKeyPath class with corresponding unit tests, and a new integration test to verify the fix. The refactoring of ToggleGroupExpandAsync to ToggleGroupExpand and making it public improves the API's clarity and usability. A significant improvement is the deep cloning of InnerGroup definitions, which prevents state corruption across different group branches.

My main feedback concerns a potential bug where a column's Title is used to look up a column by its PropertyName, which could fail if they are different. I've provided a suggestion to make this lookup more robust.

_expanded = !_expanded;
if (Items != null)
DataGrid.ToggleGroupExpandAsync(GroupDefinition.Title, Items.Key, GroupDefinition, _expanded);
DataGrid.ToggleGroupExpand(GroupDefinition.Title, GroupDefinition.KeyPath, _expanded);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using GroupDefinition.Title to identify the column in ToggleGroupExpand can be unreliable, as it's used to look up the column by PropertyName. A column's Title can be different from its PropertyName, which would cause this to fail.

To make this more robust, I suggest passing the PropertyName instead. This requires a few changes:

  1. In src/MudBlazor/Components/DataGrid/GroupDefinition.cs, add a PropertyName property:

    public string PropertyName { get; set; } = string.Empty;
  2. In src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs, populate this new property in ProcessGroup() and when cloning InnerGroup in GetGroupDefinitions():

    // In ProcessGroup()
    return new()
    {
        //...
        PropertyName = column.PropertyName,
        //...
    };
    
    // In GetGroupDefinitions() inside the innerGroup clone
    newGroupDefinition.InnerGroup = new GroupDefinition<T>
    {
        //...
        PropertyName = innerGroup.PropertyName,
        //...
    };
  3. Finally, update this line to use the new property.

                DataGrid.ToggleGroupExpand(GroupDefinition.PropertyName, GroupDefinition.KeyPath, _expanded);

@M4ntax
Copy link
Contributor Author

M4ntax commented Aug 5, 2025

@vernou , I would prefer to keep 'path' in the name because it better reflects the purpose of this class

@danielchalmers danielchalmers merged commit 06ded76 into MudBlazor:dev Aug 13, 2025
6 checks passed
@danielchalmers
Copy link
Member

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Unexpected behavior or functionality not working as intended refactor Reorganizes code and has no changes to the API or functionality in the main library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DataGrid: Multi-level grouping expands all subgroups with same key instead of only targeted subgroup

6 participants