Skip to content
This repository was archived by the owner on Oct 13, 2025. It is now read-only.
Open
183 changes: 183 additions & 0 deletions docs/filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# TreeDataGrid Filtering

The TreeDataGrid control includes a powerful and flexible filtering system that allows users to filter data across columns. This document explains how to use the filtering system and customize it for your needs.

## Basic Filtering

TreeDataGrid supports filtering on text columns. To enable filtering:

1. Make sure `ShowColumnFilters` is `true` on the TreeDataGrid (this is the default)
2. Enable filtering for specific columns by setting `IsFilterEnabled` to `true` in the column options:

```csharp
var grid = new TreeDataGrid();

var columns = new ColumnList<Person>(
new TextColumn<Person, string>(
"Name",
x => x.Name,
options: new TextColumnOptions<Person> { IsFilterEnabled = true }
),
new TextColumn<Person, string>(
"Email",
x => x.Email,
options: new TextColumnOptions<Person> { IsFilterEnabled = true }
)
);

var source = new FlatTreeDataGridSource<Person>(people) { Columns = columns };
grid.Source = source;
```

This will display filter text boxes below each column header that has filtering enabled. The filter controls will expand to take the full width of the column, ensuring consistent layout and better usability.

**Note:** For standard columns like TextColumn and CheckBoxColumn, setting IsFilterEnabled to true is sufficient. However, for TemplateColumn, you must specify all three properties: FilterValueSelector (to extract the value), Filter (to define how to filter), and FilterControlFactory (to create the filter UI).

## Column Header Filter Layout

The TreeDataGridColumnHeader is implemented as a vertical StackPanel that contains:

1. A header button for displaying the column title and handling sorting
2. A filter control that appears when filtering is enabled

This layout ensures that both the header and filter components take the full width of the column, providing a clean and consistent user interface. The filter control appears directly below the header button when `IsFilterEnabled` is set to `true`.

## Programmatic Access to Filters

You can interact with filters programmatically by accessing the column headers and their filter controls:

```csharp
// Get a reference to the column header
var nameColumnHeader = grid.ColumnHeadersPresenter?
.TryGetElement(0) as TreeDataGridColumnHeader;

// Access the filter control (if needed)
if (nameColumnHeader != null)
{
// You can apply filters programmatically through the TreeDataGridSource
var column = grid.Source.Columns[0];
grid.Source.SetFilterCondition(column, "Search term");
}
```

## Custom Filter Column Types

The TreeDataGrid supports different types of column filtering through the following systems:

1. **Value Filter System (Recommended)**: Uses the `IValueFilter<TValue>` interface
2. **Legacy System**: Uses the `IFilterableColumn<TModel>` interface

### Using the Value Filter System

The value filter system is the recommended approach for new code. It provides a more flexible and extensible way to define filters:

```csharp
// Define the filter interface
public interface IValueFilter<TValue>
{
bool Passes(object? condition, TValue? value);
}
```

Every column can use a filter by setting options. The TreeDataGrid provides several built-in filter implementations:

- `TextValueFilter` - For text-based filtering
- `BooleanValueFilter` - For boolean/checkbox filtering
- `SetValueFilter<TValue>` - For filtering based on a set of allowed values

#### Example: Using Text Filter

```csharp
var nameColumn = new TextColumn<Person, string>(
"Name",
x => x.Name,
options: new TextColumnOptions<Person> { IsFilterEnabled = true }
);
// Filter is automatically created when IsFilterEnabled is true

// Or set a filter manually in the options:
var options = new TextColumnOptions<Person> {
IsFilterEnabled = true,
Filter = new TextValueFilter(TextFilterMode.Contains, false)
};
```

#### Example: Using Template Column with Filter

```csharp
var regionColumn = new TemplateColumn<Country>(
"Region",
"RegionCellTemplate", // Template resource key
"RegionEditCellTemplate", // Edit template resource key
options: new TemplateColumnOptions<Country>
{
// While IsFilterEnabled is used for TextColumn, it's not required for TemplateColumn when all three properties below are specified
// FilterValueSelector is required to extract the value to filter on
FilterValueSelector = x => x.Region,
// Filter specifies how to filter the values
Filter = new TextValueFilter(),
// FilterControlFactory is required to create the filter UI
FilterControlFactory = (col) => new TextFilterControl(col, "custom filter...")
}
);
// For TemplateColumn, all three properties above should be specified
```


#### Example: Using Boolean Filter

```csharp
var activeColumn = new CheckBoxColumn<User>(
"Active",
x => x.IsActive,
options: new CheckBoxColumnOptions<User> { IsFilterEnabled = true }
);
// The BooleanValueFilter is automatically created when IsFilterEnabled is true
```


### The `TextValueFilter` supports different filter modes:
- `Contains` - Checks if the value contains the filter text (default)
- `StartsWith` - Checks if the value starts with the filter text
- `EndsWith` - Checks if the value ends with the filter text

You can configure these modes when creating a filter:

### Creating Custom Filter Columns

Columns implement the `IFilterableColumn<TModel>` interface to provide filtering:

```csharp
public interface IFilterableColumn
{
/// <summary>
/// Gets a value indicating whether filtering is enabled for this column.
/// </summary>
bool IsFilterEnabled { get; }
}

public interface IFilterableColumn<TModel>: IFilterableColumn
{
/// <summary>
/// Determines if the model passes the filter.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="condition">The filter condition to apply.</param>
/// <returns>True if the model passes the filter, otherwise false.</returns>
bool PassesFilter(TModel model, object? condition);
}
```


### Performance Considerations

For large datasets, consider these performance tips:

1. If possible, filter data at the source (database query) rather than in-memory
2. Implement debounced filtering (apply filter after typing stops)
3. Keep filter implementations efficient
4. Consider caching filter results when appropriate

## Complete Example

See the `CustomFilteringSample.cs` in the samples directory for a complete example of implementing custom filtering UI with TreeDataGrid.
15 changes: 13 additions & 2 deletions samples/TreeDataGridDemo/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@
<TabItem Header="Countries">
<DockPanel>
<TextBlock Classes="realized-count" DockPanel.Dock="Bottom"/>
<StackPanel DockPanel.Dock="Right" Spacing="4" Margin="4 0 0 0">
<StackPanel DockPanel.Dock="Right" Spacing="4" Margin="4 0 0 0" Width="250">
<CheckBox IsChecked="{Binding Countries.CellSelection}">Cell Selection</CheckBox>

<TextBlock TextWrapping="Wrap" Margin="0,10,0,0" FontWeight="Bold">Filter Example:</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0,5,0,0">Try typing in the filter boxes under each column header:</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0,5,0,0">• Country: Try partial names</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0,5,0,0">• Region: Try "EUROPE" or "ASIA"</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0,5,0,0">• Population: Try numbers like "10000"</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0,5,0,0">• Area: Try areas like "100000"</TextBlock>

<Separator Margin="0,10,0,10" />

<Label Target="countryTextBox">_Country</Label>
<TextBox Name="countryTextBox">Sealand</TextBox>
<Label Target="regionTextBox">_Region</Label>
Expand All @@ -28,7 +38,8 @@
</StackPanel>
<TreeDataGrid Name="countries"
Source="{Binding Countries.Source}"
AutoDragDropRows="True">
AutoDragDropRows="True"
ShowColumnFilters="True">
<TreeDataGrid.Resources>
<!-- Template for Region column cells -->
<DataTemplate x:Key="RegionCell" DataType="m:Country">
Expand Down
68 changes: 55 additions & 13 deletions samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
using Avalonia.Media;
using ReactiveUI;
using TreeDataGridDemo.Models;
using TreeDataGridDemo.ViewModels;

namespace TreeDataGridDemo.ViewModels
{
Expand All @@ -21,18 +23,58 @@ public CountriesPageViewModel()
{
Columns =
{
new TextColumn<Country, string>("Country", x => x.Name, (r, v) => r.Name = v, new GridLength(6, GridUnitType.Star), new()
{
IsTextSearchEnabled = true,
}),
new TemplateColumn<Country>("Region", "RegionCell", "RegionEditCell"),
new TextColumn<Country, int>("Population", x => x.Population, new GridLength(3, GridUnitType.Star)),
new TextColumn<Country, int>("Area", x => x.Area, new GridLength(3, GridUnitType.Star)),
new TextColumn<Country, int>("GDP", x => x.GDP, new GridLength(3, GridUnitType.Star), new()
{
TextAlignment = Avalonia.Media.TextAlignment.Right,
MaxWidth = new GridLength(150)
}),
// Text column with advanced filter
new TextColumn<Country, string?>(
"Country",
x => x.Name,
(r, v) => r.Name = v,
new GridLength(6, GridUnitType.Star),
new TextColumnOptions<Country>
{
IsTextSearchEnabled = true,
IsFilterEnabled = true // This will create a TextFilter automatically
}),

// Use a template column with filtering for region
new TemplateColumn<Country>(
"Region",
"RegionCell",
"RegionEditCell",
new GridLength(3, GridUnitType.Star),
new TemplateColumnOptions<Country>
{
// Value selector for filtering
FilterValueSelector = x => x.Region,
Filter = new TextValueFilter(),
FilterControlFactory = (col) => new TextFilterControl(col, "custom filter..."),
}),

// Population column with numeric filtering
new TextColumn<Country, int>(
"Population",
x => x.Population,
new GridLength(3, GridUnitType.Star),
new TextColumnOptions<Country> { IsFilterEnabled = true }),

// Area column with numeric filtering
new TextColumn<Country, int>(
"Area",
x => x.Area,
new GridLength(3, GridUnitType.Star),
new TextColumnOptions<Country> { IsFilterEnabled = true, StringFormat = "{0:N0}" }),

// GDP column
new TextColumn<Country, int>(
"GDP",
x => x.GDP,
new GridLength(3, GridUnitType.Star),
new TextColumnOptions<Country>
{
TextAlignment = TextAlignment.Right,
MaxWidth = new GridLength(150),
IsFilterEnabled = true,
StringFormat = "${0:N0}"
}),
}
};
Source.RowSelection!.SingleSelect = false;
Expand Down
56 changes: 53 additions & 3 deletions src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Avalonia.Controls
public class FlatTreeDataGridSource<TModel> : NotifyingBase,
ITreeDataGridSource<TModel>,
IDisposable
where TModel: class
where TModel : class
{
private IEnumerable<TModel> _items;
private TreeDataGridItemsSourceView<TModel> _itemsView;
Expand Down Expand Up @@ -76,12 +76,31 @@ public ITreeDataGridSelection? Selection

IEnumerable<object> ITreeDataGridSource.Items => Items;

public ITreeDataGridCellSelectionModel<TModel>? CellSelection => Selection as ITreeDataGridCellSelectionModel<TModel>;
public ITreeDataGridRowSelectionModel<TModel>? RowSelection => Selection as ITreeDataGridRowSelectionModel<TModel>;
public ITreeDataGridCellSelectionModel<TModel>? CellSelection =>
Selection as ITreeDataGridCellSelectionModel<TModel>;

public ITreeDataGridRowSelectionModel<TModel>? RowSelection =>
Selection as ITreeDataGridRowSelectionModel<TModel>;

public bool IsHierarchical => false;
public bool IsSorted => _comparer is not null;

/// <summary>
/// Gets a value indicating whether any filters are currently applied.
/// </summary>
public bool HasFilters()
{
foreach (var column in Columns)
{
if (column is IFilterableColumn<TModel> { IsFilterEnabled: true })
return true;
}

return false;
}

public event Action? Sorted;
public event Action? Filtered;

public void Dispose()
{
Expand All @@ -100,6 +119,8 @@ void ITreeDataGridSource.DragDropRows(
throw new NotSupportedException("Only move is currently supported for drag/drop.");
if (IsSorted)
throw new NotSupportedException("Drag/drop is not supported on sorted data.");
if (HasFilters())
throw new NotSupportedException("Drag/drop is not supported on filtered data.");
if (position == TreeDataGridRowDropPosition.Inside)
throw new ArgumentException("Invalid drop position.", nameof(position));
if (indexes.Any(x => x.Count != 1))
Expand Down Expand Up @@ -152,17 +173,46 @@ bool ITreeDataGridSource.SortBy(IColumn? column, ListSortDirection direction)
foreach (var c in Columns)
c.SortDirection = c == column ? direction : null;
}

return true;
}

return false;
}

public void Filter(IDictionary<IFilterableColumn, object?> conditions)
{
var newConditions = new Dictionary<IFilterableColumn<TModel>, object?>();
foreach (var condition in conditions)
{
newConditions.Add((IFilterableColumn<TModel>)condition.Key, condition.Value);
}
var filteredItems = _items.Where(item => PassesAllFilters(item, newConditions));
_itemsView = TreeDataGridItemsSourceView<TModel>.GetOrCreate(filteredItems);
_rows?.SetItems(_itemsView);
Filtered?.Invoke();
}

IEnumerable<object> ITreeDataGridSource.GetModelChildren(object model)
{
return Enumerable.Empty<object>();
}


private bool PassesAllFilters(TModel model, Dictionary<IFilterableColumn<TModel>, object?> conditions)
{
// Check immutable filter system first (only apply active filters)
foreach (var kvp in conditions)
{
if (kvp.Key.IsFilterEnabled)
if (!kvp.Key.PassesFilter(model, kvp.Value))
return false;
}


return true;
}

private AnonymousSortableRows<TModel> CreateRows()
{
return new AnonymousSortableRows<TModel>(_itemsView, _comparer);
Expand Down
Loading
Loading