URL-synchronized filter models for control panel data grids. Covers QueryStringBindable base class, two-way URL binding, and PropertyChanged notification. Trigger: query string, URL filter, bindable, filter model, URL sync.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
QueryStringBindable base class handles two-way bindingINotifyPropertyChanged triggers UI updates on filter changesToQuery() maps filter model to API query parametersnamespace {Company}.{Domain}.ControlPanel.Models;
public abstract class QueryStringBindable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public void BindToNavigationManager(NavigationManager nav)
{
var uri = new Uri(nav.Uri);
var query = QueryHelpers.ParseQuery(uri.Query);
LoadFromQuery(query);
PropertyChanged += (_, args) =>
{
var queryString = ToQueryString();
var baseUri = uri.GetLeftPart(UriPartial.Path);
nav.NavigateTo($"{baseUri}{queryString}", replace: true);
};
}
protected abstract void LoadFromQuery(
Dictionary<string, StringValues> query);
protected abstract string ToQueryString();
}
namespace {Company}.{Domain}.ControlPanel.Models;
public sealed class OrderFilterModel : QueryStringBindable
{
private string? _search;
private int _page = 1;
private int _pageSize = 20;
private string? _status;
private string? _sortBy;
public string? Search
{
get => _search;
set { _search = value; OnPropertyChanged(); }
}
public int Page
{
get => _page;
set { _page = value; OnPropertyChanged(); }
}
public int PageSize
{
get => _pageSize;
set { _pageSize = value; OnPropertyChanged(); }
}
public string? Status
{
get => _status;
set { _status = value; OnPropertyChanged(); }
}
public string? SortBy
{
get => _sortBy;
set { _sortBy = value; OnPropertyChanged(); }
}
protected override void LoadFromQuery(
Dictionary<string, StringValues> query)
{
if (query.TryGetValue("search", out var search))
_search = search;
if (query.TryGetValue("page", out var page) && int.TryParse(page, out var p))
_page = p;
if (query.TryGetValue("pageSize", out var ps) && int.TryParse(ps, out var s))
_pageSize = s;
if (query.TryGetValue("status", out var status))
_status = status;
if (query.TryGetValue("sortBy", out var sortBy))
_sortBy = sortBy;
}
protected override string ToQueryString()
{
var parts = new List<string>();
if (!string.IsNullOrEmpty(_search)) parts.Add($"search={Uri.EscapeDataString(_search)}");
if (_page != 1) parts.Add($"page={_page}");
if (_pageSize != 20) parts.Add($"pageSize={_pageSize}");
if (!string.IsNullOrEmpty(_status)) parts.Add($"status={_status}");
if (!string.IsNullOrEmpty(_sortBy)) parts.Add($"sortBy={_sortBy}");
return parts.Count > 0 ? "?" + string.Join("&", parts) : "";
}
public GetOrdersRequest ToApiRequest() => new()
{
Page = Page,
PageSize = PageSize,
Search = Search,
Status = Status,
SortBy = SortBy
};
}
@page "/orders"
@inject NavigationManager NavigationManager
@code {
private OrderFilterModel _filter = new();
private MudDataGrid<OrderSummaryResponse>? _dataGrid;
protected override void OnInitialized()
{
_filter.BindToNavigationManager(NavigationManager);
_filter.PropertyChanged += async (_, _) =>
{
if (_dataGrid is not null)
await _dataGrid.ReloadServerData();
};
}
}
| Anti-Pattern | Correct Approach |
|---|---|
| Filter state not in URL | Use QueryStringBindable for shareable URLs |
| Missing debounce on text fields | Debounce search fields (300ms) |
| Full page reload on filter change | Use NavigateTo with replace: true |
| Loading query from URL in every render | Load once in OnInitialized |
# Find QueryStringBindable
grep -r "QueryStringBindable" --include="*.cs" src/ControlPanel/
# Find filter models
grep -r "FilterModel\|Filter.*: QueryString" --include="*.cs" src/ControlPanel/
# Find BindToNavigationManager
grep -r "BindToNavigationManager" --include="*.razor" src/ControlPanel/
QueryStringBindable base class{Entity}FilterModelOnPropertyChanged()OnInitialized with BindToNavigationManagerPropertyChanged to data grid reload