Filtering System
A complete, decoupled filter architecture: BeepFilter builds the filter UI and emits a FilterConfiguration event; consuming controls (BeepGridPro, BeepListBox) apply it through the generic FilterEngine<T>. Eight interaction styles, 17 operators, drag-drop, validation, smart suggestions, and full keyboard support.
Table of Contents
Architecture Overview
The filtering system is split into two clearly separated responsibilities:
| Layer | Class | Responsibility |
|---|---|---|
| UI Builder | BeepFilter | Renders filter UI, accepts user input, emits FilterConfiguration via events — never touches data. |
| Data Applier | FilterEngine<T> | Evaluates a FilterConfiguration against an IEnumerable<T> — no UI involved. |
| Consumer | BeepGridPro / BeepListBox | Subscribes to FilterApplied, calls FilterEngine<T>, updates visible rows/items. |
+-----------------------------+
| BeepFilter | UI only: takes EntityStructure,
| (UI Builder) | shows filter widgets (TagPills, etc.)
| | Raises FilterApplied event -----+
+-----------------------------+ |
| FilterConfiguration
+------------------v-----------+
| BeepGridPro / BeepListBox |
| (Consumer) |
| Uses FilterEngine<T> |
+------------------------------+
FilterConfiguration and fires an event. The consuming control decides how and when to apply it. This makes BeepFilter reusable with any data source.
BeepFilter Control
Inherits BaseControl. Hosts one active IFilterPainter which handles all rendering and layout. Switching FilterStyle hot-swaps the painter at runtime.
Key Private Fields
| Field | Type | Purpose |
|---|---|---|
_filterStyle | FilterStyle | Current interaction pattern (default: TagPills). |
_activePainter | IFilterPainter? | Live painter instance; replaced when style changes. |
_activeFilter | FilterConfiguration | Living filter being built by the user. |
_hitAreas | List<FilterHitArea> | All clickable regions registered by the painter. |
_keyboardHandler | FilterKeyboardHandler? | Handles keyboard shortcuts (Add, Clear, Navigate). |
_suggestionProvider | FilterSuggestionProvider? | Provides recent/frequent value suggestions. |
_validationHelper | FilterValidationHelper? | Validates criteria type/operator compatibility. |
_autocompletePopup | FilterAutocompletePopup? | Created on demand for value input autocomplete. |
_inlineValueEditor | BeepTextBox? | Inline text editor that appears over a criterion. |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
FilterStyle | FilterStyle | TagPills | Changes the painter; triggers layout recalculation + FilterStyleChanged event. |
DisplayMode | FilterDisplayMode | AlwaysVisible | Whether filter bar is always visible, collapsible, shown on hover, or modal. |
Position | FilterPosition | Top | Position hint for parent layout (Top, Bottom, Left, Right, Floating). |
ActiveFilter | FilterConfiguration | empty list | The current filter being built; consumer reads this in FilterApplied handler. |
EntityStructure | EntityStructure | null | Provides column names and types for the field selector dropdowns. |
DataSource | object | null | Optional data source forwarded to FilterSuggestionProvider for value hints. |
IsExpanded | bool | true | Controls collapsed/expanded state for Collapsible display mode. |
FilterCount | int | 0 | Read-only count of enabled criteria; useful for badge display. |
Initialization Sequence
BeepFilter()
: base()
|- InitializeComponent() // Size=600x45, DoubleBuffered, ApplyThemeToChilds=false
+- InitializeFilterControl()
|- UpdatePainter() // Creates TagPillsFilterPainter (default)
|- _activeFilter = new FilterConfiguration { Criteria = [] }
|- InitializePhase1Components()
| |- _keyboardHandler = new FilterKeyboardHandler(this)
| |- _suggestionProvider = new FilterSuggestionProvider()
| +- _validationHelper = new FilterValidationHelper()
+- RecalculateLayout()
Filter Styles
FilterStyle controls the interaction pattern — how the user builds criteria. Visual colors, borders, and spacing always come from BeepStyling / BeepControlStyle, not from the style enum.
| Value | Painter Class | Visual Pattern | Best For |
|---|---|---|---|
TagPills |
TagPillsFilterPainter |
Horizontal row of rounded pill chips with × remove button; drag handles for reordering. | Simple, at-a-glance active filters bar. |
GroupedRows |
GroupedRowsFilterPainter |
One row per criterion with AND/OR connectors; Add Filter / Add Group buttons; indented subgroups. | Multi-condition structured queries. |
QueryBuilder |
QueryBuilderFilterPainter |
Visual logic tree with If/Else branches; Prompt vs Logic toggle. | Complex nested conditions and business rules. |
DropdownMultiSelect |
DropdownMultiSelectFilterPainter |
Field / Operator / Value dropdowns; multi-select checkboxes; active count badge. | User-friendly quick filtering with fixed column sets. |
InlineRow |
InlineRowFilterPainter |
Single compact row: Column | Operator | Value. Minimal spacing. | Space-constrained panels; table header filter rows. |
SidebarPanel |
SidebarPanelFilterPainter |
Vertical list of collapsible filter categories with checkboxes/toggles. Apply/Clear at bottom. | E-commerce / catalog browsing style. |
QuickSearch |
QuickSearchFilterPainter |
Single search bar with column selector. Real-time filtering. Filter count badge. | Fast free-text search across all columns. |
AdvancedDialog |
AdvancedDialogFilterPainter |
Modal or slide-in panel with tabs; Save/Load config; result count preview. | Power users; reusable named filter sets. |
Switching Styles at Runtime
// Painter is hot-swapped; existing ActiveFilter is preserved
beepFilter.FilterStyle = FilterStyle.GroupedRows;
// Get description from factory
string desc = FilterPainterFactory.GetStyleDescription(FilterStyle.QuickSearch);
// "Single search bar with instant filtering - quick and easy"
bool ready = FilterPainterFactory.IsFullyImplemented(FilterStyle.AdvancedDialog); // true
Data Models
FilterCriteria
One condition in the filter: ColumnName Operator Value.
| Property | Type | Description |
|---|---|---|
ColumnName | string | Column / property to filter on. |
Operator | FilterOperator | Comparison operator (one of 17). |
Value | object | Primary comparison value. |
Value2 | object | Upper bound for Between / NotBetween. |
CaseSensitive | bool | Case-sensitive text comparison. Default false. |
IsEnabled | bool | Skip this criterion when false without removing it. Default true. |
// Simple
var c1 = new FilterCriteria("Country", FilterOperator.Equals, "Germany");
// Range
var c2 = new FilterCriteria("Price", FilterOperator.Between, 10.0, 50.0);
// Case-sensitive text
var c3 = new FilterCriteria("Code", FilterOperator.StartsWith, "ABC", caseSensitive: true);
// Disabled (remembered but not evaluated)
c3.IsEnabled = false;
FilterConfiguration
A named, serialisable collection of FilterCriteria with AND / OR logic.
| Property | Type | Description |
|---|---|---|
Name | string | Human-readable name for saving / loading. |
Description | string | Optional description. |
Criteria | List<FilterCriteria> | Ordered list of conditions. |
Logic | FilterLogic | And (all must match) or Or (any match). Default And. |
IsActive | bool | Whether this configuration is currently applied. |
CreatedDate | DateTime | Auto-set on construction. |
ModifiedDate | DateTime | Should be updated on every change. |
var config = new FilterConfiguration("Q1 EU Customers")
{
Logic = FilterLogic.And
};
config.AddCriteria(new FilterCriteria("Region", FilterOperator.Equals, "EU"));
config.AddCriteria(new FilterCriteria("Revenue", FilterOperator.GreaterThan, 50000));
// Serialize / deserialize (System.Text.Json compatible)
string json = JsonSerializer.Serialize(config);
var loaded = JsonSerializer.Deserialize<FilterConfiguration>(json);
Filter Operators
17 operators covering string, numeric, date, null-check, regex, and set membership comparisons.
| Operator | Display Name | Applicable To | Notes |
|---|---|---|---|
Equals | = | All types | Exact match; case-insensitive for strings unless CaseSensitive=true. |
NotEquals | ≠ | All types | Inverse of Equals. |
Contains | contains | String | Substring search. Case-insensitive by default. |
NotContains | does not contain | String | Inverse of Contains. |
StartsWith | starts with | String | Prefix match. |
EndsWith | ends with | String | Suffix match. |
GreaterThan | > | Numeric, Date | Strict greater than. |
GreaterThanOrEqual | ≥ | Numeric, Date | |
LessThan | < | Numeric, Date | Strict less than. |
LessThanOrEqual | ≤ | Numeric, Date | |
Between | between | Numeric, Date | Inclusive. Requires both Value and Value2. |
NotBetween | not between | Numeric, Date | Inverse of Between. |
IsNull | is empty | All types | Matches null or empty string. |
IsNotNull | is not empty | All types | Inverse of IsNull. |
Regex | matches pattern | String | Full .NET regex pattern. |
In | is one of | All types | Value should be IEnumerable or comma-separated string. |
NotIn | is not one of | All types | Inverse of In. |
FilterLogic
| Value | Meaning |
|---|---|
FilterLogic.And | All enabled criteria must match (default — most restrictive). |
FilterLogic.Or | At least one enabled criterion must match. |
FilterEngine<T>
Generic, reflection-based engine that evaluates a FilterConfiguration against any IEnumerable<T>. Property info is cached on construction for performance.
Core Methods
| Method | Returns | Description |
|---|---|---|
ApplyFilter(FilterConfiguration config) |
IEnumerable<T> |
Filters data source using all enabled criteria. Returns full source if config is null or has no enabled criteria. |
ApplyQuickFilter(string text, string? prop) |
IEnumerable<T> |
Fast substring search. Pass null for prop to search all properties. |
ApplyFilterGetIndices(FilterConfiguration config) |
List<int> |
Returns 0-based indices of matched items. Useful for grid row hiding without rebuilding collections. |
// Works with any typed collection
var engine = new FilterEngine<Customer>(customers);
// Works with ExpandoObject (BeepGridPro uses this internally)
var engine = new FilterEngine<ExpandoObject>(gridRows);
// Option A: get filtered rows
var visible = engine.ApplyFilter(config).ToList();
// Option B: get matching indices (faster for large virtual grids)
var indices = engine.ApplyFilterGetIndices(config);
HideRowsNotIn(indices);
// Quick search across all properties
var results = engine.ApplyQuickFilter("Smith", propertyName: null);
Painter System
IFilterPainter Interface
Every filter style is rendered by a dedicated painter. Painters return FilterLayoutInfo (all element rectangles) and paint using BeepStyling helpers for colors and borders.
| Member | Type | Description |
|---|---|---|
FilterStyle | FilterStyle | The style this painter handles. |
SupportsAnimations | bool | Whether the painter supports animated transitions. |
SupportsDragDrop | bool | Whether drag-and-drop reordering is supported. |
CalculateLayout(BeepFilter, Rectangle) | FilterLayoutInfo | Measures and positions all UI elements; registers hit areas. |
Paint(Graphics, BeepFilter, FilterLayoutInfo) | void | Renders the filter UI using the supplied layout. |
Painter Implementations
| Class | Style | DragDrop | Special Features |
|---|---|---|---|
TagPillsFilterPainter | TagPills | Yes | Pill height 32px, corner radius 16, max 10/row; wraps to new row; remove × per pill. |
GroupedRowsFilterPainter | GroupedRows | No | AND/OR connector labels; Add Filter + Add Group buttons; indented subgroups. |
QueryBuilderFilterPainter | QueryBuilder | No | Visual If/Else tree; Prompt vs Logic mode toggle. |
DropdownMultiSelectFilterPainter | DropdownMultiSelect | No | Three-column layout: field | operator | value; count badge. |
InlineRowFilterPainter | InlineRow | No | Single-height, minimal padding; + / − buttons at row end. |
SidebarPanelFilterPainter | SidebarPanel | No | Vertical collapsible sections; fixed Apply/Clear footer. |
QuickSearchFilterPainter | QuickSearch | No | Search icon + text box + column dropdown; real-time debounce. |
AdvancedDialogFilterPainter | AdvancedDialog | No | Modal layout; tabs; Save/Load named presets; record count preview. |
FilterLayoutInfo
All geometry calculated by a painter — no pixel math in the control itself.
| Property | Description |
|---|---|
ContainerRect / ContentRect / HeaderRect / FooterRect | Primary layout regions. |
TagRects[] / RemoveButtonRects[] | Per-criterion pill and × button rectangles (TagPills). |
RowRects[] / ConnectorRects[] / DragHandleRects[] | GroupedRows / row-style layout arrays. |
FieldDropdownRects[] / OperatorDropdownRects[] / ValueDropdownRects[] | Dropdown column rectangles. |
AddFilterButtonRect / AddGroupButtonRect / ClearAllButtonRect | Action button rectangles. |
SaveButtonRect / LoadButtonRect / ApplyButtonRect | Persistence / submit button rectangles. |
SearchInputRect / SearchIconRect | QuickSearch widget rectangles. |
CountBadgeRect | Filter count badge position. |
Hit Testing
Painters register FilterHitArea objects during CalculateLayout(). Mouse events resolve to a typed FilterHitAreaType which drives all interaction logic without duplicating coordinate math.
| Type | Triggered When |
|---|---|
FilterTag | Click on a pill/row (select or begin edit). |
RemoveButton | Click on × button of a criterion. |
EditButton | Click on pencil/edit icon. |
AddFilterButton | Click “+ Add filter” button. |
AddGroupButton | Click “+ Add group” button (GroupedRows). |
LogicConnector | Click AND/OR toggle connector. |
FieldDropdown | Click column selector dropdown. |
OperatorDropdown | Click operator selector dropdown. |
ValueInput | Click value text field. |
ValueDropdown | Click value dropdown. |
DragHandle | Mouse-down on pill drag grip (TagPills). |
CollapseButton | Click section header collapse chevron (SidebarPanel). |
SearchInput | Click into search text box (QuickSearch). |
ClearAllButton | Click “Clear All” button. |
SaveButton / LoadButton | Persistence actions in AdvancedDialog. |
ApplyButton | Click “Apply” → raises FilterApplied event. |
Events
Core Filter Events
| Event | Args Type | When Raised |
|---|---|---|
FilterAdded | EventArgs | A new FilterCriteria is added to ActiveFilter. |
FilterRemoved | FilterRemovedEventArgs | A criterion is removed; args carry the removed criterion. |
FilterModified | FilterChangedEventArgs | A criterion is edited (operator or value changed). |
FiltersCleared | EventArgs | All criteria cleared via “Clear All”. |
FilterChanged | EventArgs | Any change to ActiveFilter (fired after all of the above). |
FilterEditRequested | FilterEditEventArgs | User clicks edit on a criterion (open inline editor or dialog). |
FilterApplied | EventArgs | User clicks Apply. Consuming control reads ActiveFilter here. |
FilterCanceled | EventArgs | User cancels dialog interaction. |
Group & Interaction Events
| Event | Description |
|---|---|
GroupAdded | A filter group (subgroup) is added to the GroupedRows or QueryBuilder view. |
GroupRemoved | A filter group is removed; args carry group identifier. |
FieldSelectionRequested | User clicks field/column dropdown; host can provide a custom picker. |
OperatorSelectionRequested | User clicks operator dropdown. |
ValueInputRequested | User activates value input; host can show a date picker or custom dialog. |
FilterDragStarted | Drag-and-drop reorder begins (TagPills with DragDrop enabled). |
FilterStyleChanged | Painter swapped due to FilterStyle property change. |
ConfigurationSaveRequested | User wants to save current config by name (AdvancedDialog). |
ConfigurationLoadRequested | User wants to load a saved config (AdvancedDialog). |
Helper Components
FilterSuggestionProvider
Analyses the data source and provides smart value suggestions: recent values, most frequent values, and partial-text matches. Results are cached for 5 minutes.
var provider = new FilterSuggestionProvider();
provider.SetDataSource(myList); // invalidates cache
var suggestions = provider.GetSuggestions(
columnName: "Country", partialValue: "Ge", maxResults: 10);
// Returns: Recent (star), Frequent (fire), Partial-match
// Each item: Value, DisplayText, MatchCount, Relevance, Type
FilterValidationHelper
Validates operator/type compatibility and value requirements before criteria are committed.
var helper = new FilterValidationHelper();
var result = helper.Validate(criteria, DbFieldCategory.Numeric);
if (!result.IsValid)
ShowError(result.ErrorMessage);
// Guards: null column, operator/type mismatch,
// missing Value2 for Between, type incompatibility
FilterKeyboardHandler
Attached to the BeepFilter control; provides keyboard shortcuts for adding, clearing, and navigating criteria.
FilterAutocompletePopup
Popup created on demand when a value input is focused. Shows FilterSuggestion items. Dismissed on Escape or selection.
FilterIconProvider
Returns icon glyphs (Bootstrap Icons / strings) for operator symbols and filter actions, keeping painter code free of icon literals.
FilterPainterMetrics / IFilterPainterMetricsProvider
Provides computed layout metrics (heights, widths, paddings) derived from BeepStyling so all painters share a single source of truth for spacing.
Enums Reference
FilterDisplayMode
| Value | Behavior |
|---|---|
AlwaysVisible | Filter bar is always shown. |
Collapsible | Can be collapsed; controlled by IsExpanded. |
OnHover | Shown only when the user hovers over the host area. |
Modal | Appears in a floating overlay (AdvancedDialog style). |
FilterPosition
| Value | Hint For |
|---|---|
Top | Above data view (default). |
Bottom | Below data view. |
Left | Left sidebar (SidebarPanel style). |
Right | Right sidebar. |
Floating | Detached floating panel. |
FilterHitAreaType (18 values)
None, FilterTag, RemoveButton, EditButton, AddFilterButton,
AddGroupButton, LogicConnector, FieldDropdown, OperatorDropdown,
ValueInput, ValueDropdown, DragHandle, CollapseButton,
SearchInput, ClearAllButton, SaveButton, LoadButton, ApplyButton.
Usage Patterns
Pattern 1: Standalone BeepFilter
var filter = new BeepFilter
{
EntityStructure = myEntityStructure,
FilterStyle = FilterStyle.TagPills,
DataSource = myList, // optional: enables suggestions
Dock = DockStyle.Top
};
filter.FilterApplied += (s, e) =>
{
var engine = new FilterEngine<Product>(allProducts);
var results = engine.ApplyFilter(filter.ActiveFilter);
listBox1.DataSource = results.ToList();
};
panel.Controls.Add(filter);
Pattern 2: Wired to BeepGridPro
var beepFilter = new BeepFilter
{
EntityStructure = grid.EntityStructure,
FilterStyle = FilterStyle.GroupedRows,
Dock = DockStyle.Top
};
beepFilter.FilterApplied += (s, e) =>
{
// Grid's ActiveFilter setter calls FilterEngine<ExpandoObject> internally
grid.ActiveFilter = beepFilter.ActiveFilter;
};
// Or use the grid's built-in integration:
grid.ShowAdvancedFilterDialog(); // opens AdvancedDialog style
grid.ApplyQuickFilter("Smith"); // internal QuickSearch
Pattern 3: Programmatic Filter (no UI)
var config = new FilterConfiguration();
config.AddCriteria(new FilterCriteria("Status", FilterOperator.Equals, "Active"));
config.AddCriteria(new FilterCriteria("Score", FilterOperator.GreaterThanOrEqual, 80));
config.Logic = FilterLogic.And;
var engine = new FilterEngine<Employee>(employees);
var visible = engine.ApplyFilter(config).ToList();
// Get just the indices (faster for large virtual lists)
var indices = engine.ApplyFilterGetIndices(config);
Pattern 4: Save / Load Named Configurations
// Save
beepFilter.ConfigurationSaveRequested += (s, e) =>
{
var config = beepFilter.ActiveFilter;
config.Name = "EU High-Value Q1";
File.WriteAllText("filters/eu-q1.json", JsonSerializer.Serialize(config));
};
// Load
beepFilter.ConfigurationLoadRequested += (s, e) =>
{
var json = File.ReadAllText("filters/eu-q1.json");
var config = JsonSerializer.Deserialize<FilterConfiguration>(json)!;
beepFilter.ActiveFilter = config;
beepFilter.Invalidate();
};
BeepQueryandFilter (Legacy)
Older, simpler control in the root Controls namespace. Uses a TableLayoutPanel with one row per field from an EntityStructure.
| Member | Description |
|---|---|
CreateQueryControls(EntityStructure) | Generates one label + input per field; range inputs for range operators. |
SubmitClicked | Event raised when user clicks Submit; consumer reads AppFilter list. |
BeepFilter for new code. BeepQueryandFilter predates the painter architecture; it does not support themes, painter styles, or the FilterEngine<T> pipeline.
File Organisation
| File | Purpose |
|---|---|
FilterStyle.cs | FilterStyle, FilterDisplayMode, FilterPosition enums. |
FilterOperator.cs | FilterOperator, FilterLogic enums + FilterOperatorExtensions.GetDisplayName(). |
FilterCriteria.cs | FilterCriteria and FilterConfiguration model classes. |
FilterEngine.cs | Generic FilterEngine<T> with ApplyFilter, ApplyQuickFilter, ApplyFilterGetIndices. |
IFilterPainter.cs | IFilterPainter, FilterHitArea, FilterHitAreaType, FilterLayoutInfo. |
BaseFilterPainter.cs | Abstract base painter; shared paint/layout helpers. |
FilterPainterFactory.cs | FilterStyle → IFilterPainter factory with descriptions. |
FilterPainterMetrics.cs | Shared layout metrics interface + default implementation. |
BeepFilter.cs | Main control: constructor, painter management, paint override, mouse handling. |
BeepFilter.Properties.cs | All public properties and change handlers. |
BeepFilter.Events.cs | All event definitions and On* raising methods. |
BeepFilter.HitTest.cs | Mouse coordinate → FilterHitAreaType dispatch. |
BeepFilter.Layout.cs | RecalculateLayout() dispatcher → active painter. |
FilterKeyboardHandler.cs | Keyboard shortcut handler attached to the control. |
FilterSuggestionProvider.cs | Recent/frequent value suggestion engine with 5-min cache. |
FilterValidationHelper.cs | Operator/type compatibility validation → FilterValidationResult. |
FilterAutocompletePopup.cs | On-demand popup for value autocomplete during typing. |
FilterIconProvider.cs | Icon glyph provider for operators and actions. |
Painters/TagPillsFilterPainter.cs | TagPills: pill height 32px, corner radius 16, max 10/row, drag-drop reorder. |
Painters/GroupedRowsFilterPainter.cs | GroupedRows: AND/OR connectors, subgroups. |
Painters/QueryBuilderFilterPainter.cs | QueryBuilder: If/Else tree branching. |
Painters/DropdownMultiSelectFilterPainter.cs | DropdownMultiSelect: 3-column dropdown, multi-select checkboxes. |
Painters/InlineRowFilterPainter.cs | InlineRow: compact single-row per criterion. |
Painters/SidebarPanelFilterPainter.cs | SidebarPanel: vertical collapsible categories. |
Painters/QuickSearchFilterPainter.cs | QuickSearch: search bar with real-time debounce. |
Painters/AdvancedDialogFilterPainter.cs | AdvancedDialog: modal, tabs, Save/Load, result count. |
BeepQueryandFilter.cs | Legacy query control (TableLayoutPanel, AppFilter based). |
Best Practices
- Never apply filters inside BeepFilter. Always subscribe to
FilterAppliedand useFilterEngine<T>in the consuming control. - Disable criteria instead of removing them. Set
IsEnabled = falseto temporarily skip without losing the configuration. - Use
ApplyFilterGetIndices()for large grids. Returning indices avoids creating a new collection and maps directly to row-hiding in virtual grids. - Set
EntityStructurebeforeFilterStyle. Painters read column definitions duringCalculateLayout(); setting style first may paint an empty field list. - Provide
DataSourcefor smart suggestions.FilterSuggestionProvidercan only generate recent/frequent hints whenBeepFilter.DataSourceis set. - Call
RecalculateLayout() + Invalidate()after programmatic changes. Internal layout recalculates only on user interaction and property changes, not on directActiveFilter.Criteria.Add()calls. - Serialize
FilterConfigurationwithSystem.Text.Json. All properties are plain types — no custom converters needed for basic save/load. - Avoid
BeepQueryandFilterin new code. It does not integrate with the theming system, painter architecture, orFilterEngine<T>pipeline.