ObservableBindingList<T>
Enhanced binding list with sorting, filtering, pagination, change tracking, undo/redo, and validation
Source of truth (2026)
DataManagementModelsStandard/ObservableBindingList/ObservableBindingList.cs.../ObservableBindingList.Tracking.cs.../ObservableBindingList.Validation.cs.../ObservableBindingList.UndoRedo.cs.../ObservableBindingList.VirtualLoading.cs.../ObservableBindingList.Sort.cs.../ObservableBindingList.Filter.cs.../ObservableBindingList.Pagination.cs.../ObservableBindingList.Export.cs- 26 partial class files total
Table of Contents
Overview
ObservableBindingList<T> is the single source of truth collection used by UnitofWork<T>. It extends BindingList<T> and implements IBindingListView, INotifyCollectionChanged, and IDisposable.
public partial class ObservableBindingList<T> : BindingList<T>, IBindingListView, INotifyCollectionChanged, IDisposable
where T : class, INotifyPropertyChanged, new()
26 partial files covering: tracking, CRUD, sort, filter, pagination, validation, undo/redo, virtual loading, aggregates, computed columns, bookmarks, thread safety, export, search, and more.
Architecture
| File | Responsibility |
|---|---|
ObservableBindingList.cs | Core fields, property cache, tracking dictionaries |
ObservableBindingList.Tracking.cs | Entity state tracking (Added, Modified, Deleted, Unchanged) |
ObservableBindingList.Sort.cs | Property-based sorting with direction support |
ObservableBindingList.Filter.cs | String and expression-based filtering |
ObservableBindingList.Pagination.cs | Page navigation and size management |
ObservableBindingList.Validation.cs | Data annotation and custom validation |
ObservableBindingList.UndoRedo.cs | Granular undo/redo stack |
ObservableBindingList.VirtualLoading.cs | Large dataset lazy loading |
ObservableBindingList.Aggregates.cs | Sum, Average, Min, Max, CountWhere, GroupBy |
ObservableBindingList.Computed.cs | Computed column support |
ObservableBindingList.Bookmarks.cs | Navigation bookmarks |
ObservableBindingList.ThreadSafety.cs | Reader-writer lock synchronization |
ObservableBindingList.Export.cs | JSON and CSV export |
Core Features
Properties
| Property | Type | Description |
|---|---|---|
Trackings | List<Tracking> | Change tracking records (O(1) by GUID) |
PageSize | int | Items per page (default: 20) |
CurrentPage | int | Active page number |
TotalPages | int | Total page count |
IsSorted | bool | Data is sorted |
SuppressNotification | bool | Suppress change notifications |
Events
| Event | Description |
|---|---|
ItemAdded<T> | Item added to collection |
ItemRemoved<T> | Item removed from collection |
ItemChanged<T> | Item property changed |
ItemValidating<T> | Before item validation |
ItemDeleting<T> | Before item deletion |
PropertyChanged | Standard INotifyPropertyChanged |
Change Tracking
Dual-indexed tracking with O(1) lookup:
// O(1) lookup by Guid
private Dictionary<Guid, Tracking> _trackingsByGuid;
// O(1) lookup by original index
private Dictionary<int, Tracking> _trackingsByOriginalIndex;
// Tracking state per entity
public class Tracking
{
public Guid UniqueId { get; set; }
public int OriginalIndex { get; set; }
public EntityState EntityState { get; set; } // Added, Modified, Deleted, Unchanged
public string ModifiedBy { get; set; }
public DateTime ModifiedAt { get; set; }
}
Usage
var list = new ObservableBindingList<Customer>(customers);
// Check tracking
foreach (var tr in list.Trackings)
{
Console.WriteLine($"Item {tr.OriginalIndex}: {tr.EntityState} by {tr.ModifiedBy}");
}
// Get added entities
var added = list.Trackings.Where(t => t.EntityState == EntityState.Added);
// Get modified entities
var modified = list.Trackings.Where(t => t.EntityState == EntityState.Modified);
Sorting & Filtering
var list = new ObservableBindingList<Customer>(customers);
// Sort
list.ApplySort("Name", ListSortDirection.Ascending);
list.ApplySort("CreatedDate", ListSortDirection.Descending);
// Filter
list.Filter = "Name LIKE 'John*' OR City = 'NYC'";
list.Filter = "Status = 'Active'";
// Clear filter
list.RemoveFilter();
Pagination
list.PageSize = 50;
// Navigate pages
list.GoToPage(1); // First page
list.GoToPage(3); // Page 3
list.NextPage();
list.PreviousPage();
// Info
Console.WriteLine($"Page {list.CurrentPage} of {list.TotalPages}");
Console.WriteLine($"Items: {list.Count}");
Validation
// Data annotations on entity
public class Customer : INotifyPropertyChanged
{
[Required, StringLength(100)]
public string Name { get; set; }
[Required, EmailAddress]
public string Email { get; set; }
[Range(0, 200)]
public int Age { get; set; }
}
// Validate
var list = new ObservableBindingList<Customer>(customers);
var result = list.ValidateAll();
if (!result.IsValid)
{
foreach (var error in result.Errors)
Console.WriteLine($"{error.PropertyName}: {error.Message}");
}
Undo/Redo
list.EnableUndoRedo(maxDepth: 50);
customer.Name = "Changed";
list.Undo(); // Name reverts
list.Redo(); // Restores change
// Check state
if (list.CanUndo)
list.UndoLastChange();
if (list.CanRedo)
list.RedoLastChange();
list.ClearUndoHistory();
Virtual Loading
// Enable virtual loading for large datasets
list.EnableVirtualMode(totalCount: 100000);
await list.GoToPageAsync(5);
await list.PrefetchAdjacentPagesAsync();
// Page cache
list.PageCacheSize = 5; // Keep 5 pages cached
list.InvalidatePageCache();
list.DisableVirtualMode();
Aggregates & Computed
// Aggregates
var total = list.Sum("Price");
var avg = list.Average("Price");
var max = list.Max("Price");
var min = list.Min("Price");
var count = list.CountWhere(c => c.IsActive);
var groups = list.GroupBy("Category");
var distinct = list.DistinctValues("City");
// Computed columns
list.RegisterComputed("FullName", c => $"{c.FirstName} {c.LastName}");
list.RegisterComputed("Total", c => c.Price * c.Quantity);
var fullName = list.GetComputed(customer, "FullName");
Usage Examples
Data Binding (WinForms/WPF)
var list = new ObservableBindingList<Customer>(customers);
dataGridView.DataSource = list; // Automatic two-way binding
Export
// Export to DataTable
var dt = list.ToDataTable();
// Export to JSON
using var stream = new MemoryStream();
await list.ToJsonAsync(stream);
// Export to CSV
await list.ToCsvAsync(stream, delimiter: ',');