A Blazor component library that provides a dynamic, type-safe filter builder UI for Gridify (dynamic LINQ filtering library), built with MudBlazor.
$ dotnet add package Selmir.MudGridifyA powerful, type-safe Blazor component library that provides a dynamic filter builder UI for Gridify, built with MudBlazor.
= (equals), != (not equals)> (greater than), < (less than), >= (greater or equal), <= (less or equal)=* (contains), !* (not contains)^ (starts with), !^ (not starts with)$ (ends with), !$ (not ends with),) or OR (|)/i suffix<ItemGroup>
<PackageReference Include="MudBlazor" Version="8.13.0" />
<PackageReference Include="Gridify" Version="2.17.0" />
<ProjectReference Include="../Selmir.MudGridify/Selmir.MudGridify.csproj" />
</ItemGroup>
using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Add MudBlazor services
builder.Services.AddMudServices();
await builder.Build().RunAsync();
@using MudBlazor
@using Selmir.MudGridify.Models
@using Selmir.MudGridify.Components
@using Gridify
@page "/filter-demo"
@using Selmir.MudGridify.Components
@using Selmir.MudGridify.Models
<GridifyFilterBuilder @ref="_filterBuilder"
FilterableProperties="@_filterableProperties"
ShowGeneratedQuery="true"
OnFilterChanged="@OnFilterChanged" />
<MudTable Items="@_filteredData" />
@code {
private GridifyFilterBuilder? _filterBuilder;
private List<FilterableProperty> _filterableProperties = new();
private List<MyModel> _allData = new();
private List<MyModel> _filteredData = new();
protected override void OnInitialized()
{
// Define filterable properties
_filterableProperties = new List<FilterableProperty>
{
new("FirstName", "First Name", FilterPropertyType.String),
new("Age", "Age", FilterPropertyType.Number),
new("BirthDate", "Birth Date", FilterPropertyType.Date),
new("IsActive", "Active", FilterPropertyType.Boolean)
{
TrueLabel = "Yes",
FalseLabel = "No"
}
};
_allData = GetData();
_filteredData = _allData;
}
private async Task OnFilterChanged(string filterQuery)
{
if (string.IsNullOrWhiteSpace(filterQuery))
{
_filteredData = new List<MyModel>(_allData);
}
else
{
// Apply Gridify filter
var query = _allData.AsQueryable();
_filteredData = query.ApplyFiltering(filterQuery).ToList();
}
}
}
| Parameter | Type | Default | Description |
|---|---|---|---|
FilterableProperties | List<FilterableProperty> | Required | List of properties that can be filtered |
InitialConditions | List<FilterCondition> | [] | Initial filter conditions to load |
ShowGeneratedQuery | bool | true | Whether to show the generated Gridify query string |
OnFilterChanged | EventCallback<string> | - | Event fired when the filter query changes |
public class FilterableProperty
{
public string PropertyName { get; set; } // Property name in your model
public string DisplayName { get; set; } // Display name shown to users
public FilterPropertyType PropertyType { get; set; } // Data type
public string? TrueLabel { get; set; } // Custom label for boolean true
public string? FalseLabel { get; set; } // Custom label for boolean false
}
public enum FilterPropertyType
{
String,
Number,
Boolean,
Date,
DateTime
}
// Get the current filter query
string query = _filterBuilder.GetFilterQuery();
// Set conditions programmatically
_filterBuilder.SetConditions(new List<FilterCondition>
{
new FilterCondition
{
Property = myProperty,
Operator = FilterOperator.GreaterThan,
Value = "100"
}
});
// Add a single condition
_filterBuilder.AddCondition(new FilterCondition { ... });
MudGridify supports URL persistence through the GridifyQueryParser utility, enabling shareable, bookmarkable filter states. This feature follows the application developer responsibility pattern - the component remains pure and routing-agnostic, while providing the tools you need to integrate URL persistence.
The GridifyQueryParser class parses Gridify query strings back into FilterCondition objects for state restoration.
Namespace: Selmir.MudGridify.Utilities
Method Signature:
public static (List<FilterCondition> conditions, bool isOrOperator) Parse(
string? gridifyQuery,
List<FilterableProperty> filterableProperties)
Parameters:
gridifyQuery: The Gridify query string from URL (e.g., "FirstName=John,Age>30")filterableProperties: Your list of filterable properties to resolve referencesReturns:
conditions: List of parsed FilterCondition objectsisOrOperator: true if OR logic is used (| separator), false for AND (, separator)@page "/my-data"
@inject NavigationManager Navigation
@using System.Web
<GridifyFilterBuilder @ref="_filterBuilder"
FilterableProperties="@_filterableProperties"
InitialConditions="@_initialConditions"
OnFilterChanged="@OnFilterChanged" />
@code {
private GridifyFilterBuilder? _filterBuilder;
private List<FilterableProperty> _filterableProperties = new();
private List<FilterCondition> _initialConditions = new();
private List<MyData> _allData = new();
private List<MyData> _filteredData = new();
protected override async Task OnInitializedAsync()
{
// 1. Define filterable properties
_filterableProperties = new List<FilterableProperty>
{
new("Name", "Name", FilterPropertyType.String),
new("Price", "Price", FilterPropertyType.Number),
new("CreatedDate", "Created", FilterPropertyType.Date)
};
// 2. Load data
_allData = await GetDataAsync();
_filteredData = _allData;
// 3. Parse URL parameters to restore filter state
await LoadFiltersFromUrl();
}
private async Task LoadFiltersFromUrl()
{
try
{
var uri = new Uri(Navigation.Uri);
var queryString = HttpUtility.ParseQueryString(uri.Query);
var filterParam = queryString["filter"];
if (!string.IsNullOrEmpty(filterParam))
{
// Parse Gridify query back into FilterConditions
var (conditions, isOrOperator) = GridifyQueryParser.Parse(
filterParam,
_filterableProperties
);
if (conditions.Any())
{
_initialConditions = conditions;
// Apply filter to data immediately
var query = _allData.AsQueryable();
_filteredData = query.ApplyFiltering(filterParam).ToList();
}
}
}
catch (Exception ex)
{
// Handle parsing errors gracefully
Console.WriteLine($"Error parsing URL filters: {ex.Message}");
}
}
private async Task OnFilterChanged(string filterQuery)
{
// Update URL with new filter (without page reload)
UpdateUrl(filterQuery);
// Apply filter to data
if (string.IsNullOrWhiteSpace(filterQuery))
{
_filteredData = new List<MyData>(_allData);
}
else
{
var query = _allData.AsQueryable();
_filteredData = query.ApplyFiltering(filterQuery).ToList();
}
}
private void UpdateUrl(string filterQuery)
{
var uri = new Uri(Navigation.Uri);
var baseUrl = $"{uri.Scheme}://{uri.Authority}{uri.AbsolutePath}";
if (string.IsNullOrWhiteSpace(filterQuery))
{
// Clear filter from URL
Navigation.NavigateTo(baseUrl, forceLoad: false, replace: true);
}
else
{
// Add/update filter query parameter
var encodedFilter = Uri.EscapeDataString(filterQuery);
var newUrl = $"{baseUrl}?filter={encodedFilter}";
Navigation.NavigateTo(newUrl, forceLoad: false, replace: true);
}
}
}
GridifyQueryParser.Parse() in OnInitializedAsync to read URL stateInitialConditions parameter with parsed conditionsNavigationManager.NavigateTo() with forceLoad: false, replace: true to update URL without reloadUri.EscapeDataString() when adding filter to URLThe URL structure follows this pattern:
https://example.com/page?filter=FirstName%3DJohn%2CAge%3E30
Where filter parameter contains the URL-encoded Gridify query string.
Using NavigationManager.NavigateTo() with replace: true updates the current history entry. To add new history entries (enabling back/forward navigation), use replace: false:
Navigation.NavigateTo(newUrl, forceLoad: false, replace: false);
See the URL Persistence page in the playground application for a working example with shareable links, browser history support, and detailed implementation notes.
cd src/Selmir.MudGridify.Playground
dotnet run
Navigate to http://localhost:5259 to see the component in action.
The component generates valid Gridify query strings:
# Single condition
FirstName=John
# Multiple conditions with AND
FirstName=John,Age>30
# Multiple conditions with OR
Department=Sales|Department=Marketing
# String operators
FirstName^J # Starts with J
Email$@company.com # Ends with @company.com
Department=*ing # Contains "ing"
# Case-insensitive
FirstName=john/i # Matches John, JOHN, john, etc.
# Comparison operators
Age>25 # Greater than 25
Salary>=100000 # Greater or equal to 100000
HireDate<2023-01-01 # Before Jan 1, 2023
# Complex queries
(FirstName=*Jo,Age<30)|(FirstName!=Hn,Age>30)
This project is provided as-is for demonstration purposes.
Contributions are welcome! Feel free to submit issues and pull requests.