Back to Devexpress

Column Filter Menu in Blazor Grid

blazor-404417-components-grid-data-shaping-filter-data-filter-menu.md

latest27.3 KB
Original Source

Column Filter Menu in Blazor Grid

  • Feb 03, 2026
  • 12 minutes to read

The column filter menu displays a list of unique values, allowing users to filter data. A corner grip helps users resize the menu.

Run Demo: Column Filter Menu

Use the following properties to display filter menu buttons in column headers:

DxGrid.FilterMenuButtonDisplayModeSpecifies when the Grid displays filter menu buttons in column headers.DxGridDataColumn.FilterMenuButtonDisplayModeSpecifies when the column displays the filter menu button.

Note

The Grid cannot create filter item lists for columns associated with certain data types (for instance, arrays and images). If you do not implement a filter menu template for such a column, the column filter menu displays the following text instead of filter items: “No filters are available for this column”.

Once you apply a filter to a column, other filter menus hide values that do not match specified filter criteria. Hold down Shift and click the filter button to display all values. Focus a column header and press Alt+Arrow Down or Shift+Alt+Arrow Down to open the filter menu.

When a user clicks the menu’s Clear button, the filter menu changes the current operator type to Default.

Customize Filter Items

When a user clicks a filter menu button, the Grid creates a list of default filter items. The table below lists settings that you can specify in the EditSettings render fragment:

|

Editor Settings

|

Applicable Data Types

|

Supported Properties

| | --- | --- | --- | |

DxComboBoxSettings

|

Any data type

|

Data, TextFieldName, ValueFieldName

| |

DxCheckBoxSettings

|

Any data type

|

CheckedDisplayText, ValueChecked, UncheckedDisplayText, ValueUnchecked, IndeterminateDisplayText, ValueIndeterminate

|

The following code snippet uses the DxComboBoxSettings object to specify the text used to display the Category column’s filter menu items:

razor
<DxGrid Data="GridData"
    FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
    @* ... *@
    VirtualScrollingEnabled="true">
    <Columns>
        <DxGridDataColumn FieldName="OrderDate" Width="140px" />
        <DxGridDataColumn FieldName="ProductName" MinWidth="100" />
        <DxGridDataColumn FieldName="CategoryId" Caption="Category" Width="130px">
            <EditSettings>
                <DxComboBoxSettings Data="Categories" ValueFieldName="CategoryId" TextFieldName="CategoryName" />
            </EditSettings>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="UnitPrice" DisplayFormat="c2" Width="140px" />
        <DxGridDataColumn FieldName="Quantity" Width="110px" />
        <DxGridDataColumn FieldName="Discount" DisplayFormat="p0" Width="110px" />
        <DxGridDataColumn FieldName="Total"
                          UnboundType="GridUnboundColumnType.Decimal"
                          UnboundExpression="[UnitPrice] * [Quantity] * (1 - [Discount])"
                          DisplayFormat="c2"
                          Width="110px">
            <FilterMenuTemplate>
                <Grid_Filtering_ColumnFilterMenu_CustomRange FilterContext="context" Items="TotalPriceIntervals" />
            </FilterMenuTemplate>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="Shipped"
                          UnboundType="GridUnboundColumnType.Boolean"
                          UnboundExpression="[ShippedDate] <> Null"
                          Width="100px" />
    </Columns>
</DxGrid>

You can also handle the CustomizeFilterMenu event to customize the filter menu items for all data types (for example, to concatenate values, apply formatting, or replace blank values). The event fires before the filter dropdown appears.

Note that each filter menu item cannot hold a complex filter condition and can only filter data by a single column value. To implement complex criteria, create a filter menu template.

The following code snippet shows how you can modify text in filter menu items. This example adds IDs to customer names.

razor
<DxGrid Data="@customers" 
        FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
        CustomizeFilterMenu="OnCustomizeFilterMenu">
    <Columns>
        <DxGridDataColumn FieldName="ContactName" Width="180px" />
        <DxGridDataColumn FieldName="Company" />
        <DxGridDataColumn FieldName="Country" Width="140px" />
    </Columns>
</DxGrid>

@code {
    DateTime data { get; set; }
    private Customer[]? customers;
    protected override async Task OnInitializedAsync() {
        customers = await CustomerData.GetData();
    }
    void OnCustomizeFilterMenu (GridCustomizeFilterMenuEventArgs e) {
        if (e.DataColumn.FieldName == "ContactName") {
            e.DataItems.ForEach(di => {
                int? CustomerID = customers.Where(c => 
                    c.ContactName == di.Value.ToString()).FirstOrDefault()?.ID;
                di.DisplayText = di.DisplayText + " (ID " + CustomerID + ")";
            });
        }
    }
}

Custom Filter Menu Content (Template)

To apply a complex filter condition in a filter menu (for instance, a date range), create a filter menu template. You can create a template for an individual column (FilterMenuTemplate) or for all columns in the Grid (DataColumnFilterMenuTemplate).

You can choose how to display DateTime and DateTime? values. The default view displays dates hierarchically (HierarchicalDateView). You can also display dates as a plain list (ListView). Neither option affects default content rendering (that is, paddings, alignment, etc.).

razor
<DxGrid Data="GridData"
        FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always">
    <Columns>
        <DxGridDataColumn FieldName="OrderDate">
            <FilterMenuTemplate>
                @context.ListView
            </FilterMenuTemplate>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="ProductName" MinWidth="100" />
        <DxGridDataColumn FieldName="UnitPrice" DisplayFormat="c2" />
        <DxGridDataColumn FieldName="Quantity" />
        <DxGridDataColumn FieldName="Discount" DisplayFormat="p0" />
        <DxGridDataColumn FieldName="Total"
                          UnboundType="GridUnboundColumnType.Decimal"
                          UnboundExpression="[UnitPrice] * [Quantity] * (1 - [Discount])"
                          DisplayFormat="c2" />
    </Columns>
</DxGrid>

The template’s context parameter also contains the FilterCriteria property. Use this property to specify filter criteria applied to the column and handle the FilterCriteriaChanged event to react to filter criteria changes.

View Example: Implement a date range filter

The following code snippet creates custom ranges for the Order Date and Total column’s filter menu:

razor
<DxGrid Data="GridData"
    FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always">
    <Columns>
        <DxGridDataColumn FieldName="OrderDate" Width="140px">
            <FilterMenuTemplate>
                <DateRange FilterContext="context" />
            </FilterMenuTemplate>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="ProductName" MinWidth="100" />
        <DxGridDataColumn FieldName="CategoryId" Caption="Category" Width="130px">
            <EditSettings>
                <DxComboBoxSettings Data="Categories" ValueFieldName="CategoryId" TextFieldName="CategoryName" />
            </EditSettings>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="UnitPrice" DisplayFormat="c2" Width="140px" />
        <DxGridDataColumn FieldName="Quantity" Width="110px" />
        <DxGridDataColumn FieldName="Discount" DisplayFormat="p0" Width="110px" />
        <DxGridDataColumn FieldName="Total"
                          UnboundType="GridUnboundColumnType.Decimal"
                          UnboundExpression="[UnitPrice] * [Quantity] * (1 - [Discount])"
                          DisplayFormat="c2"
                          Width="110px">
            <FilterMenuTemplate>
                <CustomRange FilterContext="context" Items="TotalPriceIntervals" />
            </FilterMenuTemplate>
        </DxGridDataColumn>
        <DxGridDataColumn FieldName="Shipped"
                          UnboundType="GridUnboundColumnType.Boolean"
                          UnboundExpression="[ShippedDate] <> Null"
                          Width="100px" />
    </Columns>
</DxGrid>
@code {
    static IReadOnlyList<CustomRangeFilterItem> TotalPriceIntervals { get; } = CreateTotalPriceIntervals();

    object GridData { get; set; }
    IReadOnlyList<Category> Categories { get; set; }

    protected override async Task OnInitializedAsync() {
        Categories = (await NwindDataService.GetCategoriesAsync()).ToList();

        var invoices = await NwindDataService.GetInvoicesAsync();
        var products = await NwindDataService.GetProductsAsync();
        GridData = invoices.Join(products, i => i.ProductId, p => p.ProductId, (i, p) => {
            return new {
                ProductName = i.ProductName,
                CategoryId = p.CategoryId,
                OrderDate = i.OrderDate,
                UnitPrice = i.UnitPrice,
                Quantity = i.Quantity,
                Discount = i.Discount,
                ShippedDate = i.ShippedDate
            };
        });
    }

    static IReadOnlyList<CustomRangeFilterItem> CreateTotalPriceIntervals() {
        var prop = new OperandProperty("Total");
        var result = new List<CustomRangeFilterItem>();

        var step = 100M;
        for(var i = 0; i < 10; i++) {
            var start = step * i;
            var end = start + step;

            result.Add(new() {
                Criteria = prop >= start & prop < end,
                DisplayText = $"from {start:c} to {end - 0.01M:c}"
            });
        }
        result.Add(new() {
            Criteria = prop > 1000,
            DisplayText = $"> {1000:c}"
        });

        return result;
    }
}
razor
@using DevExpress.Data.Filtering;
@using DevExpress.Data.Filtering.Helpers;

<DxFormLayout CssClass="spaced-content" ItemCaptionAlignment="ItemCaptionAlignment.All">
    <DxFormLayoutItem Caption="From" ColSpanSm="12">
        <DxDateEdit T="DateTime?"
                    Enabled="DateEditEnabled"
                    Date="StartDate"
                    DateChanged="StartDate_Changed"
                    MinDate="StartDateEdit_MinDate"
                    MaxDate="StartDateEdit_MaxDate"
                    ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto" />
    </DxFormLayoutItem>
    <DxFormLayoutItem Caption="To" ColSpanSm="12">
        <DxDateEdit T="DateTime?"
                    Enabled="DateEditEnabled"
                    Date="EndDate"
                    DateChanged="EndDate_Changed"
                    MinDate="EndDateEdit_MinDate"
                    MaxDate="EndDateEdit_MaxDate"
                    ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto" />
    </DxFormLayoutItem>
</DxFormLayout>

@code {
    [Parameter]
    public GridDataColumnFilterMenuTemplateContext FilterContext { get; set; }

    DateTime? StartDate { get; set; }
    DateTime? EndDate { get; set; }

    DateTime MinDate { get; set; } = DateTime.MinValue;
    DateTime MaxDate { get; set; } = DateTime.MaxValue;

    DateTime StartDateEdit_MinDate => MinDate;
    DateTime StartDateEdit_MaxDate => EndDate != null && EndDate.Value >= StartDateEdit_MinDate ? EndDate.Value : MaxDate;

    DateTime EndDateEdit_MinDate => StartDate != null && StartDate.Value <= EndDateEdit_MaxDate ? StartDate.Value : MinDate;
    DateTime EndDateEdit_MaxDate => MaxDate;

    bool DateEditEnabled { get; set; }

    protected override async Task OnInitializedAsync() {
        (StartDate, EndDate) = LoadDateRangeValues(FilterContext.FilterCriteria, FilterContext.DataColumn.FieldName);

        var items = await FilterContext.GetDataItemsAsync();
        var allDates = items.Select(i => Convert.ToDateTime(i.Value)).ToList();

        if(allDates.Any()) {
            MinDate = allDates.Min();
            MaxDate = allDates.Max();

            DateEditEnabled = true;
        }
    }

    void StartDate_Changed(DateTime? value) {
        StartDate = value;
        if(StartDate > EndDate)
            EndDate = StartDate;

        UpdateCriteria();
    }
    void EndDate_Changed(DateTime? value) {
        EndDate = value;
        if(StartDate > EndDate)
            StartDate = EndDate;

        UpdateCriteria();
    }
    void UpdateCriteria() {
        FilterContext.FilterCriteria = CreateDateRangeCriteria(StartDate, EndDate, FilterContext.DataColumn.FieldName);
    }

    static CriteriaOperator CreateDateRangeCriteria(DateTime? startDate, DateTime? endDate, string fieldName) {
        CriteriaOperator left = null;
        CriteriaOperator right = null;

        var prop = new OperandProperty(fieldName);
        if(startDate != null)
            left = prop >= startDate;
        if(endDate != null)
            right = prop < ConvertEndDateToOperandDate(endDate);

        return left & right;
    }
    static DateTime? ConvertEndDateToOperandDate(DateTime? endDate) => endDate?.Date.AddDays(1);
    static (DateTime? startDate, DateTime? endDate) LoadDateRangeValues(CriteriaOperator criteria, string fieldName) {
        CriteriaOperator left = null;
        CriteriaOperator right = null;

        if(criteria is GroupOperator groupOp && groupOp.OperatorType == GroupOperatorType.And && groupOp.Operands.Count == 2) {
            left = groupOp.Operands[0];
            right = groupOp.Operands[1];
        } else {
            left = right = criteria;
        }

        return (
            ExtractRangeDate(left, fieldName, BinaryOperatorType.GreaterOrEqual),
            ConvertOperandDateToEndDate(ExtractRangeDate(right, fieldName, BinaryOperatorType.Less))
        );
    }
    static DateTime? ExtractRangeDate(CriteriaOperator criteria, string fieldName, BinaryOperatorType opType) {
        var canExtract = criteria is BinaryOperator binaryOp &&
                         binaryOp.OperatorType == opType &&
                         binaryOp.LeftOperand is OperandProperty prop &&
                         binaryOp.RightOperand is OperandValue opValue &&
                         prop.PropertyName == fieldName &&
                         opValue.Value is DateTime;
        if(canExtract)
            return (DateTime)((OperandValue)((BinaryOperator)criteria).RightOperand).Value;
        return null;
    }
    static DateTime? ConvertOperandDateToEndDate(DateTime? endDate) => endDate?.Date.AddDays(-1);
}
css
.spaced-content {
    padding: 0.75rem;
}
razor
@using DevExpress.Data.Filtering;
@using DevExpress.Data.Filtering.Helpers;

<DxListBox TData="CustomRangeFilterItem"
           TValue="CriteriaOperator"
           Data="Items"
           ValueFieldName="Criteria"
           TextFieldName="DisplayText"
           Values="SelectedValues"
           ValuesChanged="SelectedValues_Changed"
           SelectionMode="ListBoxSelectionMode.Multiple"
           ShowCheckboxes="true"
           CssClass="h-100" />

@code {
    [Parameter]
    public GridDataColumnFilterMenuTemplateContext FilterContext { get; set; }

    [Parameter]
    public IReadOnlyList<CustomRangeFilterItem> Items { get; set; }

    IEnumerable<CriteriaOperator> SelectedValues { get; set; }

    protected override void OnInitialized() {
        SelectedValues = LoadSelectedValues(
            FilterContext.FilterCriteria,
            Items.Select(i => i.Criteria).ToHashSet()
        );
    }

    void SelectedValues_Changed(IEnumerable<CriteriaOperator> value) {
        SelectedValues = value;
        FilterContext.FilterCriteria = CreateCriteria(SelectedValues);
    }

    static CriteriaOperator CreateCriteria(IEnumerable<CriteriaOperator> values) {
        var orderedValues = values.OrderBy(v => v.ToString()).ToArray();
        if (orderedValues.Length == 0)
            return null;

        return new GroupOperator(GroupOperatorType.Or, orderedValues);
    }
    static IEnumerable<CriteriaOperator> LoadSelectedValues(CriteriaOperator criteria, IReadOnlySet<CriteriaOperator> possibleCriterias) {
        if(possibleCriterias.Contains(criteria))
            return new[] { criteria };

        if(criteria is GroupOperator groupOp && groupOp.OperatorType == GroupOperatorType.Or && groupOp.Operands.All(i => possibleCriterias.Contains(i)))
            return groupOp.Operands;

        return Array.Empty<CriteriaOperator>();
    }

    public record CustomRangeFilterItem {
        public CriteriaOperator Criteria { get; init; }
        public string DisplayText { get; init; }

        public static IReadOnlyList<CustomRangeFilterItem> CreateIntervals(string fieldName, int step, int stepsCount, bool isMult, string numberDisplayFormat) {
            var prop = new OperandProperty(fieldName);
            var result = new List<CustomRangeFilterItem>();
            var start = 0;
            var end = 0;
            var firstStepIndex = isMult ? 0 : -1;

            for(var i = firstStepIndex; i < stepsCount; ++i) {
                start = isMult ? step * i : (int)Math.Pow(step, i);
                end = isMult ? start + step : (int)Math.Pow(step, i + 1);

                result.Add(new CustomRangeFilterItem() {
                    Criteria = prop >= start & prop < end,
                    DisplayText = $"from {string.Format(numberDisplayFormat, start)} to {string.Format(numberDisplayFormat, end)}"
                });
            }
            result.Add(new CustomRangeFilterItem() {
                Criteria = prop >= end,
                DisplayText = $"from {string.Format(numberDisplayFormat, end)}"
            });

            return result;
        }
    }
}

Case-Insensitive Filtering

Filtering is always case-insensitive for most data sources except for a GridDevExtremeDataSource.

If a GridDevExtremeDataSource uses LINQ to Objects, it also ignores word case; otherwise, filtering is case-sensitive. Use the StringToLower option to enable/disable case sensitivity in the GridDevExtremeDataSource:

razor
@inherits OwningComponentBase
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
<DxGrid Data="@Data" PageSize="10">
    <Columns>
        <DxGridDataColumn FieldName="State" Width="5%" />
        <DxGridDataColumn FieldName="Area" MinWidth="100" />
        <DxGridDataColumn FieldName="City" Caption="County" MinWidth="100" />
        <DxGridDataColumn FieldName="Name" Caption="Location" MinWidth="100" />
        <DxGridDataColumn FieldName="Year" DisplayFormat="0" Width="10%" />
        <DxGridDataColumn FieldName="Bedrooms" Width="10%" />
        <DxGridDataColumn FieldName="Population" DisplayFormat="#,0" MinWidth="80" Width="10%" />
    </Columns>
</DxGrid>

@code {
    RentInfoDataService RentInfoDataService { get; set; }
    object Data { get; set; }
    protected override void OnInitialized() {
        // Refer to https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase
        RentInfoDataService = ScopedServices.GetRequiredService<RentInfoDataService>();
        var connectionString = ConnectionStringUtils.GetGridLargeDataConnectionString(Configuration);
        if(string.IsNullOrEmpty(connectionString)) return;
        var dataSource = new GridDevExtremeDataSource<AreaRentInfo>(RentInfoDataService.GetAreaRentInfo());
        dataSource.CustomizeLoadOptions = (loadOptions) => {
            loadOptions.PrimaryKey = new[] { "Oid" };
            loadOptions.PaginateViaPrimaryKey = true;
            loadOptions.StringToLower = true;
        };
        Data = dataSource;
    }
}
csharp
using System;
using System.Runtime.InteropServices;

namespace BlazorDemo.Data {
    public partial class AreaRentInfo {
        public AreaRentInfo() {
        }
        public Guid Oid { get; set; }
        public string State { get; set; }
        public string City { get; set; }
        public string Area { get; set; }
        public string Name { get; set; }
        public int Population { get; set; }
        public int Year { get; set; }
        public int Bedrooms { get; set; }
        public decimal Rent { get; set; }
    }
}
csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BlazorDemo.Data;

namespace BlazorDemo.Services {
    public partial class RentInfoDataService {
        public IQueryable<AreaRentInfo> GetAreaRentInfo() {
            // Return your data here
        }
    }
}
csharp
// ...
builder.Services.AddScoped<RentInfoDataService>();

Limitations and Specifics

Take the following filter menu limitations into account:

  • Filter values for date columns are rounded to dates and do not take into account time values.
  • If the Grid component is bound to an instant feedback data source, the number of filter menu items is limited to 10000.
  • If the Grid is bound to a server mode data source, null values are not considered unique. Handle the CustomizeFilterMenu event to process these values.
  • If a hierarchical filter menu contains more than 5,000 (Blazor Server) or 1,500 (Blazor WASM) unique dates, users may notice render delays. Consider a plain list or a date range instead.
  • A hierarchical filter menu treats filter items as dates and does not convert them to strings. That is why a column with a hierarchical filter menu does not support filtering by display text.
  • The filter menu and filter row use the same set of filter criteria for fields and may affect each other. For example, when a user types a value in the filter row and then picks a value in a filter menu of the same column, the Grid component applies different criteria. When a user enters a value in the filter row again, the Grid applies the new operator set by the filter menu.
  • A hierarchical filter menu does not support TimeOnly values.

Filter Synchronization

The Grid component automatically synchronizes filter changes across the following UI elements:

Of these elements, only the filter panel and filter builder can display all filter conditions. Other elements have limitations, for example, column filter menus do not support the Contains operator, and filter row cells cannot display the Betweencondition. To ensure the entire filter expression is visible, add a filter panel to the Grid. The panel also provides access to the filter builder dialog.

Note

The Grid updates a column’s FilterRowOperatorType once users apply filters to column data using another UI element. For instance, after a user selects a filter menu value, the column’s FilterRowOperatorType switches to Equals. The operator type remains Equals even after the filter is cleared.

You can extend filter row UI and display filter operators for each column. Refer to the following example for additional information: Incorporate a selector for filter row operator type.

Task-Based Examples

Change Display Text Used for Blank Values

csharp
void OnCustomizeFilterMenu (GridCustomizeFilterMenuEventArgs e) {
    if (e.DataColumn.FieldName == "TargetColumn") {
        e.DataItems.ForEach(di => {
            di.DisplayText = di.Value == null ? "Custom Null Text" : di.DisplayText;
        });
    }
}

Remove a Comma Separator

csharp
void OnCustomizeFilterMenu(GridCustomizeFilterMenuEventArgs e) {
    if (e.DataColumn.FieldName == "Values") {
        e.DataItems.ForEach(di => {
            var menuItemText = di.DisplayText;
            di.DisplayText = menuItemText.Replace(",", "");
        });
    }
}