TL;DR: Learn how to build a modern, responsive Student Enrollment Dashboard in .NET MAUI using Syncfusion Charts, Gauges, and Inputs. Visualize KPIs like admission count, gender distribution, program breakdown, and regional student mix with interactive charts.
Welcome to our Chart of the Week series!
Universities deal with thousands of rows of enrollment data, but spreadsheets rarely tell the full story. That’s where dashboards shine, turning raw numbers into actionable insights.
In this guide, we’ll build a Student Enrollment Dashboard in .NET MAUI using Syncfusion® .NET MAUI Charts. The goal? Transform admissions data into a responsive, visually engaging experience that works across devices.
Every tile in the dashboard offers a unique perspective on enrollment health, filled seats, gender diversity, program breakdowns, and international reach. Admissions teams can track progress, identify trends, and make data-driven decisions instantly. It includes:
The core goals are:

Begin by defining the classes that structure your data and manage business logic. These models form the backbone of the dashboard, ensuring that well-organized data drives each visualization.
Refer to the code example below.
public class DegreeModel
{
// Represents a single academic degree (e.g., Medical, Engineering, Arts & Science).
// Used to drive the degree filter, admission KPI gauge, notice banner, gender chart, and region chart.
}
public class CountryStat
{
// Holds information about student counts by country or region.
// Drives the Students by Country/Region column chart visualization.
}
public class ProgramType
{
// Represents a specific program type under a course.
// Used in the Sunburst chart to show distribution of students by program type.
}
public class CourseModel
{
// Represents a course under a degree (e.g., AI, Civil, Neuro).
// Drives the middle ring of the Sunburst chart and aggregates program type data.
}
public class GenderStat
{
// Holds gender distribution data (e.g., Male, Female, Other).
// Drives the Gender Distribution doughnut chart visualization.
}
public class SunburstNode
{
// Represents hierarchical data for the Sunburst chart:
// Degree → Course → ProgramType with associated student count.
}
Next, create the EnrollmentViewModel, which binds these models to the UI.
public partial class EnrollmentViewModel
{
// Selected degree drives all dashboard tiles
public DegreeModel? SelectedDegree {get; set;}
// KPI Gauge bindings
public int GaugeTotal {get; set;}
public double GaugeShapePointer {get; set;}
public string AdmittedSeatsByDegree {get; set;}
public int RemainingVacancies {get; set;}
// Notice banner deadline
public string ApplicationCloseText {get; set;}
// Collections bound to UI
public ObservableCollection<DegreeModel> Degrees { get; }
public IList<SunburstNode> SunburstData { get; }
public List<Brush> CustomBrushes { get; set; }
public IList<GenderStat> GenderStats => SelectedDegree?.Gender ?? new List<GenderStat>();
public IList<CountryStat> CountryStats => SelectedDegree?.Countries ?? new List<CountryStat>();
// Constructor initializes static data and palettes
public EnrollmentViewModel()
{
Degrees = new ObservableCollection<DegreeModel>(CreateStaticDegrees());
SelectedDegree = Degrees.First(d => d.Name == "Medical");
SunburstData = BuildSunburst(Degrees);
}
private IEnumerable<DegreeModel> CreateStaticDegrees()
{
// Generates the static dataset of degree, course, gender, and country stats for the dashboard.
}
private IList<SunburstNode> BuildSunburst(IEnumerable<DegreeModel> degrees)
{
// Builds hierarchical nodes (Degree → Course → ProgramType) for the Sunburst chart visualization.
}
}
Note: You can view the complete code implementation in the Model and EnrollmentViewModel classes.
Once the data structure is ready, the next step is to design the dashboard layout. A clean and responsive layout ensures that insights are easy to interpret.
We’ll use a MainGrid divided into three rows:
Here’s how you can do it in XAML code:
<!--MainGrid-->
<Grid x:Name="MainGrid" RowDefinitions="{OnPlatform Default='0.6*,4.2*,5*', Android='2*,440,5*', iOS='2*,440,5*'}" >
<Grid Grid.Row="1" ColumnDefinitions="{OnPlatform Default='*,auto', Android='*',iOS='*'}" RowDefinitions="{OnPlatform Default='*',Android='*,*',iOS='*,*'}">
<!--Title-->
<Grid ColumnDefinitions="Auto,*" >
…
</Grid>
<!--ComboBox-->
<Border Grid.Row="1" Grid.Column="1">
…
</Border>
</Grid>
<!--AdmissionData, NoticeData, GenderData-->
<Grid Grid.Row="1" ColumnDefinitions="{OnPlatform Default='*',WinUI='*,*',MacCatalyst='*,*'}" >
<Grid Grid.Column="0" RowDefinitions="{OnPlatform Default='*', Android='*,*',iOS='*,*'}" ColumnDefinitions="{OnPlatform Default='*', WinUI='6*,4*', MacCatalyst='6*,4*'}">
…
</Grid>
</Grid>
<!--OverallData, RegionData-->
<Grid Grid.Row="2" ColumnDefinitions="{OnPlatform Default='*',WinUI='*,*',MacCatalyst='*,*'}" RowDefinitions="{OnPlatform Default='*', Android='*,*',iOS='*,*'}">
…
</Grid>
</Grid>
After setting up the layout, focus on the header section. This area establishes the dashboard’s identity and provides an interactive degree filter.
Using SfComboBox, users can quickly switch between programs such as Medical, Engineering, and Arts. Each selection triggers dynamic updates across all charts, ensuring real-time insights.
Now that users can filter by degree, it’s time to visualize seat occupancy. The KPI gauge answers the critical question: How many seats are filled?
Powered by SfRadialGauge, this component delivers an intuitive radial chart with smooth animations and bold annotations. As the needle moves, users instantly see progress toward enrollment goals.
Key features:
Here’s the complete XAML code block.
<Border>
<Grid RowDefinitions="Auto,*">
<Label Text="Admission Count Details"/>
<sfGauge:SfRadialGauge Grid.Row="1">
<sfGauge:SfRadialGauge.Axes>
<sfGauge:RadialAxis Minimum="0" Maximum="{Binding SelectedDegree.TotalSeats}"/>
<sfGauge:RadialAxis.AxisLineStyle>
<sfGauge:RadialLineStyle CornerStyle="BothCurve"/>
</sfGauge:RadialAxis.AxisLineStyle>
…
<sfGauge:RadialAxis.Annotations>
<sfGauge:GaugeAnnotation>
<sfGauge:GaugeAnnotation.Content>
...
</sfGauge:GaugeAnnotation.Content>
</sfGauge:GaugeAnnotation>
</sfGauge:RadialAxis.Annotations>
<sfGauge:RadialAxis.Pointers>
…
</sfGauge:RadialAxis.Pointers>
</sfGauge:RadialAxis>
</sfGauge:SfRadialGauge.Axes>
</sfGauge:SfRadialGauge>
</Grid>
</Border>
Here’s what the Radial Gauge looks like:

Numbers alone don’t tell the full story. To provide context, add a notice banner that highlights seat availability and application deadlines. This card-style component keeps admissions teams informed and focused on key dates.
<Border x:Name="Banner" >
<Grid VerticalOptions="{OnPlatform Default=Fill, MacCatalyst=Center}" RowDefinitions="auto,*" RowSpacing="{OnPlatform Default=14, MacCatalyst=18}" Padding="{OnPlatform Default=20, MacCatalyst=20 0 20 20}">
<Image Source="graduate.png" HorizontalOptions="Start" VerticalOptions="Start" WidthRequest="60" HeightRequest="60"/>
<VerticalStackLayout Grid.Row="1" Spacing="{OnPlatform Default=10, MacCatalyst=14}">
<Label Text="{Binding SelectedDegree.Name, StringFormat='Degree: {0}'}" FontAttributes="Bold" FontSize="18" TextColor="#610097"/>
<Label Text="{Binding RemainingVacancies, StringFormat='Available seats {0}'}" FontSize="16" TextColor="#1F2937"/>
<Label Text="{Binding ApplicationCloseText}" FontSize="13" TextColor="#6B7280" LineBreakMode="WordWrap"/>
</VerticalStackLayout>
</Grid>
</Border>
Here’s the expected output:

Now, showcase diversity using a gender distribution chart. Built with SfCircularChart and DoughnutSeries, this visualization updates dynamically based on the selected degree.
This chart highlights,
Here’s a code snippet to achieve this.
<Border>
<Grid>
<sfCharts:SfCircularChart>
<sfCharts:DoughnutSeries ItemsSource="{Binding GenderStats}" StartAngle=" EndAngle="360" XBindingPath="Category" YBindingPath="Count"/>
</sfCharts:SfCircularChart>
</Grid>
</Border>
After running the code, you’ll see this.
After showcasing gender diversity, the next step is to explore the overall enrollment structure. To achieve this, we’ll use a Sunburst Chart, which provides an interactive and visually appealing way to represent hierarchical data.
This chart illustrates enrollment across various degrees, courses, and program types, enabling teams to quickly identify trends and popular programs. Think of it as a hierarchical tree reimagined as concentric rings:
With labels and interactive tooltips, users can drill down into the data to uncover which programs attract the most students and pinpoint areas with the strongest enrollment. This visualization makes complex relationships easy to interpret at a glance.
Below is the code you need to implement this feature.
<Border>
<sfSunburst:SfSunburstChart ItemsSource="{Binding SunburstData}" ValueMemberPath="StudentCount" >
<sfSunburst:SfSunburstChart.Levels>
<sfSunburst:SunburstHierarchicalLevel GroupMemberPath="DegreeName"/>
<sfSunburst:SunburstHierarchicalLevel GroupMemberPath="CourseName"/>
<sfSunburst:SunburstHierarchicalLevel GroupMemberPath="ProgramType"/>
</sfSunburst:SfSunburstChart.Levels>
</sfSunburst:SfSunburstChart>
</Border>
The following image shows the Sunburst Chart.

Finally, let’s highlight geographic diversity in enrollment. The Students by Country/Region tile provides a clear view of how student numbers vary across different countries. Each column represents a country, and its height reflects the number of students enrolled, making comparisons quick and intuitive.
This visualization is implemented using SfCartesianChart with a ColumnSeries, which binds directly to country-level statistics. As users switch between degrees, the chart updates dynamically to reflect the latest data. Rounded corners and smooth animated transitions give the chart a modern, polished look that feels approachable and professional.
Below is the code snippet you’ll need to create this feature:
<Border>
<sfCharts:SfCartesianChart>
<sfCharts:SfCartesianChart.XAxes>
<sfCharts:CategoryAxis>
</sfCharts:CategoryAxis>
</sfCharts:SfCartesianChart.XAxes>
<sfCharts:SfCartesianChart.YAxes>
<sfCharts:NumericalAxis>
</sfCharts:NumericalAxis>
</sfCharts:SfCartesianChart.YAxes>
<sfCharts:ColumnSeries ItemsSource="{Binding CountryStats}" XBindingPath="Country" YBindingPath="Count"/>
</sfCharts:SfCartesianChart>
</Border>

Implement these steps, and you’ll have a fully functional Student Enrollment Dashboard in .NET MAUI, as shown below.

For more details, refer to the .NET MAUI Student Enrollment dashboards sample on GitHub.
Thank you for reading! In this guide, we explored how to create a Student Enrollment Dashboard using Syncfusion .NET MAUI Charts.
If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.
You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!
Last time, we noted that the std::rotate function requires only forward iterators. The algorithm we explored last time involved a clever sequence of reversals, but reversals require bidirectional iterators (one to iterator forward from the beginning and another to iterate backward from the end). How do you do it when you can only iterate forward?
Let’s set up the problem again. Suppose we want to swap these two blocks A and B.
| A | B | |
| ↑ | ↑ | ↑ |
| first | mid | last |
We can early-out in the vacuous cases where either the A or B block is empty. For those cases, we can return without doing anything.
We start by swapping the elements at first and mid, then incrementing both pointers and repeating. Here’s what it looks after swapping a handful of items:
| B1 | A2 | A1 | B2 | |
| ↑ | ↑ | ↑ | ||
| first | mid | last |
Let’s first take the case where the A block is bigger than the B block. In that case, the entire B block gets swapped to the start of the buffer, and a B-sized chunk of the first part of the A block (let’s call it A1) got swapped to the end of the buffer. There’s still a leftover chunk of the A block, call it A2, that hasn’t moved yet.
| B | A2 | A1 | |
| ↑ | ↑ | ||
| first | mid last |
Okay, so the B block is in its final position, but the A1 and A2 blocks need to be swapped. Hey, I know how to swap two adjacent blocks: I call std::rotate on myself recursively! Since this is a tail recursive call, I can just move mid back to its original position at the start of the function and restart the algorithm.
| B | A2 | A1 | ||
| ↑ | ↑ | ↑ | ||
| first | mid | last | ||
| ⤩ | recursive rotate to swap A1 and A2 | |||
| B | A1 | A2 | ||
The other case is where the A block is smaller than the B block.
| A | B | |
| ↑ | ↑ | ↑ |
| first | mid | last |
In this case, we swap until we have used up all of the A elements. The entire A block gets swapped to the middle of the buffer, and an A-sized chunk of the start of the B block (call it B1) goes to the start of the buffer, with the leftover part of B (call it B2) stays at the end of the buffer.
| B1 | A | B2 | |
| ↑ | ↑ | ↑ | |
| first | mid | last |
Again, we can finish with a recursive rotate call, this time to swap A and B2.
| B1 | A | B2 | ||
| ⤩ | recursive rotate to swap A and B2 | |||
| B1 | B2 | A | ||
In both cases, the recursive call is strictly smaller than the original call because we know that both A and B are nonempty: If either was empty, we would have performed a nop early-exit. Therefore, at least one element will be swapped before we reach the recursive call, and we consequently know that the recursion will eventually terminate.
The final case is where the A and B blocks are the same size. In that case, we have swapped all the B elements to the first half and all the A elements to the second half.
| B | A | |
| ↑ | ↑ | |
| first | mid last |
If we are lucky to get here, then we are done!
Note that we don’t need a special case handler for the equal-sized blocks case. We can treat as either the first or second case and let the early-out in the recursive call realize that there is nothing to do.
The number of swaps is n. You can calculate this recursively, since we perform k swaps to move the smaller block, and the recursive call (by induction) performs n − k swaps, for a total of n. You can also calculate this directly by observing that at each step, one element gets swapped into its final position (namely, at *first, just before we increment it).
Even though this algorithm and the bidirectional-iterator version both perform n swaps, the bidirectional-iterator version has better locality of reference, so it is preferred if available.
Next time, we’ll apply this to our original problem.
Bonus chatter: You don’t need to do a preliminary std::distance to figure out whether block A or block B is smaller. You can just start swapping elements, and make a note of which happens first: Does first reach the original mid (which means that the A block is smaller) or does mid reach last (which means that the B block is smaller). This “figure it out as you go” doesn’t change the complexity, but it does lower the constant a little bit because you don’t have to iterate through all of the larger block just to realize that it’s took big.
The post How can you swap two adjacent blocks of memory using only forward iterators? appeared first on The Old New Thing.
TL;DR: Struggling with cluttered grids? Row and column spanning in Blazor DataGrid solves this by merging cells intelligently. Learn how to enable AutoSpan, handle merge/unmerge operations, and apply best practices for performance and usability.
Modern web apps demand clean, readable tables. When the same value repeats across rows or columns, grids can look cluttered and hard to scan. Syncfusion’s Blazor DataGrid solves this with row and column spanning, automatically merging duplicate values into a single cell. The result? A cleaner, more user-friendly table, without extra coding.
This feature has been introduced as part of the Syncfusion® 2025 Volume 4 release, which brings exciting updates across the suite.
In this guide, you’ll learn:
Don’t worry if you’re new to this; everything is explained in a simple, easy-to-understand way.
Row and Column spanning is enabled by using the AutoSpan property on your SfGrid tag. When adjacent cells share the same value, the grid merges them into a single cell.
The table below lists the available AutoSpanMode options:
| Settings | Description |
| AutoSpanMode.None | No merging(default). |
| AutoSpanMode.Row | Merges matching cells horizontally in the same row. |
| AutoSpanMode.Column | Merges matching cells vertically in the same column. |
| AutoSpanMode.HorizontalAndVertical | Combines both row and column spans (rows first, then columns). |
Here are some example use cases:
Row spanning merges identical values across multiple columns in the same row. For example, if Evening News appears twice in a row, the grid combines them into one wider cell.
Here’s how you can do it in code:
@using Syncfusion.Blazor.Grids
<SfGrid DataSource="@TeleCastDataList"
GridLines="GridLine.Both"
AutoSpan="AutoSpanMode.Row"
AllowSelection="false"
EnableHover="false">
<GridColumns>
<GridColumn Field=@nameof(TelecastData.Channel) HeaderText="Channel" Width="200" IsFrozen="true"></GridColumn>
<GridColumn Field=@nameof(TelecastData.Genre) HeaderText="Genre" Width="120" IsFrozen="true"></GridColumn>
<GridColumn Field=@nameof(TelecastData.Program12AM) HeaderText="12:00 AM" Width="150"></GridColumn>
<!-- ... other time slot columns ... -->
</GridColumns>
</SfGrid>
Note: Try it out with the row spanning demo and refer to the documentation for more insights.
Watch how the feature works in action:

Column spanning merges identical values vertically across consecutive rows. For example, lunch might cover multiple hourly slots for one employee.
Code snippet to achieve this:
@using Syncfusion.Blazor.Grids
<SfGrid DataSource="@EmployeeTimeSheet"
AutoSpan="AutoSpanMode.Column">
<GridColumns>
<!-- ... column definitions ... -->
</GridColumns>
</SfGrid>
Matching entries in a column are displayed in a taller cell, allowing you to see the pattern, as shown below.

Note: Try it out with the column spanning demo and refer to the documentation for more information.
Need spanning everywhere except one column? For example, if you don’t want prime-time TV slots to merge, set the AutoSpan property to AutoSpanMode.None in that column to override the grid settings.
<GridColumn Field=@nameof(TelecastData.Program8PM)
HeaderText="8:00 PM"
AutoSpan="AutoSpanMode.None">
</GridColumn>
Note: For more insights, refer to the documentation on disabling row and column spanning.
Auto spanning is great, but sometimes you need manual control. Syncfusion provides the following methods for custom merging:
Here’s a basic code example with buttons to merge and unmerge single or multiple cells:
@using Syncfusion.Blazor.Grids
<SfButton OnClick="MergeCellsAsync">Merge Cell</SfButton>
<SfButton OnClick="UnMergeCell">UnMerge Cell</SfButton>
<SfButton OnClick="MergeMultipleCellsAsync">Merge Multiple Cells</SfButton>
<SfButton OnClick="UnMergeCells">UnMerge Multiple Cells</SfButton>
<SfButton OnClick="UnMergeAllCells">UnMerge All Cells</SfButton>
<SfGrid @ref="Grid" DataSource="@EmployeeTimeSheet" GridLines="GridLine.Both" AllowSelection="false" EnableHover="false">
<GridColumns>
<GridColumn Field=@nameof(EmployeeDetails.EmployeeID) HeaderText="Employee ID" Width="150" TextAlign="TextAlign.Right" IsPrimaryKey="true" IsFrozen="true"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.EmployeeName) HeaderText="Employee Name" Width="180" IsFrozen="true"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_9_00) HeaderText="9:00 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_9_30) HeaderText="9:30 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_10_00) HeaderText="10:00 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_10_30) HeaderText="10:30 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_11_00) HeaderText="11:00 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_11_30) HeaderText="11:30 AM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_12_00) HeaderText="12:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_12_30) HeaderText="12:30 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_1_00) HeaderText="1:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_1_30) HeaderText="1:30 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_2_00) HeaderText="2:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_2_30) HeaderText="2:30 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_3_00) HeaderText="3:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_3_30) HeaderText="3:30 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_4_00) HeaderText="4:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_4_30) HeaderText="4:30 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
<GridColumn Field=@nameof(EmployeeDetails.Time_5_00) HeaderText="5:00 PM" Width="150" TextAlign="TextAlign.Center"></GridColumn>
</GridColumns>
</SfGrid>
@code
{
public List<EmployeeDetails>? EmployeeTimeSheet { get; set; }
public SfGrid<EmployeeDetails>? Grid;
protected override void OnInitialized()
{
EmployeeTimeSheet = EmployeeDetails.GetAllRecords();
}
public async Task MergeCellsAsync()
{
await Grid.MergeCellsAsync(new MergeCellInfo
{
RowIndex = 1,
ColumnIndex = 5,
ColumnSpan = 2,
});
}
public async Task UnMergeCell()
{
await Grid.UnmergeCellsAsync(new UnmergeCellInfo
{
RowIndex = 1,
ColumnIndex = 5,
});
}
public async Task MergeMultipleCellsAsync()
{
await Grid.MergeCellsAsync(new[]
{
new MergeCellInfo { RowIndex = 0, ColumnIndex = 2, ColumnSpan = 2 },
new MergeCellInfo { RowIndex = 5, ColumnIndex = 3, ColumnSpan = 3 },
new MergeCellInfo { RowIndex = 7, ColumnIndex = 4, ColumnSpan = 2 }
});
}
public async Task UnMergeCells()
{
await Grid.UnmergeCellsAsync(new[]
{
new UnmergeCellInfo { RowIndex = 0, ColumnIndex = 2 },
new UnmergeCellInfo { RowIndex = 5, ColumnIndex = 3 },
new UnmergeCellInfo { RowIndex = 7, ColumnIndex = 4 }
});
}
public async Task UnMergeAllCells()
{
await Grid.UnmergeAllAsync();
}
}
Before you implement row and column spanning, it’s important to understand a few constraints and best practices to ensure smooth functionality:
This feature has limitations with:
Here are some quick tips to optimize your grid:
Thank you for reading! Row and column spanning in Syncfusion Blazor DataGrid, introduced in the Essential Studio® 2025 Volume 4 release, helps you create cleaner, more readable tables with just a few lines of code. It’s perfect for schedules, lists, or any data with repeats. Try it out in the demos.
Check out our Release Notes and What’s New pages to see the other updates in this release, and leave your feedback in the comments section below. We would love to hear from you.
If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.
You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!