A high-performance, fully canvas-drawn DataGrid for .NET — zero native controls, pixel-perfect on every platform.
Getting Started · Columns · Action Buttons · Sorting & Filtering · Grouping · Editing · Theming · Architecture
Every visual element — cells, headers, editors, scrollbars, popups — is rendered directly on a SkiaSharp canvas. There are zero native UIKit / AppKit / WinUI controls, giving you identical, pixel-perfect output across iOS, Android, macOS Catalyst, and Windows.
- Features at a Glance
- Getting Started
- Column Types
- Action Button Columns
- Column Sizing
- Frozen Columns & Rows
- Sorting & Filtering
- Grouping
- Summaries
- Editing
- Selection
- Keyboard Navigation
- Row Drag & Drop
- Theming & Styling
- Custom Fonts
- Scrolling & Performance
- Live Data
- Architecture
- Sample Application
- Requirements
- Contributing
- License
| Category | Highlights |
|---|---|
| Rendering | 100% SkiaSharp canvas, zero native controls, 60 fps |
| Virtual scrolling | Only visible rows/columns rendered — tested with 100K+ rows |
| Column types | Text, Numeric, Boolean, Date, ComboBox, Picker, ProgressBar, Image, Template |
| Action buttons | Pill-button columns with ICommand / callback support; activate on first tap |
| Column sizing | Fixed, Star (proportional), Auto (content-sized) |
| Frozen columns | Pin columns to left or right edge |
| Frozen rows | Pin top N rows to viewport |
| Sorting | Tap header to cycle asc/desc/none; multi-column |
| Filtering | Excel-style popup with search, value checklist, and sort controls |
| Grouping | Drag-and-drop group panel, expandable groups, nested groups |
| Summaries | Table-level and per-group aggregates (Sum, Avg, Count, Min, Max) |
| Editing | Configurable triggers (tap, double-tap, long-press, F2, typing); per-column override |
| Selection | Single / Multiple / Extended modes; Row or Cell unit |
| Keyboard nav | Arrows, Tab/Shift+Tab, Enter, Home/End, Page Up/Down |
| Row drag & drop | Handle column or full-row drag reorder |
| Custom fonts | Register any TTF/OTF typeface (CJK, icon fonts) via SkiaFontRegistrar; use by family name in GridFont |
| Theming | Built-in Light / Dark / HighContrast; fully customisable style object |
| Momentum scrolling | Physics-based inertial scroll with configurable friction |
| Live data | INotifyPropertyChanged and ObservableCollection O(1) updates |
| Column resizing | Drag column border |
For a .NET MAUI app, add KumikoUI.Maui (which transitively brings in KumikoUI.Core and KumikoUI.SkiaSharp):
dotnet add package KumikoUI.Maui| Package | Description |
|---|---|
KumikoUI.Maui |
DataGridView MAUI control + MAUI hosting extensions |
KumikoUI.SkiaSharp |
SkiaSharp drawing context implementation |
KumikoUI.Core |
Platform-agnostic layout, rendering pipeline, and models |
In MauiProgram.cs, call UseSkiaKumikoUI():
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseSkiaKumikoUI(); // registers SkiaSharp KumikoUI services<ContentPage
xmlns:dg="clr-namespace:KumikoUI.Maui;assembly=KumikoUI.Maui"
xmlns:core="clr-namespace:KumikoUI.Core.Models;assembly=KumikoUI.Core"><dg:DataGridView x:Name="kumiko"
ItemsSource="{Binding Items}"
RowHeight="36"
HeaderHeight="40">
<dg:DataGridView.Columns>
<core:DataGridColumn Header="Name" PropertyName="Name" Width="180" />
<core:DataGridColumn Header="Age" PropertyName="Age" Width="80"
ColumnType="Numeric" TextAlignment="Right" />
<core:DataGridColumn Header="Active" PropertyName="IsActive" Width="80"
ColumnType="Boolean" />
</dg:DataGridView.Columns>
</dg:DataGridView>Each column is a DataGridColumn with a ColumnType that controls both the read-only cell renderer and the inline editor.
ColumnType |
Cell Renderer | Editor | Notes |
|---|---|---|---|
Text |
Left-aligned text | DrawnTextBox |
Default type |
Numeric |
Formatted number | DrawnTextBox (numeric) |
Set Format (e.g. "C0", "N2") |
Boolean |
Checkbox glyph | DrawnCheckBox |
Three-state (checked / unchecked / indeterminate) |
Date |
Formatted date | DrawnDatePicker |
Calendar popup with month navigation |
ComboBox |
Selected value | DrawnComboBox |
Searchable dropdown; set EditorItemsString |
Picker |
Selected value | DrawnScrollPicker |
Mobile-style scroll wheel with physics snap |
Image |
Centered image | — (read-only) | Aspect-ratio preserving |
ProgressBar |
Progress bar | DrawnProgressBar |
Interactive slider when editable |
Template |
Custom | Custom (EditorDescriptor) |
Provide your own ICellRenderer |
<core:DataGridColumn Header="Department"
PropertyName="Department"
Width="140"
ColumnType="ComboBox"
EditorItemsString="Engineering,Marketing,Sales,HR,Finance" />
<core:DataGridColumn Header="Salary"
PropertyName="Salary"
Width="120"
ColumnType="Numeric"
Format="C0"
TextAlignment="Right" />
<core:DataGridColumn Header="Rating"
PropertyName="Rating"
Width="120"
ColumnType="ProgressBar" /><core:DataGridColumn Header="Id" PropertyName="Id"
IsReadOnly="True"
AllowTabStop="False" />Action button columns render a row of clickable pill-buttons directly inside every cell. Any number of buttons can be declared; they are distributed with equal widths across the cell area. Buttons support both ICommand (for MVVM data-binding) and an Action callback (for code-behind).
Action buttons use the Template column type with ActionButtonsCellRenderer as the CustomCellRenderer. The renderer also implements ICellEditorProvider, so no separate CustomEditorFactory is required — the same button definitions handle both display and interaction.
Tip: Set the column's
EditTriggers="SingleTap"so buttons respond on the first touch, even when the grid's default trigger isDoubleTap. See Per-column edit trigger override.
Set PropertyName="" so the full row item is passed as the value to both the renderer and the editor.
MauiActionButtonDefinition is a BindableObject that supports {Binding} on every property, including Command.
xmlns:dg="clr-namespace:KumikoUI.Maui;assembly=KumikoUI.Maui"
xmlns:render="clr-namespace:KumikoUI.Core.Rendering;assembly=KumikoUI.Core"
<dg:DataGridView x:Name="actionsGrid" ...>
<dg:DataGridView.Columns>
<core:DataGridColumn Header="Actions"
PropertyName=""
ColumnType="Template"
Width="200"
AllowSorting="False"
AllowFiltering="False"
AllowTabStop="False"
EditTriggers="SingleTap">
<core:DataGridColumn.CustomCellRenderer>
<render:ActionButtonsCellRenderer>
<render:ActionButtonsCellRenderer.Buttons>
<dg:MauiActionButtonDefinition
Label="Details"
BackgroundColor="13,110,253"
Command="{Binding Path=BindingContext.ViewDetailsCommand,
Source={x:Reference actionsGrid}}" />
<dg:MauiActionButtonDefinition
Label="Delete"
BackgroundColor="220,53,69"
Command="{Binding Path=BindingContext.DeleteCommand,
Source={x:Reference actionsGrid}}" />
</render:ActionButtonsCellRenderer.Buttons>
</render:ActionButtonsCellRenderer>
</core:DataGridColumn.CustomCellRenderer>
</core:DataGridColumn>
</dg:DataGridView.Columns>
</dg:DataGridView>Note:
MauiActionButtonDefinitionis not in the visual tree, so it does not inheritBindingContextautomatically. UseSource={x:Reference …}orSource={RelativeSource …}to point bindings at the correct source object.
Use ActionButtonDefinition (in KumikoUI.Core) when you want a code-behind closure per row:
actionsColumn.CustomEditorFactory = (value, bounds) => new DrawnActionButtons
{
Bounds = bounds,
RowItem = value,
Buttons =
[
new ActionButtonDefinition
{
Label = "Edit",
BackgroundColor = new GridColor(13, 110, 253),
Command = vm.EditCommand // row item is CommandParameter by default
},
new ActionButtonDefinition
{
Label = "Delete",
BackgroundColor = new GridColor(220, 53, 69),
Action = () => vm.DeleteEmployee((Employee)value!)
}
]
};| Property | Type | Description |
|---|---|---|
Label |
string |
Text displayed on the button face |
BackgroundColor |
GridColor |
Pill background color (R,G,B string format in XAML) |
TextColor |
GridColor |
Label text color (default: white) |
Command |
ICommand? |
Executed on tap; row item is the default CommandParameter |
CommandParameter |
object? |
Explicit command parameter; overrides the row item default |
Action |
Action? |
Code-behind callback; executed in addition to Command if both are set |
Layout properties on DrawnActionButtons / ActionButtonsCellRenderer:
| Property | Default | Description |
|---|---|---|
CornerRadius |
5f |
Button pill corner radius (pixels) |
ButtonSpacing |
6f |
Horizontal gap between buttons (pixels) |
CellPadding |
4f |
Inset from all four cell edges (pixels) |
Columns support three sizing modes controlled by the SizeMode property.
SizeMode |
Behavior |
|---|---|
Fixed |
Exact pixel width set via Width (default) |
Auto |
Measures header text + visible cell content; respects a MinWidth/MaxWidth clamp |
Star |
Weighted share of remaining space after Fixed and Auto columns; set Width="*" or "2*" |
Modes can be mixed freely — the layout engine resolves them in a single pass per frame.
Columns can be pinned to the left or right edge and will never scroll out of view.
<!-- Pin to left (default freeze side) -->
<core:DataGridColumn Header="Id" PropertyName="Id" IsFrozen="True" />
<core:DataGridColumn Header="Name" PropertyName="Name" IsFrozen="True" />
<!-- Pin to right -->
<core:DataGridColumn Header="Total" PropertyName="Total" FreezeMode="Right" />The renderer uses 3-pass clip regions so frozen columns naturally overlay scrollable content with correct z-ordering.
Pin the first N rows so they remain visible while the rest of the data scrolls:
<dg:DataGridView FrozenRowCount="2" ... />Tap any column header to sort. The indicator cycles through Ascending → Descending → None. Multi-column sort is supported — hold the sort state on multiple columns simultaneously via code:
// Set ascending sort on a column, then refresh:
lastNameColumn.SortDirection = SortDirection.Ascending;
kumiko.DataSource.Refresh();Every column header displays a filter icon. Tapping it opens an Excel-style filter popup that provides:
- Full-text search across all values in that column
- Value checklist (check/uncheck individual values)
- Quick sort buttons (A→Z / Z→A) inside the popup
Filters are applied immediately on close and stacked across columns. Remove a filter by opening the popup and selecting all values.
Add one or more GroupDescription objects to enable a collapsible group hierarchy with an interactive drag-and-drop group panel above the header.
kumiko.DataSource.AddGroupDescription(new GroupDescription("Department"));
kumiko.DataSource.AddGroupDescription(new GroupDescription("Level"));Groups are expanded by default. You can bulk-expand or collapse:
kumiko.DataSource.ExpandAllGroups();
kumiko.DataSource.CollapseAllGroups();Remove grouping:
kumiko.DataSource.ClearGroupDescriptions();Group summaries (aggregate values per group) are declared with AddGroupSummaryRow() — see Summaries below.
Summaries appear as pinned rows at the top or bottom of the grid (table summaries) or at the bottom of each group (group summaries).
<dg:DataGridView.TableSummaryRows>
<core:TableSummaryRow Name="Totals" Position="Bottom" Title="Totals">
<core:TableSummaryRow.Columns>
<core:SummaryColumnDescription PropertyName="Salary"
SummaryType="Sum" Format="C0" />
<core:SummaryColumnDescription PropertyName="Id"
SummaryType="Count" Label="Rows: " />
<core:SummaryColumnDescription PropertyName="Salary"
SummaryType="Average" Format="C0"
Label="Avg: " />
</core:TableSummaryRow.Columns>
</core:TableSummaryRow>
</dg:DataGridView.TableSummaryRows>kumiko.DataSource.AddGroupSummaryRow(new SummaryDescription
{
Columns =
{
new SummaryColumnDescription { PropertyName = "Salary", SummaryType = SummaryType.Sum, Format = "C0" },
new SummaryColumnDescription { PropertyName = "Salary", SummaryType = SummaryType.Average, Format = "C0", Label = "Avg: " }
}
});Sum · Average · Count · Min · Max
Configure which gestures or keys open the inline editor:
<dg:DataGridView EditTriggers="DoubleTap,F2Key,Typing"
EditTextSelectionMode="SelectAll"
DismissKeyboardOnEnter="True" />| Trigger | Description |
|---|---|
SingleTap |
Single tap on a cell |
DoubleTap |
Double-tap (default) |
LongPress |
Long-press gesture |
F2Key |
F2 keyboard shortcut |
Typing |
Start typing immediately to replace cell content |
Triggers are flags — combine freely: "DoubleTap,F2Key,Typing".
Each column can override the grid-level EditTriggers independently via its own EditTriggers property. When set, that column uses its own trigger flags; when null (the default), the column inherits the grid-level setting.
<dg:DataGridView EditTriggers="DoubleTap, F2Key, Typing">
<dg:DataGridView.Columns>
<!-- Most accessible: single-tap, F2, or typing to edit -->
<core:DataGridColumn Header="Name" PropertyName="Name"
EditTriggers="SingleTap, F2Key, Typing" />
<!-- ComboBox: double-tap or F2 only (no accidental typing edits) -->
<core:DataGridColumn Header="Department" PropertyName="Department"
ColumnType="ComboBox"
EditorItemsString="Engineering,Marketing,Finance,HR"
EditTriggers="DoubleTap, F2Key" />
<!-- Salary: keyboard-only — protects financial data from pointer gestures -->
<core:DataGridColumn Header="Salary" PropertyName="Salary"
ColumnType="Numeric" Format="C0"
EditTriggers="F2Key" />
<!-- Action buttons always activate on first tap -->
<core:DataGridColumn Header="Actions" PropertyName=""
ColumnType="Template"
EditTriggers="SingleTap" />
</dg:DataGridView.Columns>
</dg:DataGridView>Set EditTriggers="None" to suppress all gesture/keyboard triggers for a column without making it fully read-only via IsReadOnly.
<!-- Select all existing text when the editor opens -->
<dg:DataGridView EditTextSelectionMode="SelectAll" />
<!-- Or keep cursor at end -->
<dg:DataGridView EditTextSelectionMode="End" />- Enter — commit and move to the next row
- Tab / Shift+Tab — commit and move to the next/previous editable cell
- Escape — cancel and restore the original value
For Template columns supply an EditorDescriptor:
var column = new DataGridColumn
{
Header = "Rating",
PropertyName = "Rating",
ColumnType = DataGridColumnType.Template,
EditorDescriptor = new NumericUpDownEditorDescriptor { Min = 0, Max = 10, Step = 1 }
};<dg:DataGridView GridSelectionMode="Multiple" />SelectionMode |
Behavior |
|---|---|
Single |
One item at a time |
Multiple |
Each tap toggles the item |
Extended |
Click + Shift/Ctrl range/toggle (desktop-style) |
SelectionUnit |
Behavior |
|---|---|
Row |
Whole row is selected |
Cell |
Individual cell is selected |
var selectedRows = kumiko.Selection.SelectedRows; // HashSet<int> of row indices
var selectedCells = kumiko.Selection.SelectedCells; // HashSet<CellPosition>Full keyboard navigation is supported on all platforms (iOS/macOS via UIKeyCommand, Android via hidden Entry with IME diffing, Windows via hidden Entry).
| Key | Action |
|---|---|
Arrow keys |
Move selection one cell/row |
Tab |
Move to next editable cell (wraps rows) |
Shift+Tab |
Move to previous editable cell |
Enter |
Commit edit / open edit on selected cell |
Escape |
Cancel edit |
F2 |
Open inline editor (if F2Key trigger enabled) |
Home / End |
First / last cell in row |
Ctrl+Home / Ctrl+End |
First / last cell in grid |
Page Up / Page Down |
Scroll one viewport height |
Rows can be reordered by the user via two modes.
A dedicated grab-handle column appears as the first (or last) column:
var style = kumiko.GridStyle;
style.ShowRowDragHandle = true;
style.RowDragHandlePosition = DragHandlePosition.Left; // or Right
kumiko.GridStyle = style;Long-press any row to begin dragging (no handle column required):
style.AllowRowDragDrop = true;
style.ShowRowDragHandle = false;
kumiko.GridStyle = style;A visual overlay tracks the dragged row. The row list is updated on drop.
<dg:DataGridView Theme="Light" /> <!-- default -->
<dg:DataGridView Theme="Dark" />
<dg:DataGridView Theme="HighContrast" />Every color, font, size, and line width is configurable through the DataGridStyle object:
var style = kumiko.GridStyle;
// Colors
style.BackgroundColor = new GridColor(0.95f, 0.95f, 0.97f);
style.AlternateRowColor = new GridColor(0.90f, 0.90f, 0.93f);
style.HeaderBackgroundColor = new GridColor(0.20f, 0.40f, 0.80f);
style.HeaderTextColor = GridColor.White;
style.SelectedRowColor = new GridColor(0.85f, 0.92f, 1.00f);
style.GridLineColor = new GridColor(0.80f, 0.80f, 0.80f);
style.FrozenDividerColor = new GridColor(0.40f, 0.40f, 0.40f);
// Typography
style.CellFontSize = 14f;
style.HeaderFontSize = 14f;
style.CellFontBold = false;
style.HeaderFontBold = true;
// Grid lines
style.ShowHorizontalGridLines = true;
style.ShowVerticalGridLines = false;
kumiko.GridStyle = style;Apply a CellStyleResolver delegate to the DataGridStyle to override the style of individual cells at render time:
var style = kumiko.GridStyle;
style.CellStyleResolver = (item, column) =>
{
if (column.PropertyName == "Salary" && item is Employee e && e.Salary > 100_000)
return new CellStyle { TextColor = new GridColor(0.8f, 0.1f, 0.1f), Bold = true };
return null;
};
kumiko.GridStyle = style;Style resolution cascades: per-cell resolver → column CellStyle → grid DataGridStyle.
SkiaFontRegistrar (in KumikoUI.SkiaSharp) is a static registry for custom SKTypeface instances. Register any TrueType or OpenType font by a family name string, and SkiaDrawingContext will resolve it by that name whenever a GridFont requests that family — bypassing the system font manager entirely.
This is the recommended fix for two common problems on Android:
- CJK text (Japanese, Chinese, Korean) renders as empty boxes because the Android system font manager doesn't surface CJK typefaces to SkiaSharp.
- Icon-font glyphs (Material Icons, Font Awesome, etc.) appear garbled for the same reason.
Register fonts in MauiProgram.cs before the MAUI UI loop starts. Place font files in Resources/Raw/ so they are bundled with the app.
// MauiProgram.cs
using KumikoUI.SkiaSharp;
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>().UseSkiaKumikoUI();
// Register CJK font
using var jpStream = await FileSystem.OpenAppPackageFileAsync("NotoSansJP-Regular.ttf");
SkiaFontRegistrar.RegisterTypefaceFromStream("NotoSansJP", jpStream);
// Register icon font
using var icStream = await FileSystem.OpenAppPackageFileAsync("MaterialIcons-Regular.ttf");
SkiaFontRegistrar.RegisterTypefaceFromStream("MaterialIcons", icStream);
return builder.Build();
}Note:
RegisterTypefaceFromStreamtakes ownership of the created typeface and disposes it whenSkiaFontRegistrar.Clear()is called. The stream may be disposed after the call returns. UseRegisterTypeface(family, typeface)instead when you want to manage the typeface lifetime yourself.
Reference the registered family name in GridFont on a DataGridStyle or directly in a custom renderer:
// Apply a CJK font to all headers and cells
var style = kumiko.GridStyle;
style.HeaderFont = new GridFont("NotoSansJP", 14, bold: true);
style.CellFont = new GridFont("NotoSansJP", 13);
kumiko.GridStyle = style;Store the Unicode code-point string as the cell value and render it with a custom ICellRenderer that specifies the icon font family:
// Renderer that draws cell text using a named icon-font typeface
public class IconFontCellRenderer(string fontFamily, float fontSize) : ICellRenderer
{
public void Render(IDrawingContext ctx, GridRect cellRect, object? value,
string displayText, DataGridColumn column,
DataGridStyle style, bool isSelected, CellStyle? cellStyle = null)
{
if (string.IsNullOrEmpty(displayText)) return;
ctx.DrawTextInRect(displayText, cellRect, new GridPaint
{
Color = isSelected ? style.SelectionTextColor : style.CellTextColor,
Font = new GridFont(fontFamily, fontSize),
IsAntiAlias = true
}, GridTextAlignment.Center, GridVerticalAlignment.Center);
}
}Attach it to a column:
// "\uE876" = checkmark, "\uE5CD" = close, "\uE002" = warning in MaterialIcons
new DataGridColumn
{
Header = "Status",
PropertyName = nameof(Item.StatusIcon), // returns e.g. "\uE876"
Width = 60,
IsReadOnly = true,
TextAlignment = GridTextAlignment.Center,
CustomCellRenderer = new IconFontCellRenderer("MaterialIcons", 22f)
}| Method | Description |
|---|---|
SkiaFontRegistrar.RegisterTypefaceFromStream(family, stream) |
Creates a typeface from a stream and registers it. Registrar owns the typeface lifetime. |
SkiaFontRegistrar.RegisterTypeface(family, typeface) |
Registers an existing SKTypeface. Caller retains ownership. |
SkiaFontRegistrar.TryGetTypeface(family, out typeface) |
Looks up a registered typeface by family name. |
SkiaFontRegistrar.Clear() |
Removes all registrations and disposes owned typefaces. |
Only the rows and columns currently within the viewport are measured and drawn. The visible row range is computed as:
firstRow = scrollOffsetY / rowHeight
lastRow = firstRow + viewportHeight / rowHeight + 1
Column visibility is resolved by GridLayoutEngine walking cumulative widths from the scroll offset.
Touch-based scrolling uses physics-based inertia — a velocity tracker samples recent pointer deltas and the InertialScroller decelerates with configurable friction once the finger lifts. The deceleration is frame-normalized so it behaves consistently at any refresh rate.
~60 GridPaint objects are pre-computed once per frame from DataGridStyle so there are zero per-cell paint allocations during rendering.
SKTypeface, SKFont, and SKPaint instances are cached by value-type keys and disposed at frame end — no GC pressure from native Skia objects.
When ItemsSource is an ObservableCollection and no sort, filter, group, or summary transforms are active, add/remove operations are handled in O(1) without rebuilding the entire flat view.
The grid automatically reacts to standard .NET data-change notifications.
var employees = new ObservableCollection<Employee>(initialList);
kumiko.ItemsSource = employees;
// These automatically update the grid:
employees.Add(new Employee { Name = "Alice" });
employees.RemoveAt(3);Individual cell values update in real time when the bound object implements INotifyPropertyChanged:
public class Employee : INotifyPropertyChanged
{
private decimal _salary;
public decimal Salary
{
get => _salary;
set { _salary = value; PropertyChanged?.Invoke(this, new(nameof(Salary))); }
}
public event PropertyChangedEventHandler? PropertyChanged;
}PropertyName supports dotted paths for nested objects:
<core:DataGridColumn Header="City" PropertyName="Address.City" Width="120" />Accessors are compiled once via Expression.Property chains and cached.
KumikoUI.Core (net9.0) — zero external dependencies
↑
KumikoUI.SkiaSharp (net9.0) — SkiaSharp 3.119.2
↑
KumikoUI.Maui (net10.0 multi-target: iOS / Android / macOS Catalyst / Windows)
KumikoUI.Core has no dependency on MAUI or SkiaSharp. All rendering operations go through the IDrawingContext abstraction, making the core portable to any backend (Blazor Canvas, WPF, Direct2D, CoreGraphics, etc.).
| Class | Responsibility |
|---|---|
DataGridRenderer |
Orchestrates the 18-step draw pipeline (layout → background → headers → rows → overlays → popups) |
DataGridSource |
Data management: filtering, sorting, grouping, summaries, INotifyCollectionChanged observation |
GridLayoutEngine |
Column width computation (Fixed / Auto / Star), visible row/column range |
GridHitTester |
Resolves pointer coordinates to one of 14 HitRegion types across 3 column panes |
PaintCache |
Pre-computes ~60 GridPaint objects per frame from DataGridStyle |
GridInputController |
Routes pointer and keyboard events to selection, editing, resizing, drag handlers |
SelectionModel |
Row/cell selection state, Single / Multiple / Extended modes |
EditSession |
Cell edit lifecycle — begin, commit, cancel, validation, write-back |
CellEditorFactory |
Instantiates the correct DrawnComponent editor for each column type |
InertialScroller |
Physics-based scroll deceleration with configurable friction |
PopupManager |
Popup z-order and input priority routing (filter, ComboBox, DatePicker) |
IDrawingContext |
Platform-independent drawing API: fill, stroke, text, images, clipping |
SkiaDrawingContext |
SKCanvas implementation with 3-tier native object caching |
DataGridView |
MAUI control — hosts SKCanvasView, translates touch/scroll/keyboard events |
See docs/ARCHITECTURE.md for an in-depth contributor guide and docs/RENDERING.md for the full 18-step rendering pipeline.
samples/SampleApp.Maui demonstrates every feature across four pages:
| Page | What it shows |
|---|---|
| All Components | Every column type, frozen columns, frozen rows, edit triggers, selection modes, drag & drop, summaries, theme toggle |
| Grouping & Filtering | Interactive group panel, nested groups, filter popups, group summaries |
| MVVM + Column EditTriggers | Action button columns with MVVM commands; per-column EditTriggers overrides across all trigger types |
| Custom Fonts | CJK (Japanese) column headers via NotoSansJP; Material Design icon-glyph columns via MaterialIcons |
| Large Data | 100K-row stress test with virtual scrolling performance metrics |
| Theming | Live Light / Dark / HighContrast switching, custom color picker |
All Components |
Grouping & Filtering |
Dark Theme |
100K Rows — Virtual Scroll |
| Target | Minimum version |
|---|---|
| .NET (library) | 9.0 |
| .NET (MAUI app) | 10.0 |
| SkiaSharp | 3.119.2 |
| iOS | 15.0+ |
| Android | 5.0+ (API 21) |
| macOS Catalyst | 15.0+ |
| Windows | 10.0.17763+ |
Contributions are welcome! Please open an issue before submitting a large pull request so we can discuss the approach.
- Fork the repository and create your branch from
main. - Build the solution:
dotnet build KumikoUI.sln - Run the tests:
dotnet test - Submit a pull request with a clear description of the change.
See docs/ARCHITECTURE.md for code structure and extension points.
MIT — see LICENSE for details.



