DataGrid
BitDataGrid is a robust way to display an information-rich collection of items, and allow people to sort and filter the content. Use a data-grid when information density is critical.
Notes
To use this component, you need to install the
Bit.BlazorUI.Extras
NuGet package, as described in the Optional steps of the
Getting started page.
Usage
Basic & sorting
@* TItem is inferred from the grid for all columns; Property is the strongly typed alternative to Field="Name"; the Columns wrapper element is an optional alias of the grid's child content. *@ <BitDataGrid Items="@products" Height="430px" MultiSort="true"> <Columns> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Align="BitDataGridColumnAlign.Right" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Align="BitDataGridColumnAlign.Right" /> <BitDataGridColumn Property="p => p.Stock" Align="BitDataGridColumnAlign.Right" /> <BitDataGridColumn Property="p => p.Rating" Format="N1" Align="BitDataGridColumnAlign.Right" /> </Columns> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(50); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
The item type flows from the grid to its columns automatically (no per-column TItem needed), a column binds to its property with the strongly typed Property selector (the string-based Field parameter also remains available), and columns can be wrapped in an optional
Filtering & paging
<BitDataGrid Items="@products" Height="430px" Filterable="true" Pageable="true" PageSize="10" PagerPosition="BitDataGridPagerPosition.Bottom" ShowToolbar="true" ShowCsvExport="true"> <BitDataGridColumn Field="Id" Title="ID" Filterable="false" /> <BitDataGridColumn Field="Name" /> <BitDataGridColumn Field="Category" /> <BitDataGridColumn Field="Supplier" /> <BitDataGridColumn Field="Price" Format="C2" Align="BitDataGridColumnAlign.Right" /> <BitDataGridColumn Field="Stock" Align="BitDataGridColumnAlign.Right" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(200); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
This example binds columns with the string-based
Selection
<BitDataGrid Items="@products" Height="420px" SelectionMode="BitDataGridSelectionMode.Multiple" @bind-SelectedItems="selected" Pageable="true" PageSize="10"> <BitDataGridColumn Property="p => p.Id" Title="ID" Align="BitDataGridColumnAlign.Right" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Align="BitDataGridColumnAlign.Right" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(60); private IReadOnlyList<Product> selected = new List<Product>(); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
0 selected
Inline editing
@* TItem stays explicit here: EventCallback parameters (OnRowSave/OnRowDelete/OnRowCreate) bound to method groups cannot participate in generic type inference. *@ <BitDataGrid TItem="Product" Items="@products" Height="460px" Editable="true" NewItemFactory="CreateProduct" OnRowSave="OnSave" OnRowDelete="OnDelete" OnRowCreate="OnCreate" ShowToolbar="true" Pageable="true" PageSize="10" KeyField="p => p.Id"> <BitDataGridColumn Property="p => p.Id" Title="ID" Editable="false" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> <BitDataGridColumn Property="p => p.Discontinued" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(25); private int nextId; protected override void OnInitialized() => nextId = products.Max(p => p.Id) + 1; private Product CreateProduct() => new() { Id = nextId++, Name = "New product", Category = Category.Electronics, ReleaseDate = DateTime.Today }; private void OnCreate(Product p) { /* called when a new row starts being added */ } private void OnSave(Product p) { // The grid is keyed by Id (KeyField="p => p.Id") and hands back an edited copy, so match on Id // rather than the object reference: update the existing row in place, or insert a brand-new one. var index = products.FindIndex(x => x.Id == p.Id); if (index >= 0) products[index] = p; else products.Insert(0, p); } private void OnDelete(Product p) => products.RemoveAll(x => x.Id == p.Id); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
EditTemplate on a column to supply your own editor.
Grouping & aggregates
<BitDataGrid Items="@products" Height="500px" Groupable="true" ShowFooter="true" Sortable="true"> <BitDataGridColumn Property="p => p.Name" Groupable="false" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Supplier" AggregateBy="rows => DistinctSuppliers(rows)" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Align="BitDataGridColumnAlign.Right" Aggregate="BitDataGridAggregateType.Sum" Groupable="false" /> <BitDataGridColumn Property="p => p.Stock" Align="BitDataGridColumnAlign.Right" Aggregate="BitDataGridAggregateType.Average" AggregateFormat="N0" Groupable="false" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(80); // A custom aggregate: receives the rows of the footer's view (or of each group) and returns any value. private object? DistinctSuppliers(IReadOnlyList<Product> rows) => $"{rows.Select(p => p.Supplier).Distinct().Count()} distinct"; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Aggregate types, AggregateBy computes a custom
aggregate - here the Supplier column counts distinct suppliers per group and overall.
Templates & detail rows
<BitDataGrid Items="@products" Height="470px" Sortable="true" ShowFooter="true"> <DetailTemplate Context="p"> <div>Supplier: @p.Supplier</div> </DetailTemplate> <Columns> <BitDataGridColumn Property="p => p.Name"> <HeaderTemplate>📦 Product</HeaderTemplate> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Price" Format="C2" Align="BitDataGridColumnAlign.Right" Aggregate="BitDataGridAggregateType.Sum"> <FooterTemplate Context="agg">Total: @agg.FormattedValue</FooterTemplate> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Stock" Align="BitDataGridColumnAlign.Right"> <Template Context="p">@p.Stock in stock</Template> </BitDataGridColumn> @* A template-only column (no Field) becomes sortable through its SortBy key selector. *@ <BitDataGridColumn ColumnId="Value" Title="Value" Align="BitDataGridColumnAlign.Right" SortBy="@(p => p.Price * p.Stock)"> <Template Context="p">@((p.Price * p.Stock).ToString("C0"))</Template> </BitDataGridColumn> </Columns> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(30); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Field) is not sortable by default - give it a
SortBy key selector to sort it, like the computed Value column here.
Resize, reorder & freeze columns
<BitDataGrid Items="@products" Height="430px" Resizable="true" Reorderable="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="80px" Frozen="true" /> <BitDataGridColumn Property="p => p.Name" Width="220px" Frozen="true" /> <BitDataGridColumn Property="p => p.Category" Width="160px" /> <BitDataGridColumn Property="p => p.Price" Width="160px" Format="C2" Align="BitDataGridColumnAlign.Right" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(40); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Column header groups
<BitDataGrid Items="@products" Height="460px" Sortable="true" Bordered="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" /> <BitDataGridColumn Property="p => p.Name" Group="Identity" /> <BitDataGridColumn Property="p => p.Category" Group="Identity" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Group="Commercials" /> <BitDataGridColumn Property="p => p.Stock" Group="Commercials" /> <BitDataGridColumn Property="p => p.Rating" Format="N1" Group="Quality" /> <BitDataGridColumn Property="p => p.Supplier" Group="Quality" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(40); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Group on consecutive columns to render them under a single spanning header cell.
Column spanning
<BitDataGrid Items="@products" Height="460px" Bordered="true"> <BitDataGridColumn Property="p => p.Name" ColSpan="p => NameSpan(p)"> <Template Context="p">@p.Name</Template> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" ColSpan="p => PriceSpan(p)" /> <BitDataGridColumn Property="p => p.Stock" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(40); private int? NameSpan(Product p) => p.Discontinued ? 2 : null; private int? PriceSpan(Product p) => p.Price > 800 ? 2 : null; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
ColSpan function on a column to let a single cell span several columns based on its row.
Discontinued rows span the Name cell over Category; premium rows span Price over Stock.
Virtualization
<BitDataGrid Items="@products" Height="520px" Virtualize="true" RowHeight="36" Sortable="true" Filterable="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(10_000); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Height and RowHeight.
10,000 rows
Server-side data
<BitDataGrid OnRead="LoadData" Height="430px" Pageable="true" PageSize="10" Sortable="true" Filterable="true" Loading="loading"> <BitDataGridColumn Property="p => p.Id" Title="ID" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private bool loading; private readonly List<Product> all = SampleData.Generate(523); private async Task<BitDataGridReadResult<Product>> LoadData(BitDataGridReadRequest request) { loading = true; await InvokeAsync(StateHasChanged); // re-render so the loading indicator shows try { await Task.Delay(250, request.CancellationToken); // simulate a backend round-trip IEnumerable<Product> query = all; // filtering - honor the operator the grid emits, not just contains/equals foreach (var f in request.Filters) { query = f.ColumnId switch { // text column uses the string operators from the grid's text filter editor nameof(Product.Name) => query.Where(p => MatchText(p.Name, f)), // numeric columns receive a typed value with a comparison operator, so honor the // requested equality/range operator instead of a hard-coded equals nameof(Product.Price) => query.Where(p => MatchComparable(p.Price, f)), nameof(Product.Id) => query.Where(p => MatchComparable(p.Id, f)), _ => query }; } // sorting (honor every active sort descriptor, not just the first) IOrderedEnumerable<Product>? ordered = null; foreach (var sort in request.Sorts) { Func<Product, object> key = sort.ColumnId switch { nameof(Product.Name) => p => p.Name, nameof(Product.Price) => p => p.Price, _ => p => p.Id }; if (ordered is null) ordered = sort.Direction == BitDataGridSortDirection.Descending ? query.OrderByDescending(key) : query.OrderBy(key); else ordered = sort.Direction == BitDataGridSortDirection.Descending ? ordered.ThenByDescending(key) : ordered.ThenBy(key); } if (ordered is not null) query = ordered; // paging var filtered = query.ToList(); var items = filtered.Skip(request.Skip).Take(request.Take ?? filtered.Count).ToList(); // A superseded request can finish filtering/sorting/paging after a newer one started; bail // out before returning so the grid never receives stale rows for a cancelled load. request.CancellationToken.ThrowIfCancellationRequested(); return new BitDataGridReadResult<Product>(items, filtered.Count); } finally { // Only the active request should clear the loading state; a superseded request observes a // cancelled token, so skip the reset and let the newer in-flight load own the indicator. if (!request.CancellationToken.IsCancellationRequested) { loading = false; await InvokeAsync(StateHasChanged); // re-render after the load completes (runs as a callback) } } } // Applies a text-column filter the way the grid's text editor emits it. private static bool MatchText(string value, BitDataGridFilterDescriptor f) { if (f.Operator is BitDataGridFilterOperator.IsEmpty) return string.IsNullOrEmpty(value); if (f.Operator is BitDataGridFilterOperator.IsNotEmpty) return !string.IsNullOrEmpty(value); var term = f.Value?.ToString(); if (string.IsNullOrWhiteSpace(term)) return true; return f.Operator switch { BitDataGridFilterOperator.Contains => value.Contains(term, StringComparison.OrdinalIgnoreCase), BitDataGridFilterOperator.DoesNotContain => !value.Contains(term, StringComparison.OrdinalIgnoreCase), BitDataGridFilterOperator.StartsWith => value.StartsWith(term, StringComparison.OrdinalIgnoreCase), BitDataGridFilterOperator.EndsWith => value.EndsWith(term, StringComparison.OrdinalIgnoreCase), BitDataGridFilterOperator.Equals => string.Equals(value, term, StringComparison.OrdinalIgnoreCase), BitDataGridFilterOperator.NotEquals => !string.Equals(value, term, StringComparison.OrdinalIgnoreCase), _ => true }; } // Applies a non-text-column filter against the typed value the grid emits so the requested // equality/range operator is honored instead of a substring match on ToString(). private static bool MatchComparable<T>(T value, BitDataGridFilterDescriptor f) where T : IComparable { if (f.Operator is BitDataGridFilterOperator.IsEmpty) return value is null; if (f.Operator is BitDataGridFilterOperator.IsNotEmpty) return value is not null; if (f.Value is null) return true; if (f.Value is not T typed) return true; var cmp = value.CompareTo(typed); return f.Operator switch { BitDataGridFilterOperator.Equals => cmp == 0, BitDataGridFilterOperator.NotEquals => cmp != 0, BitDataGridFilterOperator.GreaterThan => cmp > 0, BitDataGridFilterOperator.GreaterThanOrEqual => cmp >= 0, BitDataGridFilterOperator.LessThan => cmp < 0, BitDataGridFilterOperator.LessThanOrEqual => cmp <= 0, _ => true }; } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
OnRead callback to take over sorting/filtering/paging (e.g. against a database).
This example simulates a backend with a small delay.
Last request → skip 0, take 10, sorts: 0, filters: 0, total: 523
Infinite scrolling
<BitDataGrid OnLoadMore="LoadMore" LoadMoreBatchSize="40" Height="520px" RowHeight="40" Sortable="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private readonly List<Product> all = SampleData.Generate(2_017); private async Task<BitDataGridReadResult<Product>> LoadMore(BitDataGridReadRequest request) { await Task.Delay(350, request.CancellationToken); // simulate a backend round-trip IEnumerable<Product> query = all; // Apply every active sort descriptor before paging out the batch. IOrderedEnumerable<Product>? ordered = null; foreach (var sort in request.Sorts) { Func<Product, object> key = sort.ColumnId switch { nameof(Product.Name) => p => p.Name, nameof(Product.Price) => p => p.Price, _ => p => p.Id }; if (ordered is null) ordered = sort.Direction == BitDataGridSortDirection.Descending ? query.OrderByDescending(key) : query.OrderBy(key); else ordered = sort.Direction == BitDataGridSortDirection.Descending ? ordered.ThenByDescending(key) : ordered.ThenBy(key); } if (ordered is not null) query = ordered; // Take is the batch size while scrolling; null means "all rows" (issued by CSV/Excel exports). var batch = query.Skip(request.Skip).Take(request.Take ?? all.Count).ToList(); // Drop a superseded batch before returning so a cancelled load never yields stale rows. request.CancellationToken.ThrowIfCancellationRequested(); // Pass 0 as the total count to signal there is no known total (infinite scrolling). return new BitDataGridReadResult<Product>(batch, 0); } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
OnLoadMore instead of binding Items. The grid appends the next batch
automatically as the user scrolls toward the end - with no total count or paging UI. A fixed
Height is required. CSV/Excel exports issue a single request with
Take = null ("all rows"), so the handler should honor a null Take.
Batch #1 → loaded rows 1–40 (40 rows)
Tree view (hierarchical rows)
<BitDataGrid Items="@roots" Height="460px" Sortable="true" ChildrenSelector="n => n.Children" TreeInitiallyExpanded="true" KeyField="n => n.Id" @ref="grid"> <BitDataGridColumn Property="p => p.Name" Width="320px" /> <BitDataGridColumn Property="p => p.Kind" Title="Type" /> <BitDataGridColumn Property="p => p.Size" Format="N0" Align="BitDataGridColumnAlign.Right" /> </BitDataGrid>@code { private List<FileNode> roots = FileSystemData.Build(); private BitDataGrid<FileNode>? grid; private async Task ExpandAll() { if (grid is not null) await grid.ExpandAllAsync(); } private async Task CollapseAll() { if (grid is not null) await grid.CollapseAllAsync(); } public class FileNode { public int Id { get; set; } public string Name { get; set; } = ""; public string Kind { get; set; } = "Folder"; public long Size { get; set; } public DateTime Modified { get; set; } public List<FileNode> Children { get; set; } = new(); } public static class FileSystemData { public static List<FileNode> Build() { var id = 0; var baseDate = new DateTime(2025, 1, 1); FileNode Folder(string name, params FileNode[] children) { var node = new FileNode { Id = ++id, Name = name, Kind = "Folder", Modified = baseDate.AddDays(id), Children = children.ToList() }; node.Size = node.Children.Sum(c => c.Size); return node; } FileNode File(string name, long size) => new() { Id = ++id, Name = name, Kind = "File", Size = size, Modified = baseDate.AddDays(id) }; return new List<FileNode> { Folder("src", Folder("BitDataGrid", File("BitDataGrid.razor", 24_500), File("BitDataGrid.razor.cs", 41_200), Folder("Models", File("BitDataGridColumnAlign.cs", 320), File("BitDataGridSortDescriptor.cs", 540), File("BitDataGridFilterOperator.cs", 610)), Folder("Infrastructure", File("BitDataGridDataProcessor.cs", 8_900), File("BitDataGridPropertyAccessor.cs", 3_400))), Folder("BitDataGrid.Demo", File("Program.cs", 1_200), Folder("Components", File("App.razor", 760), File("Routes.razor", 280)))), Folder("docs", File("README.md", 6_400), File("CHANGELOG.md", 2_100)), Folder("assets", File("logo.svg", 4_800), File("styles.css", 12_300), File("favicon.ico", 1_150)), File("LICENSE", 1_070), File(".gitignore", 410) }; } } }
ChildrenSelector parameter to a function that returns each item's direct children.
The grid then treats Items as the root nodes and renders expand/collapse toggles with indentation.
Master detail
<BitDataGrid Items="@suppliers" Height="520px" Sortable="true"> <DetailTemplate Context="supplier"> <BitDataGrid Items="supplier.Products" Sortable="true"> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid> </DetailTemplate> <Columns> <BitDataGridColumn Property="p => p.Name" Title="Supplier" /> <BitDataGridColumn Property="p => p.ProductCount" Title="Products" /> </Columns> </BitDataGrid>@code { private List<SupplierModel> suppliers = BuildSuppliers(); private static List<SupplierModel> BuildSuppliers() => SampleData.Generate(240) .GroupBy(p => p.Supplier) .Select(g => new SupplierModel { Name = g.Key, Products = g.OrderBy(p => p.Name).ToList() }) .OrderBy(s => s.Name) .ToList(); public sealed class SupplierModel { public string Name { get; set; } = ""; public List<Product> Products { get; set; } = new(); public int ProductCount => Products.Count; public int TotalStock => Products.Sum(p => p.Stock); public decimal AveragePrice => Products.Count == 0 ? 0 : Math.Round(Products.Average(p => p.Price), 2); } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
DetailTemplate can render anything - including another BitDataGrid.
Each supplier expands to show a nested, sortable grid of the products it provides.
Row reordering
<BitDataGrid TItem="Product" Items="@products" Height="460px" RowReorderable="true" OnRowReorder="OnReorder" Sortable="false"> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(12); private void OnReorder(BitDataGridRowReorderEventArgs<Product> e) { // e.DraggedItem, e.TargetItem, e.FromIndex, e.ToIndex // FromIndex/ToIndex are int? and may be null when Items is not an indexable IList<T>. } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
RowReorderable="true" to show a drag handle on each row. Grab the ⠿ handle and drop it
onto another row. The grid reorders the bound list in place and raises OnRowReorder.
Cell events & context menu
<style> /* The invisible overlay catches the click/right-click that dismisses the menu. */ .ctx-menu-overlay { position: fixed; inset: 0; z-index: 999; } .ctx-menu { position: fixed; z-index: 1000; min-width: 180px; padding: 4px; display: flex; flex-direction: column; background: var(--bit-clr-bg-pri); border: 1px solid var(--bit-clr-brd-ter); border-radius: var(--bit-shp-brd-radius); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); } </style> <BitDataGrid @ref="grid" TItem="Product" Items="@products" Height="420px" OnCellClick="OnCellClick" OnCellDoubleClick="OnCellDoubleClick" OnCellContextMenu="OnCellContextMenu"> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid> @if (cellMenuArgs is not null) { <div class="ctx-menu-overlay" @onclick="CloseCellMenu" @oncontextmenu="CloseCellMenu" @oncontextmenu:preventDefault="true"></div> <div class="ctx-menu" role="menu" style="left:@(cellMenuX)px;top:@(cellMenuY)px" @onkeydown="OnCellMenuKeyDown"> <div>@cellMenuArgs.Item.Name - @cellMenuArgs.ColumnTitle</div> <button type="button" role="menuitem" @ref="cellMenuFirstItem" @onclick="CopyCellValue">Copy value</button> <button type="button" role="menuitem" @onclick="DeleteCellMenuRow">Delete row</button> </div> }@code { private List<Product> products = SampleData.Generate(40); private BitDataGrid<Product>? grid; private BitDataGridCellEventArgs<Product>? cellMenuArgs; private int cellMenuX; private int cellMenuY; private ElementReference cellMenuFirstItem; private bool cellMenuFocusPending; private void OnCellClick(BitDataGridCellEventArgs<Product> e) { /* e.Item, e.ColumnTitle, e.Value */ } private void OnCellDoubleClick(BitDataGridCellEventArgs<Product> e) { /* e.Item, e.ColumnTitle, e.Value */ } private void OnCellContextMenu(BitDataGridCellEventArgs<Product> e) { cellMenuArgs = e; // ClientX/Y are viewport coordinates, matching the menu's position:fixed placement. cellMenuX = (int)e.Mouse.ClientX; cellMenuY = (int)e.Mouse.ClientY; // Move focus into the menu once it renders so keyboard users can operate/dismiss it. cellMenuFocusPending = true; } private void CloseCellMenu() => cellMenuArgs = null; private void OnCellMenuKeyDown(KeyboardEventArgs e) { if (e.Key == "Escape") CloseCellMenu(); } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (cellMenuFocusPending && cellMenuArgs is not null) { cellMenuFocusPending = false; await cellMenuFirstItem.FocusAsync(); } } private async Task CopyCellValue() { if (cellMenuArgs is null) return; await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", cellMenuArgs.Value?.ToString() ?? ""); cellMenuArgs = null; } private async Task DeleteCellMenuRow() { if (cellMenuArgs is null) return; products.Remove(cellMenuArgs.Item); cellMenuArgs = null; // The grid caches its processed view; mutating the bound list in place requires an explicit refresh. if (grid is not null) await grid.RefreshAsync(); } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
OnCellClick, OnCellDoubleClick and OnCellContextMenu
with the row, column and the underlying mouse event. Here OnCellContextMenu opens a custom
context menu at the pointer position (right-click any cell).
Click, double-click or right-click any cell.
Keyboard cell navigation
<BitDataGrid Items="@products" Height="460px" CellNavigation="true" Sortable="true" Editable="true" OnRowSave="(Product _) => {}" OnRowDelete="(Product p) => products.Remove(p)"> <BitDataGridColumn Property="p => p.Id" Title="ID" Editable="false" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(40); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
CellNavigation="true" to enable a roving tab stop. Use arrow keys, Home/End,
Ctrl+Home/End, PageUp/PageDown to move, Enter/F2 to edit (Esc to cancel), and Delete to
delete the focused row (requires Editable).
Variable row height
<BitDataGrid Items="@products" Height="480px" Sortable="true" RowHeightSelector="RowHeight"> <BitDataGridColumn Property="p => p.Name"> <Template Context="p"><strong>@p.Name</strong></Template> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(40); private float RowHeight(Product p) => p.Price > 500 ? 64f : 36f; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
RowHeightSelector a function that returns the desired height (in pixels) for a given row.
Here premium products (price over $500) get a taller row.
Empty state
<BitDataGrid Items="@items" Height="320px" Sortable="true"> <EmptyTemplate> <div>Nothing here yet. Try loading the sample data to populate the grid.</div> </EmptyTemplate> <Columns> <BitDataGridColumn Property="p => p.Id" Title="ID" /> <BitDataGridColumn Property="p => p.Name" /> </Columns> </BitDataGrid>@code { private List<Product> items = new(); // empty public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } }
EmptyTemplate to customize the placeholder shown when there is no data,
or rely on the built-in "No records to display" message.
Borders & Striping
<BitDataGrid Items="@products" Height="420px" Bordered="@bordered" Striped="@striped" Sortable="true" Pageable="true" PageSize="8"> <BitDataGridColumn Property="p => p.Id" Title="ID" Frozen="true" /> <BitDataGridColumn Property="p => p.Name" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(60); private bool bordered = true; private bool striped = true; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Bordered and Striped parameters.
RTL
<BitDataGrid Items="@products" Height="420px" Direction="BitDir.Rtl" Sortable="true" Pageable="true" PageSize="8"> <BitDataGridColumn Property="p => p.Id" Title="شناسه" Frozen="true" /> <BitDataGridColumn Property="p => p.Name" Title="نام" /> <BitDataGridColumn Property="p => p.Category" Title="دستهبندی"> <Template Context="product">@CategoryFa(product.Category)</Template> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Price" Title="قیمت" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" Title="موجودی" /> </BitDataGrid>@code { private List<Product> products = SampleData.GeneratePersian(60); private static string CategoryFa(Category category) => category switch { Category.Electronics => "الکترونیک", Category.Books => "کتاب", Category.Clothing => "پوشاک", Category.Home => "خانه", Category.Toys => "اسباببازی", Category.Sports => "ورزش", Category.Grocery => "خواربار", _ => category.ToString() }; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator that produces Persian sample data for the RTL demo. public static class SampleData { static readonly string[] Adjectives = { "فوقالعاده", "ممتاز", "اقتصادی", "هوشمند", "کلاسیک", "حرفهای", "کوچک", "بزرگ", "قدیمی", "مدرن", "لوکس", "فشرده" }; static readonly string[] Nouns = { "ویجت", "گجت", "بلندگو", "دفترچه", "ژاکت", "چراغ", "مخلوطکن", "پهپاد", "کولهپشتی", "کفش", "دوربین", "لیوان" }; static readonly string[] Suppliers = { "شرکت آلفا", "گلوبکس", "اینیتک", "آمبرلا", "سویلنت", "صنایع استارک", "شرکت وین", "ونکا" }; public static List<Product> GeneratePersian(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Direction parameter to BitDir.Rtl.
Filter operators
<BitDataGrid Items="@products" Height="430px" Filterable="true" FilterOperators="true" ShowToolbar="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> @* A column can override the grid-level setting and keep the fixed default filter. *@ <BitDataGridColumn Property="p => p.Supplier" FilterOperators="false" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" /> <BitDataGridColumn Property="p => p.ReleaseDate" Title="Released" Format="yyyy-MM-dd" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(150); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
FilterOperators to let users pick the comparison for each filter instead of the fixed
default: text columns offer contains / doesn't contain / starts with / ends with / = / ≠,
while number and date columns offer = ≠ > ≥ < ≤. Changing the operator re-applies the current filter text.
Individual columns can override the grid-level setting - the Supplier column opts out here,
keeping its fixed default filter.
Edit validation
<BitDataGrid Items="@products" Height="430px" Editable="true" KeyField="p => p.Id" Pageable="true" PageSize="8"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Editable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" Validate="(p, v) => ValidateName(p, v)" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Validate="(p, v) => ValidatePrice(p, v)" /> <BitDataGridColumn Property="p => p.Stock" Validate="(p, v) => ValidateStock(p, v)" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(30); // Validators receive the row and the proposed (type-converted) value. // Return an error message to reject the edit (blocking Save), or null to accept. private string? ValidateName(Product product, object? value) => string.IsNullOrWhiteSpace(value as string) ? "Name is required." : null; private string? ValidatePrice(Product product, object? value) => value is decimal price && price < 0 ? "Price cannot be negative." : null; private string? ValidateStock(Product product, object? value) => value is int stock && stock < 0 ? "Stock cannot be negative." : null; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Validate function to check proposed edits. While any editor holds an
invalid value, the message renders under it and Save is blocked. Edits are buffered - the underlying
object is only modified when Save is clicked, so Cancel always leaves the row untouched.
Try clearing the Name, or entering a negative Price or Stock.
State persistence
<BitStack Horizontal Wrap VerticalAlign="BitAlignment.Center"> <BitButton OnClick="SaveGridState">Save state</BitButton> <BitButton OnClick="RestoreGridState" IsEnabled="savedState is not null" Variant="BitVariant.Outline">Restore state</BitButton> <BitText>@stateStatus</BitText> </BitStack> <BitDataGrid @ref="grid" Items="@products" Height="430px" Sortable="true" Filterable="true" Resizable="true" Pageable="true" PageSize="10" ShowToolbar="true" ShowColumnChooser="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(120); private BitDataGrid<Product>? grid; private BitDataGridState? savedState; private string stateStatus = "Adjust the grid, then save its state."; private void SaveGridState() { // The snapshot is serializable - persist it to local storage or a user-preferences store. savedState = grid?.GetState(); stateStatus = savedState is null ? "Nothing to save yet." : $"Saved: page {savedState.CurrentPage}, {savedState.Sorts.Count} sort(s), {savedState.Filters.Count} filter(s)."; } private async Task RestoreGridState() { if (grid is null || savedState is null) return; await grid.ApplyStateAsync(savedState); stateStatus = "State restored."; } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
GetState() captures the user-adjustable state (page, page size, sorts, filters, groups and
column layout) as a serializable snapshot; ApplyStateAsync() restores it. Adjust the grid
(sort, filter, resize, hide columns, change page), save the state, shuffle things around, then restore.
Adjust the grid, then save its state.
Server-side virtualization
<BitDataGrid OnRead="LoadVirtualServerData" Virtualize="true" Height="480px" RowHeight="40" Sortable="true" Filterable="true" ShowFooter="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="90px" Frozen="true" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Width="240px" /> <BitDataGridColumn Property="p => p.Category" Width="140px" /> <BitDataGridColumn Property="p => p.Supplier" Width="180px" /> <BitDataGridColumn Property="p => p.Price" Width="140px" Format="C2" Aggregate="BitDataGridAggregateType.Sum" AggregateFormat="C0" /> <BitDataGridColumn Property="p => p.Stock" Width="120px" /> <BitDataGridColumn Property="p => p.Rating" Width="110px" Format="N1" FrozenEnd="true" /> </BitDataGrid>@code { private List<Product> all = SampleData.Generate(100_000); private async Task<BitDataGridReadResult<Product>> LoadVirtualServerData(BitDataGridReadRequest request) { // Simulate backend latency. Superseded scroll windows are cancelled by the grid; let the // OperationCanceledException propagate so the grid discards the stale read - returning an // empty result instead would be rendered as real data and blank the viewport. await Task.Delay(150, request.CancellationToken); IEnumerable<Product> query = all; // ...apply request.Filters and request.Sorts (see the Server-side data example)... var filtered = query.ToList(); var items = filtered.Skip(request.Skip).Take(request.Take ?? filtered.Count).ToList(); // Aggregates computed over the WHOLE filtered dataset (not just the returned window), // so the footer shows a real grand total instead of a per-window number. var priceSum = filtered.Sum(p => p.Price); var aggregates = new List<BitDataGridAggregateResult> { new() { ColumnId = nameof(Product.Price), Type = BitDataGridAggregateType.Sum, Value = priceSum, FormattedValue = priceSum.ToString("C0") } }; return new BitDataGridReadResult<Product>(items, filtered.Count) { Aggregates = aggregates }; } public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Virtualize with OnRead (and no paging) to browse an arbitrarily large
remote dataset: row windows are fetched on demand as you scroll - this example simulates 100,000 rows.
The footer shows server-provided aggregates (via BitDataGridReadResult.Aggregates)
computed over the whole filtered dataset, and the Rating column is pinned to the end edge
with FrozenEnd.
Localization
<BitDataGrid Items="@products" Height="420px" Direction="BitDir.Rtl" Strings="@persianStrings" Filterable="true" Pageable="true" PageSize="8"> <BitDataGridColumn Property="p => p.Id" Title="شناسه" Width="90px" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Title="نام" Width="220px" /> <BitDataGridColumn Property="p => p.Price" Title="قیمت" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" Title="موجودی" /> </BitDataGrid>@code { private List<Product> products = SampleData.GeneratePersian(60); // Every user-visible string has an English default; override what you need. private readonly BitDataGridStrings persianStrings = new() { EmptyText = "رکوردی برای نمایش وجود ندارد.", LoadingText = "در حال بارگذاری…", FilterPlaceholder = "فیلتر…", FilterAllText = "همه", PagerRangeFormat = "{0}–{1} از {2}", PagerPageFormat = "صفحهٔ {0} از {1}", PerPageFormat = "{0} در صفحه", RowsPerPageLabel = "تعداد ردیف در صفحه", FirstPageLabel = "صفحهٔ اول", PreviousPageLabel = "صفحهٔ قبل", NextPageLabel = "صفحهٔ بعد", LastPageLabel = "صفحهٔ آخر", ClearFiltersText = "حذف فیلترها", }; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator that produces Persian sample data for the RTL demo. public static class SampleData { static readonly string[] Adjectives = { "فوقالعاده", "ممتاز", "اقتصادی", "هوشمند", "کلاسیک", "حرفهای", "کوچک", "بزرگ", "قدیمی", "مدرن", "لوکس", "فشرده" }; static readonly string[] Nouns = { "ویجت", "گجت", "بلندگو", "دفترچه", "ژاکت", "چراغ", "مخلوطکن", "پهپاد", "کولهپشتی", "کفش", "دوربین", "لیوان" }; static readonly string[] Suppliers = { "شرکت آلفا", "گلوبکس", "اینیتک", "آمبرلا", "سویلنت", "صنایع استارک", "شرکت وین", "ونکا" }; public static List<Product> GeneratePersian(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Strings parameter
(a BitDataGridStrings). Assign a customized instance to localize the grid; this
example renders a Persian (RTL) grid.
IQueryable data source
<BitDataGrid Items="@products" Height="430px" Sortable="true" Filterable="true" Pageable="true" PageSize="10"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> <BitDataGridColumn Property="p => p.Supplier" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" /> </BitDataGrid>@code { // Any IQueryable works - filtering, sorting and paging are composed as expression trees the // provider executes at the source, so with EF Core this becomes SQL WHERE/ORDER BY/OFFSET // and only the current page is materialized: // private IQueryable<Product> products => dbContext.Products; private IQueryable<Product> products = SampleData.Generate(400).AsQueryable(); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
IQueryable<T> to Items and the grid translates filtering,
sorting and paging into expression trees composed onto the queryable - so a remote LINQ
provider such as EF Core executes them at the source (SQL WHERE/ORDER BY/OFFSET)
and only the current page is ever materialized. This example uses an in-memory queryable;
bind dbContext.Products the same way.
Excel export
@* ExcelExportStyled samples the grid's rendered theme (colors, striping, borders, fonts) from the live DOM at export time and bakes it into the workbook's styles. *@ <BitDataGrid Items="@products" Height="430px" Filterable="true" Pageable="true" PageSize="10" Striped="true" ShowToolbar="true" ShowCsvExport="true" ShowExcelExport="true" ExcelExportStyled="true"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Filterable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" /> <BitDataGridColumn Property="p => p.Discontinued" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(120); // Programmatic exports are also available: // string csv = await grid.ToCsvAsync(); // all matching rows in every data mode // byte[] xlsx = await grid.ToExcelAsync(); // real .xlsx workbook, no external library public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
ShowExcelExport adds a toolbar button that downloads the full (filtered/sorted)
data as a real .xlsx workbook - generated in-process with no external library.
Numbers and booleans are written as native cell types so spreadsheet formulas work on them,
and the workbook mirrors the grid's layout: a bold, frozen header row, the grid's column
widths, frozen columns as Excel freeze panes and ColSpan cells as merged cells.
With ExcelExportStyled the workbook also carries the grid's current visual
theme - the rendered header/row colors, the striped alternating background, the border
color and bold/italic fonts are sampled from the live DOM at export time, so whatever theme
is active (light, dark, custom) is what lands in the file.
Exports always cover all matching rows, not just the rendered ones: server and
infinite-scrolling modes fetch the full set through OnRead/OnLoadMore,
and tree mode includes collapsed branches.
Export with complex layouts
<BitDataGrid Items="@products" Height="460px" Bordered="true" Sortable="true" Filterable="true" ShowToolbar="true" ShowCsvExport="true" ShowExcelExport="true" ExcelExportStyled="true"> <DetailTemplate Context="p"> @* Detail content is presentation-only: exports cover the master rows. *@ <div><strong>Supplier:</strong> @p.Supplier · <strong>Rating:</strong> @p.Rating.ToString("N1")</div> </DetailTemplate> <Columns> @* Leading frozen columns (with the header row) become an Excel freeze pane; the exported columns keep their declared order in both formats. *@ <BitDataGridColumn Property="p => p.Id" Title="ID" Width="80px" Frozen="true" Filterable="false" /> @* A templated field column exports the raw field value, not the rendered markup. In Excel the ColSpan becomes a merged cell; in CSV it is flattened, so there the Category hidden under the span is exported too. *@ <BitDataGridColumn Property="p => p.Name" Width="220px" Frozen="true" ColSpan="p => NameSpan(p)"> <Template Context="p"> @if (p.Discontinued) { <span>⚠ @p.Name - discontinued</span> } else { @p.Name } </Template> </BitDataGridColumn> <BitDataGridColumn Property="p => p.Category" Width="170px" /> <BitDataGridColumn Property="p => p.Price" Width="150px" Format="C2" /> <BitDataGridColumn Property="p => p.Stock" Width="130px" /> <BitDataGridColumn Property="p => p.Supplier" Width="200px" /> @* A template-only column (no Field) has no exportable value and is skipped. *@ <BitDataGridColumn ColumnId="Value" Title="Value" Width="140px" SortBy="@(p => p.Price * p.Stock)"> <Template Context="p">@((p.Price * p.Stock).ToString("C0"))</Template> </BitDataGridColumn> </Columns> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(30); // In the Excel export this span becomes a merged cell (the covered Category cell stays empty); // the CSV export has no merge concept and writes every column's own value. private int? NameSpan(Product p) => p.Discontinued ? 2 : null; public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
DetailTemplate, frozen ID/Name columns and
ColSpan spanning (discontinued rows span Name over Category) - try scrolling,
expanding details and filtering, then export. The Excel file mirrors the layout:
the frozen ID/Name columns (plus the header row) become a freeze pane and each spanning
cell becomes a merged cell. The data stays faithful in both formats: frozen columns keep
their declared order, a templated field column (Name) exports the raw field value rather
than its markup, detail content is not exported (master rows only), and the template-only
Value column (no Field) is skipped. CSV has no layout concepts, so it
stays flat - there every column exports its own value, including the Category a span covers.
Lazy tree loading
<BitDataGrid Items="@roots" Height="430px" ChildrenProvider="LoadChildrenAsync" HasChildrenSelector="IsFolder" KeyField="n => n.Id" Sortable="false"> <BitDataGridColumn Property="p => p.Name" Width="280px" /> <BitDataGridColumn Property="p => p.Kind" Width="110px" /> <BitDataGridColumn Property="p => p.Size" Format="N0" /> <BitDataGridColumn Property="p => p.Modified" Format="yyyy-MM-dd" /> </BitDataGrid>@code { private List<FileNode> roots = new() { new() { Id = 1, Name = "src", Kind = "Folder" }, new() { Id = 2, Name = "docs", Kind = "Folder" }, new() { Id = 4, Name = "LICENSE", Kind = "File", Size = 1_070 }, }; // Unloaded nodes can't know whether they have children - this predicate decides // whether the expand toggle renders before the first fetch. private static bool IsFolder(FileNode node) => node.Kind == "Folder"; // Called once per node, on its first expand; the grid caches the result. private async Task<IEnumerable<FileNode>?> LoadChildrenAsync(FileNode parent) { return await httpClient.GetFromJsonAsync<List<FileNode>>($"api/files/{parent.Id}/children"); } public class FileNode { public int Id { get; set; } public string Name { get; set; } = ""; public string Kind { get; set; } = "Folder"; public long Size { get; set; } public DateTime Modified { get; set; } } }
ChildrenProvider (instead of ChildrenSelector) to fetch a node's
children asynchronously on first expand - e.g. from a backend - with the results cached for
later toggles. Pair it with HasChildrenSelector so unloaded nodes know whether to
render an expand toggle. Expanding a folder below simulates a 600 ms backend call.
Touch drag & drop
@* Row and column reordering automatically work with touch/pen input as well: a pointer-event fallback drives the same reorder pipeline where native HTML5 drag-and-drop is mouse-only. No extra configuration is needed. *@ <BitDataGrid Items="@products" Height="430px" RowReorderable="true" Reorderable="true" Sortable="false" KeyField="p => p.Id"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" /> <BitDataGridColumn Property="p => p.Name" Width="220px" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(12); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
Column virtualization
<BitDataGrid Items="@products" Height="430px" VirtualizeColumns="true" Virtualize="true" RowHeight="36" Sortable="false"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="80px" Frozen="true" /> <BitDataGridColumn Property="p => p.Name" Width="200px" /> @for (var m = 1; m <= 40; m++) { var month = m; <BitDataGridColumn ColumnId="@($"m{month}")" Title="@($"M{month:00}")" Width="110px"> <Template Context="product">@(((product.Id * 37 + month * 13) % 1000))</Template> </BitDataGridColumn> } </BitDataGrid>@code { // 3,000 rows x 42 columns - but only the visible window of rows AND columns exists in the DOM. // Column virtualization needs explicit pixel widths so the column window can be computed. private List<Product> products = SampleData.Generate(3_000); public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
VirtualizeColumns renders only the columns in (and near) the horizontal viewport,
replacing scrolled-out runs with spacer cells - for grids with very many columns. This example
declares 40 measurement columns (plus a frozen ID column) but only the visible window exists in
the DOM while you scroll horizontally. Combine it with row Virtualize for large
datasets in both dimensions. Columns need explicit pixel widths.
Programmatic control
<BitButton OnClick="SortByPrice">Sort by price ↓</BitButton> <BitButton OnClick="FilterExpensive">Filter price > 500</BitButton> <BitButton OnClick="GroupByCategory">Group by category</BitButton> <BitButton OnClick="GoToPage3">Go to page 3</BitButton> <BitDataGrid @ref="grid" Items="@products" Height="430px" Sortable="true" Filterable="true" Groupable="true" Pageable="true" PageSize="10"> <BitDataGridColumn Property="p => p.Id" Title="ID" Width="70px" Filterable="false" Groupable="false" /> <BitDataGridColumn Property="p => p.Name" Width="220px" Groupable="false" /> <BitDataGridColumn Property="p => p.Category" /> <BitDataGridColumn Property="p => p.Price" Format="C2" Groupable="false" /> <BitDataGridColumn Property="p => p.Stock" Groupable="false" /> </BitDataGrid>@code { private List<Product> products = SampleData.Generate(200); private BitDataGrid<Product>? grid; private async Task SortByPrice() => await grid!.SortByAsync(nameof(Product.Price), BitDataGridSortDirection.Descending); private async Task FilterExpensive() => await grid!.ApplyFilterAsync(nameof(Product.Price), BitDataGridFilterOperator.GreaterThan, 500m); private async Task GroupByCategory() => await grid!.GroupByAsync(nameof(Product.Category)); private async Task GoToPage3() => await grid!.GoToPageAsync(3); // Also available: ClearSortsAsync, ClearFilterAsync/ClearFiltersAsync, UngroupAsync/ClearGroupsAsync, // SetPageSizeAsync, RefreshAsync, GetState/ApplyStateAsync, ToCsvAsync/ToExcelAsync. public enum Category { Electronics, Books, Clothing, Home, Toys, Sports, Grocery } public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public Category Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public double Rating { get; set; } public bool Discontinued { get; set; } public DateTime ReleaseDate { get; set; } public string Supplier { get; set; } = ""; } // Deterministic generator so the demo data is reproducible. public static class SampleData { static readonly string[] Adjectives = { "Ultra", "Premium", "Eco", "Smart", "Classic", "Pro", "Mini", "Mega", "Vintage", "Modern", "Deluxe", "Compact" }; static readonly string[] Nouns = { "Widget", "Gadget", "Speaker", "Notebook", "Jacket", "Lamp", "Blender", "Drone", "Backpack", "Sneaker", "Camera", "Mug" }; static readonly string[] Suppliers = { "Acme Corp", "Globex", "Initech", "Umbrella", "Soylent", "Stark Industries", "Wayne Enterprises", "Wonka Inc" }; public static List<Product> Generate(int count, int seed = 42) { var rng = new Random(seed); var categories = Enum.GetValues<Category>(); var list = new List<Product>(count); var referenceDate = new DateTime(2024, 1, 1); for (int i = 1; i <= count; i++) { list.Add(new Product { Id = i, Name = $"{Adjectives[rng.Next(Adjectives.Length)]} {Nouns[rng.Next(Nouns.Length)]} {rng.Next(100, 999)}", Category = categories[rng.Next(categories.Length)], Price = Math.Round((decimal)(rng.NextDouble() * 990 + 5), 2), Stock = rng.Next(0, 500), Rating = Math.Round(rng.NextDouble() * 4 + 1, 1), Discontinued = rng.Next(0, 5) == 0, ReleaseDate = referenceDate.AddDays(-rng.Next(0, 2000)), Supplier = Suppliers[rng.Next(Suppliers.Length)] }); } return list; } } }
SortByAsync /
ClearSortsAsync, ApplyFilterAsync / ClearFiltersAsync,
GroupByAsync / UngroupAsync, GoToPageAsync,
SetPageSizeAsync and RefreshAsync.
API
BitDataGrid parameters
| Name | Type | Default value | Description |
|---|---|---|---|
| Items | IEnumerable<TItem>? | null | The data source bound to the grid for client-side processing. An IQueryable<T> (e.g. an EF Core DbSet) gets filtering/sorting/paging translated into expression trees the provider executes at the source, materializing only the current page. |
| OnRead | Func<BitDataGridReadRequest, Task<BitDataGridReadResult<TItem>>>? | null | Server-side data callback. When set, the grid delegates sort/filter/page/group to the caller. |
| OnLoadMore | Func<BitDataGridReadRequest, Task<BitDataGridReadResult<TItem>>>? | null | Infinite-scrolling data callback. Loads rows in batches and appends the next batch as the user scrolls toward the end. CSV/Excel exports issue a single request with Take = null ("all rows"), so the handler should honor a null Take. |
| LoadMoreBatchSize | int | 50 | Number of rows fetched per batch in infinite-scrolling mode. |
| ChildContent | RenderFragment? | null | Column definitions and other declarative children. |
| Columns | RenderFragment? | null | Alias of ChildContent, letting column definitions read declaratively as <Columns>...</Columns>. Both fragments are rendered when both are set. |
| Loading | bool | false | Shows a loading overlay while data is being fetched. |
| KeyField | Func<TItem, object>? | null | Optional key selector used for selection/edit identity. Defaults to reference equality. |
| ChildrenSelector | Func<TItem, IEnumerable<TItem>?>? | null | Child selector that turns the grid into a hierarchical tree grid. |
| ChildrenProvider | Func<TItem, Task<IEnumerable<TItem>?>>? | null | Async children provider for a lazily-loaded tree grid: children are fetched on a node's first expand (e.g. from a backend) and cached. Mutually exclusive with ChildrenSelector; pair with HasChildrenSelector. |
| HasChildrenSelector | Func<TItem, bool>? | null | Tells whether a node can have children before they are loaded, so unloaded lazy nodes render an expand toggle. Only used with ChildrenProvider. |
| TreeInitiallyExpanded | bool | false | When tree mode is active, controls whether nodes start expanded. Ignored in lazy mode (ChildrenProvider). |
| Class | string? | null | Custom CSS class for the root element. |
| Style | string? | null | Custom inline style for the root element. |
| Height | string? | null | Height of the scroll viewport, e.g. "480px". Required for virtualization and infinite scrolling. |
| Striped | bool | true | Renders alternate-row striping. |
| Hoverable | bool | true | Highlights the row under the pointer. |
| Bordered | bool | true | Renders cell borders. |
| ShowHeader | bool | true | Renders the header row. |
| ShowFooter | bool | false | Renders the footer/aggregate row. |
| Direction | BitDir | BitDir.Ltr | Text direction (LTR/RTL). |
| Sortable | bool | true | Enables column sorting by clicking headers. |
| MultiSort | bool | true | Enables multi-column sorting via Ctrl/⌘+click with priority badges. |
| Filterable | bool | false | Renders a per-column quick-filter row. |
| FilterOperators | bool | false | Shows an operator dropdown next to text/number/date filter editors so users pick the comparison (contains/starts with/=/≠/>/≥/</≤) instead of the fixed default. |
| Strings | BitDataGridStrings | new() | All user-visible strings rendered by the grid; assign a customized instance to localize the UI. |
| Resizable | bool | false | Enables column resizing by dragging header edges. |
| Reorderable | bool | false | Enables column reordering via drag-and-drop (mouse via native HTML5 DnD, touch/pen via a pointer-event fallback). |
| Groupable | bool | false | Enables grouping via a header button on groupable columns. |
| ShowToolbar | bool | false | Renders the toolbar area. |
| ShowColumnChooser | bool | false | Renders a column show/hide chooser in the toolbar. |
| ShowCsvExport | bool | false | Renders a CSV export button. The export covers all matching rows in every data mode, not just the rendered ones. |
| ShowExcelExport | bool | false | Renders an Excel (.xlsx) export button. The workbook is generated in-process with no external dependency, covers all matching rows in every data mode, and mirrors the grid's layout: bold frozen header row, column widths, leading frozen columns as a freeze pane and ColSpan cells as merged cells. |
| ExcelExportStyled | bool | false | When true, Excel exports also carry the grid's current visual theme: the rendered header/row colors, striped alternating rows, border color and bold/italic fonts are sampled from the live DOM at export time (so the active theme - including dark mode - lands in the workbook). Falls back to the plain bold-header styling when JS is unavailable (prerendering). |
| CellNavigation | bool | false | Enables keyboard cell navigation with a roving tabindex (arrows/Home/End/PageUp/PageDown to move, Enter/F2 to edit, Delete to delete the focused row when Editable). |
| RowReorderable | bool | false | Enables drag-and-drop row reordering (mouse, touch and pen; plus keyboard via the drag handle's arrow keys). |
| OnRowReorder | EventCallback<BitDataGridRowReorderEventArgs<TItem>> | Raised when a row is dropped onto another row during reordering. | |
| SelectionMode | BitDataGridSelectionMode | BitDataGridSelectionMode.None | How rows can be selected (None/Single/Multiple). |
| SelectedItems | IReadOnlyList<TItem>? | null | The selected items (supports two-way binding). |
| SelectedItemsChanged | EventCallback<IReadOnlyList<TItem>> | Raised when the selection changes. | |
| OnRowClick | EventCallback<TItem> | Raised when a row is clicked. | |
| OnCellClick | EventCallback<BitDataGridCellEventArgs<TItem>> | Raised when a data cell is clicked. | |
| OnCellDoubleClick | EventCallback<BitDataGridCellEventArgs<TItem>> | Raised when a data cell is double-clicked. | |
| OnCellContextMenu | EventCallback<BitDataGridCellEventArgs<TItem>> | Raised when a data cell is right-clicked. | |
| IsRowSelectionDisabled | Func<TItem, bool>? | null | Predicate returning true when a given row may not be selected. |
| Pageable | bool | false | Enables paging with a pager UI. |
| PageSize | int | 20 | The number of rows per page. |
| PageSizeOptions | int[] | { 10, 20, 50, 100 } | The page-size options offered in the pager dropdown. |
| PagerPosition | BitDataGridPagerPosition | BitDataGridPagerPosition.Bottom | Where the pager renders relative to the grid. |
| Virtualize | bool | false | Renders only the visible rows for large datasets. Requires a fixed Height and RowHeight. In server mode (OnRead) with paging off, row windows are fetched on demand as the user scrolls; with OnLoadMore, the accumulated batches are virtualized so the DOM stays bounded. |
| RowHeight | float | 36 | Uniform row height in pixels (required when virtualizing). |
| RowHeightSelector | Func<TItem, float>? | null | Optional per-row height selector (ignored while virtualizing). |
| VirtualizeColumns | bool | false | Renders only the columns in (and near) the horizontal viewport, replacing scrolled-out runs with spacers - for grids with very many columns. Requires explicit px column widths; not applied with column header groups or ColSpans. |
| Editable | bool | false | Enables inline editing with a command column. |
| NewItemFactory | Func<TItem>? | null | Factory used by the toolbar Add button to create a new row. |
| OnRowSave | EventCallback<TItem> | Raised when an edited row is saved. | |
| OnRowCancel | EventCallback<TItem> | Raised when an edit is cancelled. | |
| OnRowDelete | EventCallback<TItem> | Raised when a row is deleted (via the command column's Delete button or the Delete key in cell navigation). | |
| OnRowCreate | EventCallback<TItem> | Raised when a new row is created. | |
| EmptyTemplate | RenderFragment? | null | Custom content rendered when there is no data. |
| ToolbarTemplate | RenderFragment? | null | Custom content rendered in the toolbar's start area. |
| DetailTemplate | RenderFragment<TItem>? | null | Expandable master-detail content rendered under a row. |
BitDataGrid public members
| Name | Type | Default value | Description |
|---|---|---|---|
| RefreshAsync | Task | Recomputes the data view (filter → sort → group → page) and re-renders the grid. | |
| SortByAsync | Task | SortByAsync(columnId, direction, additive) - programmatically sorts by a column; BitDataGridSortDirection.None removes the sort. | |
| ClearSortsAsync | Task | Removes all active sorts and refreshes. | |
| ApplyFilterAsync | Task | ApplyFilterAsync(columnId, operator, value) - programmatically applies a filter, replacing any existing one on the column. | |
| ClearFilterAsync | Task | ClearFilterAsync(columnId) - removes the filter(s) applied to a column. | |
| ClearFiltersAsync | Task | Clears all active column filters and refreshes. | |
| GroupByAsync | Task | GroupByAsync(columnId) - adds the column as the next (nested) grouping level. | |
| UngroupAsync | Task | UngroupAsync(columnId) - removes the column's grouping level. | |
| ClearGroupsAsync | Task | Removes all active groupings and refreshes. | |
| GoToPageAsync | Task | GoToPageAsync(page) - navigates to the given 1-based page (clamped to the valid range). | |
| SetPageSizeAsync | Task | SetPageSizeAsync(size) - changes the page size and resets to the first page (without mutating the PageSize parameter). | |
| GetState | BitDataGridState | Captures the user-adjustable state (page, page size, sorts, filters, groups, column layout) as a serializable snapshot. | |
| ApplyStateAsync | Task | ApplyStateAsync(state) - restores a state snapshot captured by GetState. | |
| ExportCsvAsync | Task | Generates the full (filtered/sorted) data as CSV and triggers a client-side download. Covers all matching rows in every data mode - server/infinite modes fetch them through OnRead/OnLoadMore, tree mode includes collapsed branches. | |
| ToCsv | string | Builds a CSV string of the full dataset synchronously - tree mode includes collapsed branches, queryable mode covers all pages. Only server/infinite modes are limited to the loaded rows (their providers are async); use ToCsvAsync there. | |
| ToCsvAsync | Task<string> | Builds a CSV string of the full dataset - server/infinite modes issue an OnRead/OnLoadMore request with no paging (Take = null), tree mode includes collapsed branches. | |
| ToExcelAsync | Task<byte[]> | Generates the full (filtered/sorted) dataset as an Excel workbook (.xlsx), covering all matching rows in every data mode (like ToCsvAsync). The workbook mirrors the grid's layout: bold frozen header row, column widths, leading frozen columns as a freeze pane and ColSpan cells as merged cells; with ExcelExportStyled it also carries the grid's rendered theme (colors, striping, borders, fonts). | |
| ExportExcelAsync | Task | Generates the .xlsx workbook and triggers a client-side download. | |
| ActiveSorts | IReadOnlyList<BitDataGridSortDescriptor> | [] | The active sort descriptors, in priority order. |
| ActiveFilters | IReadOnlyList<BitDataGridFilterDescriptor> | [] | The active filter descriptors. |
| ActiveGroups | IReadOnlyList<BitDataGridGroupDescriptor> | [] | The active group descriptors, in nesting order. |
| TotalCount | int | 0 | Total number of rows in the current (filtered) view; the server-reported total in server mode. |
| TotalPages | int | 1 | Total number of pages while paging is active. |
| CurrentPage | int | 1 | The 1-based current page. |
| ExpandAllAsync | Task | Expands every node in the tree. No-op outside tree mode. | |
| CollapseAllAsync | Task | Collapses every node in the tree. No-op outside tree mode. |
BitDataGridColumn properties
Defines a column inside a BitDataGrid. Place these as child content of the grid.
| Name | Type | Default value | Description |
|---|---|---|---|
| Field | string? | null | Name of the property this column is bound to. Supports nested paths ("Address.City"). Prefer Property for a strongly typed, refactor-safe alternative. |
| Property | Expression<Func<TItem, object?>>? | null | Typed selector of the property this column is bound to, e.g. Property="p => p.Name". A strongly typed, refactor-safe alternative to Field that supports nested member chains (p => p.Address.City). Takes precedence over Field when both are set. |
| ColumnId | string? | null | Stable identifier for the column. Defaults to the resolved Property/Field path. |
| Title | string? | null | Header text. Defaults to a humanized Property/Field name. |
| Width | string? | null | CSS width, e.g. "120px" or "20%". When null the column shares remaining space. |
| MinWidth | int | 60 | Minimum width in pixels the column can be resized to. |
| MaxWidth | int? | null | Maximum width in pixels the column can be resized to. |
| Sortable | bool? | null | Overrides the grid-level Sortable for this column. |
| SortBy | Func<TItem, object?>? | null | Custom sort key selector. Enables sorting for template-only columns and overrides the field value as the sort key (client mode). |
| SortDescendingFirst | bool | false | When true, the first click on the header sorts descending instead of ascending. |
| Validate | Func<TItem, object?, string?>? | null | Validator for inline edits: receives the row and the proposed value, returns an error message to reject it (blocking Save) or null to accept. |
| Filterable | bool? | null | Overrides the grid-level Filterable for this column. |
| FilterOperators | bool? | null | Overrides the grid-level FilterOperators (the operator dropdown next to this column's filter editor). |
| Resizable | bool? | null | Overrides the grid-level Resizable for this column. |
| Reorderable | bool? | null | Overrides the grid-level Reorderable for this column. |
| Editable | bool? | null | Overrides the grid-level Editable for this column. |
| Groupable | bool? | null | Overrides the grid-level Groupable for this column. |
| Frozen | bool | false | Pins the column to the start edge so it stays visible while scrolling horizontally. |
| FrozenEnd | bool | false | Pins the column to the end edge (right in LTR, left in RTL). Typical for action/status columns. |
| Group | string? | null | Optional header group name. Consecutive columns sharing the same value render under a single spanning header cell. |
| ColSpan | Func<TItem, int?>? | null | Optional per-row column span. |
| Visible | bool | true | Whether the column is visible. |
| Align | BitDataGridColumnAlign | BitDataGridColumnAlign.Left | Horizontal alignment of cell content. |
| Format | string? | null | A .NET format string applied to the value (e.g. "C2", "yyyy-MM-dd"). |
| DataType | BitDataGridColumnDataType | BitDataGridColumnDataType.Auto | The data type used to pick the editor/filter. |
| Aggregate | BitDataGridAggregateType | BitDataGridAggregateType.None | The footer/group aggregate function. |
| AggregateBy | Func<IReadOnlyList<TItem>, object?>? | null | Custom aggregate function for computations beyond the built-ins (e.g. distinct count). Takes precedence over Aggregate. |
| AggregateFormat | string? | null | Format string for the aggregate value. Falls back to Format. |
| HeaderClass | string? | null | Custom CSS class applied to the header cell. |
| CellClass | string? | null | Custom CSS class applied to each data cell. |
| Template | RenderFragment<TItem>? | null | Custom rendering for a data cell. |
| HeaderTemplate | RenderFragment? | null | Custom rendering for the header cell content. |
| EditTemplate | RenderFragment<TItem>? | null | Custom editor rendered when the row/cell is in edit mode. |
| FooterTemplate | RenderFragment<BitDataGridAggregateResult>? | null | Custom rendering for the footer/aggregate cell. |
BitDataGridReadRequest properties
Describes the data the grid needs from a server-side/infinite source (passed to OnRead/OnLoadMore).
| Name | Type | Default value | Description |
|---|---|---|---|
| Skip | int | 0 | Zero-based number of items to skip. |
| Take | int? | null | Maximum number of items to return (null means all). |
| Sorts | IReadOnlyList<BitDataGridSortDescriptor> | [] | The active sort descriptors ordered by priority. |
| Filters | IReadOnlyList<BitDataGridFilterDescriptor> | [] | The active filter descriptors. |
| Groups | IReadOnlyList<BitDataGridGroupDescriptor> | [] | The active group descriptors in nesting order, letting a server-side handler reconstruct the grouping. Empty when no grouping is active. |
| CancellationToken | CancellationToken | A token that is cancelled when the request is superseded by a newer one. |
BitDataGridReadResult<TItem> properties
Result returned from a grid's OnRead/OnLoadMore callback.
| Name | Type | Default value | Description |
|---|---|---|---|
| Items | IReadOnlyList<TItem> | The items for the current page/window. | |
| TotalCount | int | The total number of items matching the current filters (ignored in infinite mode). | |
| Aggregates | IReadOnlyList<BitDataGridAggregateResult>? | null | Optional aggregates computed by the data source over the whole filtered dataset; when provided, the footer shows these instead of aggregating the current page locally. |
BitDataGridCellEventArgs<TItem> properties
Arguments passed to cell-level event callbacks.
| Name | Type | Default value | Description |
|---|---|---|---|
| Item | TItem | The row item. | |
| Column | BitDataGridColumn<TItem> | The column the cell belongs to. | |
| ColumnId | string | The column field/identifier. | |
| ColumnTitle | string | The column's display title. | |
| Value | object? | null | The raw value of the cell. |
| Mouse | MouseEventArgs | The underlying browser mouse event. |
BitDataGridRowReorderEventArgs<TItem> properties
Arguments raised when a row is reordered via drag-and-drop.
| Name | Type | Default value | Description |
|---|---|---|---|
| DraggedItem | TItem | The dragged row item. | |
| TargetItem | TItem | The drop-target row item. | |
| FromIndex | int? | The original index of the dragged item, or null when the bound Items is not an indexable list. | |
| ToIndex | int? | The destination index, or null when the bound Items is not an indexable list. |
BitDataGridSortDescriptor properties
Describes the sort state applied to a single column (found on BitDataGridReadRequest.Sorts).
| Name | Type | Default value | Description |
|---|---|---|---|
| ColumnId | string | The identifier of the column being sorted. | |
| Direction | BitDataGridSortDirection | BitDataGridSortDirection.Ascending | The sort direction. |
| Priority | int | int.MaxValue | Priority for multi-column sorting (1 = primary). |
BitDataGridFilterDescriptor properties
Describes a filter applied to a single column (found on BitDataGridReadRequest.Filters).
| Name | Type | Default value | Description |
|---|---|---|---|
| ColumnId | string | The identifier of the column being filtered. | |
| Operator | BitDataGridFilterOperator | BitDataGridFilterOperator.Contains | The comparison operator applied to the value. |
| Value | object? | null | The value compared against the column's cell value. |
BitDataGridGroupDescriptor properties
Describes a grouping applied to a column.
| Name | Type | Default value | Description |
|---|---|---|---|
| ColumnId | string | The identifier of the column being grouped. | |
| Direction | BitDataGridSortDirection | BitDataGridSortDirection.Ascending | The sort direction applied to the group keys. |
BitDataGridAggregateResult properties
Holds the computed aggregate value for a column footer or group (passed to a column's FooterTemplate).
| Name | Type | Default value | Description |
|---|---|---|---|
| ColumnId | string | The identifier of the aggregated column. | |
| Type | BitDataGridAggregateType | The aggregate function that produced the value. | |
| Value | object? | null | The raw aggregate value. |
| FormattedValue | string | string.Empty | The aggregate value formatted using the column's AggregateFormat/Format. |
BitDataGridState properties
A serializable snapshot of the grid's user-adjustable state, captured with GetState() and restored with ApplyStateAsync(). Enables persisting grid state across sessions.
| Name | Type | Default value | Description |
|---|---|---|---|
| CurrentPage | int | 1 | The 1-based current page. |
| PageSize | int? | null | The user-selected page size, or null when the grid's PageSize parameter applies. |
| Sorts | List<BitDataGridSortDescriptor> | [] | The active sort descriptors. |
| Filters | List<BitDataGridFilterDescriptor> | [] | The active filter descriptors. |
| Groups | List<BitDataGridGroupDescriptor> | [] | The active group descriptors. |
| Columns | List<BitDataGridColumnState> | [] | Per-column layout state. |
BitDataGridColumnState properties
A per-column layout entry inside BitDataGridState.Columns.
| Name | Type | Default value | Description |
|---|---|---|---|
| ColumnId | string | string.Empty | The column's stable identifier (ColumnId or Field). |
| Visible | bool | true | Whether the column is shown (column-chooser state). |
| Width | double? | null | The resized width in pixels, or null when the column was never resized. |
| Order | int | 0 | The display position among all columns. |
BitDataGridStrings properties
All user-visible (and screen-reader) strings rendered by the grid, defaulting to English. Assign a customized instance to the Strings parameter to localize - including empty/loading texts, pager texts, filter placeholders and operators, edit buttons, aggregate labels, aria-labels and live-region announcements.
| Name | Type | Default value | Description |
|---|---|---|---|
| EmptyText | string | "No records to display." | Shown when the grid has no rows to display. |
| LoadingText | string | "Loading…" | Shown while Loading is true. |
| PagerRangeFormat | string | "{0}–{1} of {2}" | Pager range summary format. |
| PagerPageFormat | string | "Page {0} of {1}" | Pager page summary format. |
| InvalidValueError | string | "Invalid value for {0}." | Error shown when an edited value can't be converted to the column's type. |
| … | string | Plus ~40 more: toolbar/edit button texts, filter placeholder and operator labels, boolean/enum option texts, group/detail/tree/reorder aria-labels, aggregate label formats and aria-live announcements. |
BitDataGridColumnAlign enum
| Name | Value | Description |
|---|---|---|
| Left | 0 | |
| Center | 1 | |
| Right | 2 |
BitDataGridSortDirection enum
| Name | Value | Description |
|---|---|---|
| None | 0 | |
| Ascending | 1 | |
| Descending | 2 |
BitDataGridSelectionMode enum
| Name | Value | Description |
|---|---|---|
| None | 0 | |
| Single | 1 | |
| Multiple | 2 |
BitDataGridAggregateType enum
| Name | Value | Description |
|---|---|---|
| None | 0 | |
| Sum | 1 | |
| Average | 2 | |
| Count | 3 | |
| Min | 4 | |
| Max | 5 | |
| Custom | 6 | The value was produced by the column's custom AggregateBy delegate rather than a built-in function. |
BitDataGridPagerPosition enum
| Name | Value | Description |
|---|---|---|
| Bottom | 0 | |
| Top | 1 | |
| TopAndBottom | 2 |
BitDir enum
| Name | Value | Description |
|---|---|---|
| Ltr | 0 | |
| Rtl | 1 | |
| Auto | 2 |
BitDataGridColumnDataType enum
| Name | Value | Description |
|---|---|---|
| Auto | 0 | |
| Text | 1 | |
| Number | 2 | |
| Boolean | 3 | |
| Date | 4 | |
| DateTime | 5 | |
| DateTimeOffset | 6 | |
| Enum | 7 |
BitDataGridFilterOperator enum
| Name | Value | Description |
|---|---|---|
| Unspecified | 0 | |
| Contains | 1 | |
| DoesNotContain | 2 | |
| StartsWith | 3 | |
| EndsWith | 4 | |
| Equals | 5 | |
| NotEquals | 6 | |
| GreaterThan | 7 | |
| GreaterThanOrEqual | 8 | |
| LessThan | 9 | |
| LessThanOrEqual | 10 | |
| IsEmpty | 11 | |
| IsNotEmpty | 12 |
Feedback
You can give us your feedback through our GitHub repo by filing a new Issue or starting a new Discussion.
Or you can review / edit this page on GitHub.