DEV Community

Cover image for Get in Line: Customizing Column Order in MudBlazor
Jane Swingler
Jane Swingler

Posted on

Get in Line: Customizing Column Order in MudBlazor

About MudBlazor

MudBlazor is an open-source Material Design component library for Blazor, Microsoft’s web framework for building interactive web apps in C#. It provides developers pre-built, stylistically consistent UI components, such as buttons, forms, and data grids. This allows developers to focus on application logic rather than writing HTML and CSS from scratch.

Why MudBlazor?

Blazor developers often want the ease and polish of a design framework like React’s Material UI, but within .NET. MudBlazor fills that gap by offering a pure-C# front-end toolkit that integrates directly into existing Blazor projects without JavaScript dependencies.

Who Uses It

Typical users are .NET web developers building things like dashboards and admin panels for webapps and internal business tools. For example, a developer at a logistics company could use a MudDataGrid to display and filter live shipment data in a browser-based portal without writing custom table logic.

A Component in Action

Below you can see the MudStepper component in action on MudBlazor’s documentation site:


MudStepper enables multi-stage workflows such as onboarding forms, checkouts, or application processes, using built-in step navigation and validation

The stepper acts as a wizard that guides users through a sequence of steps, in this example configuring campaign settings, creating an ad group, and finalizing an ad. A developer building an e-commerce checkout could configure this component to lead users through a transaction one stage at a time, allowing them to go back and change details if necessary.

🌍 Community & Reach

 ⭐ GitHub Stars: ~10,000+
 📦 NuGet Downloads: ~20M+
 👩‍💻 Active Contributors: 200+


The Issue: Out of Order (Literally)

I worked on issue #10697, “HierarchyColumn doesn't respect order of placement”.

In MudBlazor’s MudDataGrid, a HierarchyColumn is a special column type that lets you expand a row to show nested content. It’s essentially the toggle that controls the expandable “row detail” view.


Clicking the expand arrow in the HierarchyColumn reveals the detailed view for each element.

This issue pointed out that when using MudDataGrid, the HierarchyColumn always renders as the first column in the grid, no matter where you place it in the markup. In other words, even if a developer defines the HierarchyColumn last (or in the middle) inside the Columns section of a MudDataGrid, it still ends up on the far left in the UI.


One might expect the “expand” column to appear after “Price.” Instead, at runtime the HierarchyColumn renders as the left-most column, even though it’s declared last in the markup.

The behavior described in the issue wasn’t a random bug but the result of explicit logic in MudBlazor’s codebase which hard codes the position of Hierarchy and Select columns. The MudDataGrid component maintains an internal list of columns (RenderedColumns), which it builds by calling the AddColumn() method for each column declared in <Columns><Columns/>. This method checks the column type before inserting it, ensuring special columns are always added at fixed positions. If it’s a HierarchyColumn, it’s always inserted at index 0, meaning it will render first, no matter where it appears in the markup. If it’s a SelectColumn, it’s inserted right after the hierarchy column (index 1 if present, otherwise 0).


AddColumn guarantees HierarchyColumn → index 0, SelectColumn → index 1, others → append

While this is the default behaviour in Mudblazor, some developers pointed out that their designs required placement of hierarchy columns elsewhere:

Developers who use MudDataGrid often want more control over layout. For example, placing the expand arrow beside an “Actions” column rather than far left. Hard-coding these positions made that impossible, limiting design flexibility and causing confusion when markup order wasn’t respected.


The Codebase

Tech Stack

To make the column ordering behavior configurable, I worked within MudBlazor’s existing Blazor-based stack shown below:

Component Lifecycle: How a Column Becomes Visible in MudDataGrid

The workflow below shows the full lifecycle of a column inside MudDataGrid from user markup to browser output. Columns declared in Razor are instantiated as components, register themselves with the parent grid through AddColumn(), and finally render through RenderedColumns in the header and body sections. Step 4, “Order Resolution” highlights that Special columns have hardcoded insertion logic in AddColumn(), which this issue set out to make configurable.

Key Code Locations

The key parts of the codebase related to this issue:

  • MudDataGrid.razor.cs – owns the grid’s column registry (RenderedColumns) and the AddColumn() logic that hard-codes placement (Hierarchy at index 0; Select at 0/1; others appended). This is the source of the “always first” behavior.

  • Column.razor.cs / PropertyColumn.cs / TemplateColumn.cs – base column and its common implementations. Most data-bearing columns derive from Column; they register with the parent grid and participate in rendering.

  • HierarchyColumn.razor.cs / SelectColumn.razor.cs – wrappers that do not derive from Column. They wrap/compose a TemplateColumn for their cell/header content and are handled specially by the grid’s insertion logic.

  • MudDataGrid.razor / DataGridVirtualizeRow.razor – header and row rendering iterate the grid’s column list to produce visible cells.

This logic ultimately affects how the header and body sections loop through RenderedColumns to draw the table.


PR #1 — The UseDeclaredColumnOrder Experiment

My first solution was to introduce a boolean parameter, UseDeclaredColumnOrder in MudDataGrid, the default set to false. I saw the flag as a practical compromise: it preserved backward compatibility while allowing more flexible behavior for those who wanted it. It also kept the change fairly contained, I didn’t have to touch too many other parts of the grid’s internals.

When the flag was set to true, the HierarchyColumn was inserted at its declared position instead of being forced to the front. I applied the same logic to SelectColumn for consistency. It felt reasonable that someone might want a SelectColumn (the column with checkboxes) at the end of a grid too. Implementation-wise, I had to adjust the AddColumn() method to check for the UseDeclaredColumnOrder flag before deciding where to insert each column.

I tested the change visually by running the documentation site locally (Docs.WASM) and confirming the new behavior. I also added a Razor test component and unit test, verified that existing tests still passed, and then opened my first pull request on the issue: MudDataGrid: add UseDeclaredColumnOrder option (Closes #10697)

The maintainers were encouraging, but disliked the idea of adding another flag to the grid, pointing out it would not solve existing custom column ordering issues. One of the maintainers proposed introducing a SortBy (or Order) parameter at the column level. This would let each column define its own display position explicitly, without relying on a global toggle, and that idea became the starting point for my next PR…


PR #2 — Introducing the Order Parameter

Implementing the Order Parameter

I started over, this time implementing the solution described by maintainer @versile2🩷. Following that design, I added a ParameterState Order property and an OrderChanged callback to Column.razor.cs, and then introduced an OrderedColumns parameter in MudDataGrid.razor.cs.
OrderedColumns is a computed list that sorts the grid’s RenderedColumns by each column’s Order value (if specified). Columns without an explicit Order keep their existing default relative priorities,    Hierarchy = 0,    Select = 1,    Others = 2, while ensuring backward compatibility. Because LINQ’s OrderBy is stable, columns sharing the same Order retain their declared order in markup.

internal List<Column<T>> OrderedColumns => RenderedColumns
    .OrderBy(x => x.Order ?? x.Tag switch
    {
        "hierarchy-column" => 0,
        "select-column"    => 1,
        _                  => 2
    })
    .ToList();
Enter fullscreen mode Exit fullscreen mode

The OrderedColumns property computes the final display order dynamically based on each column’s Order value (or default priority)

Cleaning Up the Grid

After getting the basic ordering logic in place, I went through the DataGrid directory to update every reference that still relied on RenderedColumns. Anywhere columns were being iterated for display, I replaced those references with OrderedColumns. This included files responsible for rendering headers, body cells, and virtualized rows.


References to RenderedColumns replaced with OrderedCOlumns where appropriate.

Extending It Beyond the Base Column

The maintainers outline described adding the Order property to Column, but that alone didn’t reach every column type. HierarchyColumn and SelectColumn are separate components that don’t derive/inherit from Column, so updating the base class didn’t touch them. I added Order and OrderChanged directly to those two components, and because they wrap TemplateColumn, that component also needed to accept an Order parameter so the value could be passed through correctly. Although I don’t like duplicating code, this pattern already existed elsewhere in MudBlazor, for example with DragDropEnabled, so I followed it 🫡.

Updated Ordering Flow

Together, these updates created a more flexible ordering system. The diagram below shows how the revised flow replaces the old hard-coded logic with a dynamic ordering pipeline based on OrderedColumns.


Updated workflow showing how the new Order system fits into the existing MudDataGrid lifecycle. AddColumn() now inserts columns in declared order, while OrderedColumns computes final display order dynamically and drives all rendering.

Putting It to the Test

To verify that the new Order parameter worked as expected, I added a Razor test component and a unit test covering three cases:

 1. Default ordering (no Order specified)
 2. Overriding the order of special columns
 3. Promoting a regular column ahead of the default priority columns

All tests passed locally, confirming that explicit ordering overrides were respected and that default behavior remained unchanged.


Demo


In the video above I demo the new Order property. We have a MudDataGrid where columns are declared in order: Product, Order Date, Order #, Quantity, Total, SelectColumn, HierarchyColumn.

Initial State:

HierarchyColumn appears first (Order=0), SelectColumn second (Order=1), and others follow (Order=2).
Step 1: Setting Order=3 on HierarchyColumn moves it to the last position.
Step 2: Setting Order=0 on Order # moves it ahead of SelectColumn.
Step 3: Setting Order=-1 on SelectColumn moves it back to the first position.

Huzzah!🪄✨


Challenge: Drag&Drop, Meet OrderedColumns 🤝

A key challenge was figuring out how the new Order parameter would interact with MudDataGrid’s existing drag-and-drop reordering. Both features change column positions, but in different ways, so I needed to see how they lined up or conflicted in practice.

To fully understand how my change interacted with DragAndDrop, I wrote unit tests and ran them in debug mode, inspecting how RenderedColumns and OrderedColumns changed during drag-and-drop to trace where the logic split. I also cloned the TryMudBlazor site and ran it with my local MudBlazor build so I could test behavior live and root out all edge cases.

I identified that because OrderedColumns is a computed property based on RenderedColumns, after DragAndDrop updates the RenderedColumns order, OrderedColumns immediately re-sorts them which overrides the drag result. Since all rendering now references OrderedColumns, the visual order always resets to the computed one.

Once I understood this, it was clear that the Order parameter, at least as designed, couldn’t fully coexist with DragAndDrop without deciding whether DragAndDrop should update Order values dynamically or be disabled for columns with fixed Order values. The best next step I could see was to open a draft PR summarizing the issue, showing my progress, and asking the community for input. Check it out here...


My Draft PR


Until Next Commit 🚀

Working on this issue has taught me a lot about debugging interactions between features in a large library. It was an iterative process of testing and tracing until the behavior made sense in every scenario. For now, I’m refining the implementation and discussing next steps with maintainers before finalizing the drag-and-drop behavior…watch this space👀

Top comments (0)