skills/microservice/controlpanel/query-string-bindable/SKILL.md
Use when synchronizing filter models with URL query strings in Blazor data grids.
npx skillsauth add faysilalshareef/dotnet-ai-kit query-string-bindableInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 reloaddata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.