Stingray.Components.MultiSelectComponent 2.2.0

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package Stingray.Components.MultiSelectComponent --version 2.2.0
                    
NuGet\Install-Package Stingray.Components.MultiSelectComponent -Version 2.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Stingray.Components.MultiSelectComponent" Version="2.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Stingray.Components.MultiSelectComponent" Version="2.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Stingray.Components.MultiSelectComponent" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Stingray.Components.MultiSelectComponent --version 2.2.0
                    
#r "nuget: Stingray.Components.MultiSelectComponent, 2.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Stingray.Components.MultiSelectComponent@2.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Stingray.Components.MultiSelectComponent&version=2.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Stingray.Components.MultiSelectComponent&version=2.2.0
                    
Install as a Cake Tool

Stingray.Components.MultiSelectComponent

Searchable single- and multi-select dropdowns for Blazor, in server-paged and offline variants.

Virtualized option lists, debounced search, keyboard navigation, EditForm validation, RTL, and localization through a cascaded IStringLocalizer. Works on Blazor Server and WebAssembly; the design deliberately keeps opening, closing, focusing and keyboard navigation off the SignalR wire.


Contents


Setup

Add the stylesheet to your host page (App.razor, or index.html for WebAssembly):

<link rel="stylesheet" href="_content/Stingray.Components.MultiSelectComponent/stingray-dropdown.css" />

Link it yourself rather than have the component inject it, so your app controls load order — anything after that line can override it at equal specificity. An injected link would be appended to <head> after your own stylesheets and would then beat every override you wrote, at equal specificity, from inside a package you cannot edit.

That is the only wiring. There is no service registration and no <script> tag — the component imports its own module, on a URL it versions itself.

If the dropdowns render as plain unstyled boxes, or open and show nothing, this link is missing or the path is wrong.

_content/ paths are unhashed, and a host serving them with UseStaticFiles sends no Cache-Control at all — only an ETag. That leaves a browser free to reuse the copy it already has without revalidating, so after an upgrade you can be running the new assembly against the old stylesheet. It matters more here than it looks: the menu is painted entirely by this stylesheet's :popover-open rules, so a stale copy is not a cosmetic regression — it is a dropdown that opens and shows nothing.

The module handles this itself — it stamps its own import URL with the package version, which is why you will see ?v= on it in devtools — but the stylesheet is your markup, so give it a version too. On .NET 9+:

<link rel="stylesheet" href="@Assets["_content/Stingray.Components.MultiSelectComponent/stingray-dropdown.css"]" />

@Assets[…] requires app.MapStaticAssets() in place of app.UseStaticFiles(). It fingerprints the URL, and — the part that matters here — serves unfingerprinted _content/ paths as no-cache, so they revalidate on every request.

On .NET 8, or on any host still using UseStaticFiles, stamp the version yourself — but take it from the package rather than typing it, or it is one more number to remember at upgrade time:

@using Stingray.Components.MultiSelectComponent

<link rel="stylesheet"
      href="@($"_content/Stingray.Components.MultiSelectComponent/stingray-dropdown.css?v={MultiSelectVersion.Current}")" />

MultiSelectVersion.Current is the package's own informational version, prerelease label included, and it is the identical value the module stamps on its import URL. Hard-coding ?v=2.1.0 works until the upgrade nobody remembers to edit it for, which is the upgrade that needs it.

Requirements

  • .NET 8.0 or later.
  • The Popover API — Chrome/Edge 114+, Safari 17+, Firefox 125+.

The menu is a popover="auto" element opened by a popovertarget invoker, so the browser owns opening it, dismissing it on an outside click, and dismissing it on Escape. There is no fallback path: without the API the trigger does nothing.

Choosing a component

Fetches pages from you Filters a list you supply
One value SearchableSingleDropdown SearchableSingleDropdownOffline
Many values SearchableMultiDropdown SearchableMultiDropdownOffline

Use a server variant when the option set is too large to send to the browser, or lives behind a query. You supply SearchDataConsumer (a page of results for a search term) and LoadSelectedDataConsumer (the display objects behind values that are already selected — needed because a selected value may not be on the current page).

Use an offline variant when you already have the whole list in memory. You supply Datasource; sorting and filtering happen locally.

The two type parameters

Every variant is generic over two types, and getting them the right way round matters:

  • TInput — the option object you work with (Person).
  • TOutput — the value you bind (int, for Person.Id).

OutputSelector maps one to the other. They are often the same type in offline dropdowns, where binding the whole object is convenient.

Quick start

Server-paged, multi-select

<SearchableMultiDropdown TInput="Person" TOutput="int"
                         TextSelector="p => p.Name"
                         OutputSelector="p => p.Id"
                         SearchDataConsumer="SearchPeople"
                         LoadSelectedDataConsumer="LoadPeopleByIds"
                         @bind-SelectedOptions="selectedIds" />

@code {
    List<int> selectedIds = new();

    // (search term, page size, start index) -> one page, plus the total for that term.
    async Task<SearchableDropDownDto<Person>> SearchPeople(string search, int pageSize, int startIndex)
    {
        var query = _db.People.Where(p => search == "" || p.Name.Contains(search));
        return new SearchableDropDownDto<Person>
        {
            TotalCount = await query.CountAsync(),
            Items      = await query.Skip(startIndex).Take(pageSize).ToListAsync(),
        };
    }

    // The display objects behind values that are already selected.
    Task<List<Person>> LoadPeopleByIds(List<int> ids) =>
        _db.People.Where(p => ids.Contains(p.Id)).ToListAsync();
}

TotalCount is the count for the whole search, not the page — it is what sizes the scrollbar.

Offline, single-select

<SearchableSingleDropdownOffline TInput="string" TOutput="string"
                                 TextSelector="x => x"
                                 OutputSelector="x => x"
                                 Datasource="_countries"
                                 @bind-SelectedOption="_country" />

Custom option rendering

<SearchableSingleDropdownOffline TInput="Person" TOutput="int"
                                 TextSelector="p => p.Name"
                                 OutputSelector="p => p.Id"
                                 Datasource="_people"
                                 @bind-SelectedOption="_personId">
    <ItemTemplate>
        <span class="badge bg-info me-2">@context.Id</span>
        <strong>@context.Name</strong>
    </ItemTemplate>
</SearchableSingleDropdownOffline>

ItemTemplate covers both the menu rows and the selected display. Use MenuItemTemplate or SelectedItemTemplate to differ between them; both fall back to ItemTemplate, then to TextSelector.

Parameters

Required, on every variant

Parameter Type Purpose
TextSelector Func<TInput, string?> The option's display text. Also what search filters on, offline.
OutputSelector Func<TInput, TOutput> Maps an option to the value you bind.

Common, on every variant

Parameter Type Default Purpose
MinCharacterCount int? 1 Characters before a search runs. Below it, the list is unfiltered. Values under 1 are raised to 1.
PageSize int? 20 Rows per fetch. Values under 1 are raised to 1.
ClearSearchValueAfterExpanding bool? true Reopening starts from an empty search box.
ShowPaginationInformation bool? false Shows the "Showing x To y From z" counters. They describe the rendered window, so an empty result reads Showing 0 To 0 From 0; the line is never withheld.
ShowTooltip bool? false Puts the option's text in a title attribute.
IsDisabled bool false Disables the control.
ItemSize float 30 Row height in px, for virtualization. Keep in step with --stingray-dropdown-item-height.
CssClass string "" Added to the component's root element.
PlaceHolder string "Select" Shown when nothing is selected.
SearchInputPlaceHolder string "Search..." Placeholder in the search box.
NoMatchPlaceHolder string "No available records" Shown when the list is empty.
SearchValue string "" Initial search term. Read once, at first render — the search box is uncontrolled, so assigning this later does not change what is in it.
InputDefaultValue TInput? default Treated as "no selection" on the input side.
OutputDefaultValue TOutput? default Treated as "no selection" on the output side.
InputEquality Func<TInput, TInput, bool> EqualityComparer<TInput>.Default How two options are compared.
OutputEquality Func<TOutput, TOutput, bool> EqualityComparer<TOutput>.Default How two values are compared. Supply this for reference types compared by id.
ItemTemplate RenderFragment<TInput>? Custom rendering for menu rows and the selected display.
MenuItemTemplate RenderFragment<TInput>? Menu rows only. Falls back to ItemTemplate.
SelectedItemTemplate RenderFragment<TInput>? Selected display only. Falls back to ItemTemplate.
OnLoadError EventCallback<Exception> Raised when a load fails, with the exception. See Handling load failures.

The three defaults above that are quiet by design — ClearSearchValueAfterExpanding, ShowPaginationInformation, ShowTooltip — changed in 2.0. See Upgrading.

Server variants only

Parameter Type Purpose
SearchDataConsumer Func<string, int, int, Task<SearchableDropDownDto<TInput>>> Required. (search, pageSize, startIndex) → one page plus the total for that search.
LoadSelectedDataConsumer Func<TOutput, Task<TInput>> (single) / Func<List<TOutput>, Task<List<TInput>>> (multi) Required. Resolves already-selected values to display objects.

Offline variants only

Parameter Type Default Purpose
Datasource IEnumerable<TInput> [] Required. The full option list. A null is treated as empty.
IsDescinding bool? null true/false sort by TextSelector; null keeps the given order.

Single-select only

Parameter Type Default Purpose
SelectedOption TOutput Bind with @bind-SelectedOption.
SelectedOptionChanged EventCallback<TOutput> Supplied by @bind-.
SelectedOptionExpression Expression<Func<TOutput>>? Supplied by @bind-; drives validation.
AllowDeselect bool true Whether the clear button is offered.

Multi-select only

Parameter Type Default Purpose
SelectedOptions List<TOutput> Bind with @bind-SelectedOptions.
SelectedOptionsChanged EventCallback<List<TOutput>> Supplied by @bind-.
SelectedOptionsExpression Expression<Func<List<TOutput>>>? Supplied by @bind-; drives validation.
MaxSelectionCount int 0 Cap on selections; 0 means no cap. At the cap, unselected rows are disabled and selected ones stay clickable.
AllowSelectAll bool true (offline multi only) Offers a "select all" row above the options.
SelectAllPlaceHolder string "Select All" (offline multi only) Label for that row.

Cascading values

Cascade Required Purpose
IStringLocalizer no Localizes built-in strings; falls back to the English key.
EditContext no Supplied by EditForm; drives validation styling.
<CascadingValue Value="Localizer">
    <SearchableSingleDropdown … />
</CascadingValue>

Localization

Cascade an IStringLocalizer. Keys are the natural English strings, so an unlocalized app shows sensible text with no cascade at all:

Select · Search... · No available records · Loading · Showing · To · From · Select All · Remove · Clear · Clear all · Could not load data

RTL is supported throughout — the stylesheet uses logical properties, so an ar culture mirrors without extra work.

Theming

Set any of these on the dropdown or any ancestor:

Property Default Applies to
--stingray-dropdown-hover-bg #ededed Row hover and the selected row
--stingray-dropdown-item-height 30px Option row height
--stingray-dropdown-menu-max-height 200px Scroll height of the option list
--stingray-dropdown-error-color #b02a37 The load-failure row
.my-compact-dropdown {
    --stingray-dropdown-item-height: 24px;
    --stingray-dropdown-menu-max-height: 320px;
}

Keep --stingray-dropdown-item-height in step with the ItemSize parameter — that is the number virtualization uses to size the scroll region, and a mismatch shows as drifting scroll positions.

These properties are only ever read by the package, never declared on the component itself, so setting one on any ancestor wins.

Validation

Inside an EditForm, @bind- supplies the expression the component needs and validation styling follows automatically:

<EditForm Model="_model" OnValidSubmit="Save">
    <DataAnnotationsValidator />

    <SearchableSingleDropdown TInput="Person" TOutput="int"
                              TextSelector="p => p.Name"
                              OutputSelector="p => p.Id"
                              SearchDataConsumer="SearchPeople"
                              LoadSelectedDataConsumer="LoadPersonById"
                              @bind-SelectedOption="_model.OwnerId" />
    <ValidationMessage For="() => _model.OwnerId" />
</EditForm>

If you set the value and the change handler separately instead of using @bind-, also set SelectedOptionExpression / SelectedOptionsExpression — without it the component cannot tell the EditContext which field changed.

Handling load failures

When SearchDataConsumer or LoadSelectedDataConsumer throws, the component:

  1. shows a localized "Could not load data" row inside the menu, where the results would be, and
  2. raises OnLoadError with the exception.

The exception's own message is never rendered. An e.Message out of EF or an HTTP wrapper names tables, columns and internal hosts. Subscribe to OnLoadError if you want the detail:

<SearchableMultiDropdown … OnLoadError="ex => _logger.LogError(ex, "Dropdown load failed")" />

The failure is cleared as soon as a later fetch succeeds, or when the search term changes.

Behaviour worth knowing

Search is debounced — 500ms on server variants, 250ms offline. A fetch issued for a term you have already typed past is discarded rather than cached, so results cannot arrive out of order.

Pages are cached per search term, and the cache is dropped when the term changes. Reopening the menu does not refetch.

The menu closes itself when its trigger scrolls out of view or is clipped by a scrollable ancestor. The menu is position: fixed, so it would otherwise hang in the viewport next to nothing. A focused menu on a narrow viewport is exempt, so an on-screen keyboard cannot dismiss the menu it was opened to type into.

Keyboard: the search box is the single tab stop. Arrow keys, Home and End move between options; Enter or Space selects; Escape closes and returns focus to the trigger.

The menu is a popover in the top layer, so it is not clipped by overflow: hidden, not affected by transformed ancestors, and not subject to your z-index stack — while remaining a child of the component in the DOM, so your theming tokens still inherit into it.

Multi-select shows up to five chips in the control, with a count badge and a clear-all button. Server-backed multi-selects also pin the current selection above the search results in the menu.

Upgrading from 1.x

No .razor markup changes are required. Five things to check.

  1. Link the stylesheet (see Setup). 1.x shipped scoped CSS that arrived automatically; 2.0 serves one stylesheet from _content/, so without the link the dropdowns render unstyled.

  2. The Popover API is now required. See Requirements. 1.x re-parented the menu to <body>; 2.0 uses the top layer instead.

  3. Four defaults changed. Set them explicitly if you relied on the old values:

    Parameter 1.x 2.0
    MinCharacterCount 3 1
    ClearSearchValueAfterExpanding false true
    ShowPaginationInformation true false
    ShowTooltip true false

    ShowTooltip is the most visible: long option text no longer gets a native hover tooltip.

  4. appsettings keys are gone. StingrayComponents_Dropdown_*, StingrayComponents_PaginatedDropdown_* and StingrayComponents_MinCharacterCount are no longer read. Every one of them is already a parameter — set it there.

  5. The ShowMessage cascade is gone, and with it the need for a window.DotNetHelper script in your host page. Load failures now surface in the menu and through OnLoadError. If your host page declares DotNetHelper for this package you can delete it; leaving it does no harm.

Also note: SearchableMultiDropdownOffline.Datasource widened from List<TInput> to IEnumerable<TInput>. Passing a list still compiles.

If you have CSS overriding the component's internals, re-check it. The control is now a flex row containing a .sd-toggle button rather than one big <button>, so the chips and clear buttons are no longer nested interactive content.

For AI coding agents

This package ships an AGENTS.md describing the contracts that are easy to break from the outside — what looks like a bug but is deliberate, what must never be done, and the accessibility guarantees. It extracts to:

~/.nuget/packages/stingray.components.multiselectcomponent/<version>/AGENTS.md

Nothing reads it automatically. To point your agent at it, add this to your own CLAUDE.md, AGENTS.md or equivalent:

## Stingray MultiSelect

When working with `SearchableSingleDropdown`, `SearchableMultiDropdown`,
`SearchableSingleDropdownOffline` or `SearchableMultiDropdownOffline`, first read
`~/.nuget/packages/stingray.components.multiselectcomponent/<version>/AGENTS.md`
(substitute the version from your `.csproj`). It documents behaviour that looks like a bug
and is not, and constraints that are not obvious from the API surface.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
Loading failed