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

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

FileResponsibility
ObservableBindingList.csCore fields, property cache, tracking dictionaries
ObservableBindingList.Tracking.csEntity state tracking (Added, Modified, Deleted, Unchanged)
ObservableBindingList.Sort.csProperty-based sorting with direction support
ObservableBindingList.Filter.csString and expression-based filtering
ObservableBindingList.Pagination.csPage navigation and size management
ObservableBindingList.Validation.csData annotation and custom validation
ObservableBindingList.UndoRedo.csGranular undo/redo stack
ObservableBindingList.VirtualLoading.csLarge dataset lazy loading
ObservableBindingList.Aggregates.csSum, Average, Min, Max, CountWhere, GroupBy
ObservableBindingList.Computed.csComputed column support
ObservableBindingList.Bookmarks.csNavigation bookmarks
ObservableBindingList.ThreadSafety.csReader-writer lock synchronization
ObservableBindingList.Export.csJSON and CSV export

Core Features

Properties

PropertyTypeDescription
TrackingsList<Tracking>Change tracking records (O(1) by GUID)
PageSizeintItems per page (default: 20)
CurrentPageintActive page number
TotalPagesintTotal page count
IsSortedboolData is sorted
SuppressNotificationboolSuppress change notifications

Events

EventDescription
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
PropertyChangedStandard 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: ',');