KMVUnion.Grid
1.3.0
dotnet add package KMVUnion.Grid --version 1.3.0
NuGet\Install-Package KMVUnion.Grid -Version 1.3.0
<PackageReference Include="KMVUnion.Grid" Version="1.3.0" />
<PackageVersion Include="KMVUnion.Grid" Version="1.3.0" />
<PackageReference Include="KMVUnion.Grid" />
paket add KMVUnion.Grid --version 1.3.0
#r "nuget: KMVUnion.Grid, 1.3.0"
#:package KMVUnion.Grid@1.3.0
#addin nuget:?package=KMVUnion.Grid&version=1.3.0
#tool nuget:?package=KMVUnion.Grid&version=1.3.0
KMVUnion.Grid
A high-performance, feature-rich DataGrid control for .NET MAUI (.NET 10+).
Supports in-memory collections and async/server-side data sources with pagination, multi-column sorting, global search, per-column filters, inline editing, multi-row selection, grouping, CSV export, clipboard copy, conditional styling, summary aggregates, and fully custom DataTemplate cells.
Platforms: Android - iOS - macCatalyst - Windows
Table of Contents
- Requirements
- Installation & Setup
- Quick Start
- Column Types
- Column Base Properties
- Data Sources
- Sorting
- Global Search
- Column Filters
- Pagination
- Row Selection
- Inline Row Editing
- Row Grouping
- CSV Export & Share
- Clipboard Copy
- Conditional Row Styling
- Summary Row
- Appearance & Theming
- Commands & Events
- Page Load Hooks
- License
Requirements
- .NET MAUI targeting .NET 10 or later
- Platforms: Android 5.0+, iOS 15+, macOS 15+ (Mac Catalyst), Windows 10 1809+
Installation & Setup
dotnet add package KMVUnion.Grid
Register the library in MauiProgram.cs:
using KMVUnion.Grid;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseDataGridMaui(); // registers Material Icons font + default theme
return builder.Build();
}
}
Add XML namespaces to any XAML page:
xmlns:dg="clr-namespace:KMVUnion.Grid.Controls;assembly=KMVUnion.Grid"
xmlns:col="clr-namespace:KMVUnion.Grid.Columns;assembly=KMVUnion.Grid"
Quick Start
<dg:DataGridControl
ItemsSource="{Binding Employees}"
ShowSearchBar="True"
ShowPagination="True"
PageSize="25">
<dg:DataGridControl.Columns>
<col:RowNumberColumn />
<col:TextColumn Header="Name" Binding="{Binding Name}" SortPropertyName="Name" />
<col:TextColumn Header="Department" Binding="{Binding Department}" SortPropertyName="Department" />
<col:NumberColumn Header="Salary" Binding="{Binding Salary}" SortPropertyName="Salary" Format="N0" />
<col:BooleanColumn Header="Active" Binding="{Binding IsActive}" />
</dg:DataGridControl.Columns>
</dg:DataGridControl>
ObservableCollection<T> is fully supported - adding or removing items triggers an automatic grid refresh.
Column Types
| Type | Description |
|---|---|
TextColumn |
String values; participates in global search |
NumberColumn |
Numeric values with optional .NET format strings; right-aligned by default |
BooleanColumn |
True/false with customisable TrueLabel / FalseLabel (defaults: ✓ / ✗) |
DateTimeColumn |
DateTime / DateTimeOffset with configurable Format; date picker in edit mode |
UrlColumn |
Clickable hyperlink; opens in the system browser; tracks visited state per session |
ProgressBarColumn |
Progress bar visualization (0–1) with individual and relative (row-relative-to-total) modes; customizable colors and height |
RowNumberColumn |
Auto-generated row numbers, pagination-aware; excluded from CSV export |
TemplateColumn |
Fully custom DataTemplate cell - buttons, images, checkboxes, badges, etc. |
DateTimeColumn
<col:DateTimeColumn Header="Hired" Binding="{Binding HireDate}" Format="dd.MM.yyyy" SortPropertyName="HireDate" />
UrlColumn
<col:UrlColumn Header="Website" Binding="{Binding WebsiteUrl}" DisplayTextBinding="{Binding WebsiteLabel}" />
ProgressBarColumn
Displays a customizable progress bar (0–1). Supports two modes:
- Individual - each row shows its own progress value.
- Relative - each row's value is shown relative to the sum of all values in the column.
<col:ProgressBarColumn
Header="Progress"
Binding="{Binding ProgressValue}"
Mode="Individual"
BarColor="Green"
TrackColor="LightGray"
BarHeight="8"
ShowLabel="True" />
<col:ProgressBarColumn
Header="Relative Share"
Binding="{Binding SalesAmount}"
Mode="Relative"
AllRows="{Binding AllEmployees}"
BarColor="Blue"
TrackColor="LightGray"
BarHeight="12"
ShowLabel="True"
LabelColor="Black" />
| Property | Type | Default | Description |
|---|---|---|---|
Binding |
BindingBase? |
null |
Source property for the progress value (0–1 or raw value in relative mode) |
Mode |
ProgressMode |
Individual |
Individual or Relative |
AllRows |
BindingBase? |
null |
In relative mode, a binding to the full row collection (for computing the total) |
BarColor |
Color |
Green |
Progress bar fill color |
TrackColor |
Color |
LightGray |
Progress bar background/track color |
BarHeight |
double |
8 |
Progress bar height in DIPs |
ShowLabel |
bool |
false |
Display percentage label inside the progress bar |
LabelColor |
Color |
Black |
Percentage label text color |
TemplateColumn
<col:TemplateColumn Header="Actions" Width="120" IsSortable="False">
<col:TemplateColumn.CellTemplate>
<DataTemplate>
<HorizontalStackLayout Spacing="4" HorizontalOptions="Center">
<Button Text="Edit" Command="{Binding ...}" CommandParameter="{Binding .}" />
<Button Text="Delete" Command="{Binding ...}" CommandParameter="{Binding .}" />
</HorizontalStackLayout>
</DataTemplate>
</col:TemplateColumn.CellTemplate>
</col:TemplateColumn>
Column Base Properties
All column types inherit these properties from ColumnBase:
| Property | Type | Default | Description |
|---|---|---|---|
Header |
string |
"" |
Column header text |
HeaderTemplate |
DataTemplate? |
null |
Custom header cell template |
CellTemplate |
DataTemplate? |
null |
Overrides the default cell renderer |
Width |
GridLength |
* |
Fixed, auto, or proportional (2*) |
MinWidth |
double |
40 |
Minimum width in DIPs |
MaxWidth |
double |
inf | Maximum width in DIPs |
AutoWidth |
bool |
true |
Measures widest content and sizes column automatically |
IsVisible |
bool |
true |
Show / hide the entire column |
IsSortable |
bool |
true |
Allow user sorting |
SortPropertyName |
string? |
null |
Model property name used for sorting |
IsFilterable |
bool |
false |
Show filter icon in the header |
FilterPropertyName |
string? |
null |
Property for filter values (falls back to SortPropertyName) |
IsEditable |
bool |
false |
Allow inline editing for this column |
HorizontalContentAlignment |
LayoutOptions |
Start |
Horizontal cell content alignment |
VerticalContentAlignment |
LayoutOptions |
Center |
Vertical cell content alignment |
HeaderHorizontalAlignment |
HeaderTextAlignment |
Center |
Header text alignment (Inherit, Start, Center, End) |
SummaryHorizontalAlignment |
LayoutOptions? |
null |
Override alignment in the summary row cell |
Summary |
SummaryAggregate? |
null |
Count, Sum, Min, Max, or Avg |
Data Sources
In-Memory (ItemsSource)
Bind any IEnumerable. The grid wraps it in InMemoryDataGridSource<T> which handles sorting, filtering, and pagination via LINQ.
<dg:DataGridControl ItemsSource="{Binding Employees}" />
Async / Server-Side (DataSource)
Implement IDataGridSource<T> for server-side loading, virtualization, or complex data pipelines.
<dg:DataGridControl DataSource="{Binding MyServerSource}" />
ItemsSourceandDataSourceare mutually exclusive - setting one clears the other.
Sorting
Tapping a sortable column header cycles Ascending > Descending > No Sort. Hold Shift (desktop) to add additional sort levels (multi-column sort with priority indicators).
<col:TextColumn Header="Name" Binding="{Binding Name}" SortPropertyName="Name" IsSortable="True" />
myGrid.SortChanged += (sender, descriptor) =>
Console.WriteLine($"Sorted by {descriptor.PropertyName} {descriptor.Direction}");
Global Search
Filters rows across all searchable TextColumn columns (debounced 300 ms).
<dg:DataGridControl
ShowSearchBar="True"
SearchPlaceholder="Search employees..."
SearchText="{Binding SearchQuery, Mode=TwoWay}"
SearchColumns="Name,Department"
... />
Exclude a column from search: <col:TextColumn IsSearchable="False" ... />
Column Filters
Per-column dropdown with two tabs:
- Values - Excel-style checkbox list of distinct values.
- Range - Numeric greater/less-than fields or DateTime From/To pickers.
Multiple column filters combine with AND logic; values within InSet use OR logic.
<col:TextColumn
Header="Department"
Binding="{Binding Department}"
SortPropertyName="Department"
IsFilterable="True" />
Available filter operations: Equals, Contains, StartsWith, EndsWith, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, IsNull, IsNotNull, InSet.
Pagination
<dg:DataGridControl
ShowPagination="True"
PageSize="25"
PageSizeOptions="10,25,50,100"
ShowPageSizeSelector="True"
ShowRangeLabel="True"
RangeLabelFormat="Showing {0}-{1} of {2} records"
CurrentPage="{Binding CurrentPage, Mode=TwoWay}"
TotalPages="{Binding TotalPages}"
TotalCount="{Binding TotalCount}"
... />
| Property | Type | Default | Description |
|---|---|---|---|
PageSize |
int |
25 |
Rows per page |
CurrentPage |
int |
1 |
Current page (two-way bindable) |
TotalPages |
int |
- | Total page count (read-only) |
TotalCount |
int |
- | Total filtered record count (read-only) |
ShowPageSizeSelector |
bool |
true |
Show page size buttons |
PageSizeOptions |
string |
"10,25,100" |
Comma-separated page sizes (max 5) |
Row Selection
<dg:DataGridControl
SelectionMode="Single"
SelectedItem="{Binding SelectedEmployee, Mode=TwoWay}"
SelectionColor="#BFDBFE" />
<dg:DataGridControl
SelectionMode="Multiple"
SelectedItems="{Binding SelectedEmployees, Mode=TwoWay}" />
Inline Row Editing
Double-tap a row to enter edit mode. Confirm with checkmark / Enter, cancel with X / Escape.
<dg:DataGridControl
IsEditable="True"
RowCommittedCommand="{Binding SaveCommand}"
RowCancelledCommand="{Binding RevertCommand}" />
Control per-column editing with IsEditable on individual columns:
<col:TextColumn Header="Name" Binding="{Binding Name}" IsEditable="True" />
<col:NumberColumn Header="ID" Binding="{Binding Id}" IsEditable="False" />
Row Grouping
<dg:DataGridControl GroupByProperty="Department" ... >
<dg:DataGridControl.GroupHeaderTemplate>
<DataTemplate>
<Label Text="{Binding KeyDisplay}" FontAttributes="Bold" Padding="8,4" />
</DataTemplate>
</dg:DataGridControl.GroupHeaderTemplate>
</dg:DataGridControl>
CSV Export & Share
<dg:DataGridControl
ShowExportButton="True"
ShowShareButton="True"
ExportCommand="{Binding CustomExportCommand}" />
- Export - saves filtered/sorted data as RFC 4180 CSV to the platform Downloads/Documents folder.
- Share - opens the native OS share sheet (email, cloud drive, etc.).
- Respects
NumberColumn.FormatandBooleanColumn.TrueLabel/FalseLabel; excludesRowNumberColumn. - Programmatic API:
await myGrid.ExportAsync(stream, cancellationToken).
Clipboard Copy
<dg:DataGridControl
ShowCopyButton="True"
IsCopyEnabled="True"
IsCellCopyEnabled="True"
IsTableCopyEnabled="True" />
- Cell copy - long-press any data cell to copy its value.
- Table copy - copies all filtered rows with headers as TSV (pasteable directly into Excel/Google Sheets).
Conditional Row Styling
public class MyRowStyleSelector : DataGridRowStyleSelector
{
public override DataGridRowStyle SelectStyle(object item, BindableObject container)
{
if (item is EmployeeModel { IsActive: false })
return new DataGridRowStyle { TextColor = Colors.Gray };
return null; // fall through to default
}
}
<dg:DataGridControl RowStyleSelector="{StaticResource MySelector}" ... />
Evaluation order: editing > selected > hovered > conditional > default.
Summary Row
Displays aggregate values beneath all rows. Computes over the full filtered dataset, independent of the current page.
<dg:DataGridControl ShowSummaryRow="True" ... >
<col:NumberColumn Header="Salary" Binding="{Binding Salary}" Summary="Avg" />
<col:TextColumn Header="Name" Binding="{Binding Name}" Summary="Count" />
</dg:DataGridControl>
| Aggregate | Description |
|---|---|
Count |
Count of non-null values |
Sum |
Sum of numeric values |
Min |
Minimum value |
Max |
Maximum value |
Avg |
Arithmetic mean |
Appearance & Theming
| Property | Type | Default | Description |
|---|---|---|---|
IsStriped |
bool |
true |
Alternating row background colours |
RowHeight |
double |
-1 |
Fixed row height in DIPs; -1 = auto (size to content) |
HeaderHeight |
double |
52 |
Header row height in DIPs |
HoverColor |
Color? |
theme | Row background on mouse hover (desktop/tablet) |
SelectionColor |
Color? |
theme | Row background when selected |
NavButtonBackgroundColor |
Color |
#6B7280 |
Pagination button background |
NavButtonTextColor |
Color |
White |
Pagination button text / icon colour |
NavButtonCornerRadius |
int |
3 |
Corner radius for all pagination buttons |
ActiveButtonBackgroundColor |
Color |
#374151 |
Active page button background |
Commands & Events
| Event | Command | Args |
|---|---|---|
RowTapped |
RowTappedCommand |
Row data object |
SortChanged |
SortCommand |
SortDescriptor |
PageChanged |
- | int page number |
RowCommitted |
RowCommittedCommand |
Modified item |
RowCancelled |
RowCancelledCommand |
Restored item |
ColumnReordered |
ColumnReorderedCommand |
ColumnReorderRequestedArgs (FromIndex, ToIndex) |
ExportRequested |
ExportCommand |
Fires before export action |
SelectionChanged |
SelectionChangedCommand |
IReadOnlyList<object> snapshot |
PageLoaded |
PageLoadedCommand |
IReadOnlyList<object?> — all rows on the current page |
RowPageLoaded |
RowPageLoadedCommand |
object? — individual row ViewModel |
Page Load Hooks
Execute logic whenever a page of data is rendered — on initial load, page navigation, page size change, sort, filter, or search change. Four approaches are available; they can be combined freely.
Approach 1 — PageLoadedCommand (XAML binding, once per page)
Executed once after each page load. The command parameter is IReadOnlyList<object?> containing all rows on the current page.
<dg:DataGridControl PageLoadedCommand="{Binding OnPageLoadedCommand}" />
// CommunityToolkit.Mvvm
[RelayCommand]
void OnPageLoaded(IReadOnlyList<object?> rows)
{
var total = rows.OfType<EmployeeModel>().Sum(e => e.Salary);
StatusLabel = $"Page salary total: {total:N0}";
}
// Plain ICommand
public ICommand OnPageLoadedCommand { get; } = new Command<IReadOnlyList<object?>>(rows =>
{
foreach (var item in rows.OfType<EmployeeModel>())
item.LoadExtraData();
});
Approach 2 — RowPageLoadedCommand (XAML binding, once per row)
Executed once per row after each page load. The command parameter is the individual row ViewModel (object?).
<dg:DataGridControl RowPageLoadedCommand="{Binding OnRowLoadedCommand}" />
[RelayCommand]
void OnRowLoaded(object? row)
{
if (row is EmployeeModel emp)
emp.IsMarkedAsSeen = true;
}
Approach 3 — PageLoaded event (code-behind)
Subscribe in OnAppearing to guarantee the handler is registered before the first render completes.
If RefreshAsync finishes before the subscription (race condition on fast in-memory sources), use CurrentPageRows as a fallback.
protected override void OnAppearing()
{
base.OnAppearing();
TheGrid.PageLoaded += OnGridPageLoaded;
// Fallback: grid already loaded before subscription
if (TheGrid.CurrentPageRows.Count > 0)
OnGridPageLoaded(TheGrid, TheGrid.CurrentPageRows);
}
private void OnGridPageLoaded(object? sender, IReadOnlyList<object?> rows)
{
var active = rows.OfType<EmployeeModel>().Count(e => e.IsActive);
Debug.WriteLine($"Page loaded: {rows.Count} rows, {active} active");
}
protected override void OnDisappearing()
{
base.OnDisappearing();
TheGrid.PageLoaded -= OnGridPageLoaded;
}
Approach 4 — NotifyPageLoaded (manual, code-behind)
Manually re-fires all page-load notifications (PageLoaded event + PageLoadedCommand + RowPageLoaded + RowPageLoadedCommand) for the supplied row list.
Use CurrentPageRows to pass exactly the rows currently displayed.
// Re-trigger for current page without a full reload
TheGrid.NotifyPageLoaded(TheGrid.CurrentPageRows);
// Or supply a custom list
TheGrid.NotifyPageLoaded(myFilteredRows);
CurrentPageRows
Read-only property that always contains the rows currently displayed on the page.
Updated at the end of every RefreshAsync and every NotifyPageLoaded call.
var rows = myGrid.CurrentPageRows; // IReadOnlyList<object?>
License
This package is licensed under the MIT License.
© 2025 KMVUnion
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-android36.0 is compatible. net10.0-ios26.0 is compatible. net10.0-maccatalyst26.0 is compatible. net10.0-windows10.0.19041 is compatible. |
-
net10.0-android36.0
- Microsoft.Maui.Controls (>= 10.0.20)
-
net10.0-ios26.0
- Microsoft.Maui.Controls (>= 10.0.20)
-
net10.0-maccatalyst26.0
- Microsoft.Maui.Controls (>= 10.0.20)
-
net10.0-windows10.0.19041
- Microsoft.Maui.Controls (>= 10.0.20)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v1.3.0 — Added PageLoaded / RowPageLoaded events and PageLoadedCommand / RowPageLoadedCommand bindable properties; exposed CurrentPageRows for code-behind access to the current page data; all four notification approaches (command binding, event subscription, manual NotifyPageLoaded call) fire on every page load including the first render.; Fixed some issues related to programmatic change ItemSource.