Skip to content

feat(Select): use DynamicElement ensure release event callback#7894

Merged
ArgoZhang merged 1 commit intomainfrom
feat-select
Apr 19, 2026
Merged

feat(Select): use DynamicElement ensure release event callback#7894
ArgoZhang merged 1 commit intomainfrom
feat-select

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Apr 19, 2026

Link issues

fixes #7893

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Improve the Select component’s event handling and item rendering behavior to ensure proper callback lifecycle and better integration with DynamicElement.

Bug Fixes:

  • Ensure the Select input’s change event uses a managed EventCallback that is cleared on dispose to avoid stale or leaking event handlers.

Enhancements:

  • Render Select dropdown items via DynamicElement to better support extended element behavior and styling.
  • Trigger a UI refresh after selecting an item to immediately reflect the updated state in the Select component.

Copilot AI review requested due to automatic review settings April 19, 2026 06:44
@bb-auto bb-auto bot added the enhancement New feature or request label Apr 19, 2026
@bb-auto bb-auto bot added this to the v10.5.0 milestone Apr 19, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 19, 2026

Reviewer's Guide

Refactors the Select component’s item rendering and input change handling by routing click events through DynamicElement, centralizing the editable input’s change EventCallback, forcing a re-render after item selection, and cleaning up the callback on disposal to avoid stale references.

Sequence diagram for editable Select input change handling with centralized EventCallback

sequenceDiagram
    actor User
    participant Browser
    participant SelectComponent

    User->>Browser: Type/select value in input
    Browser->>SelectComponent: onchange via _onChangeEventCallback
    activate SelectComponent
    SelectComponent->>SelectComponent: OnChange(args)
    SelectComponent->>SelectComponent: Update CurrentValueAsString
    SelectComponent->>SelectComponent: Determine SelectedItem from allItems
    SelectComponent-->>Browser: Render updated UI (implicit)
    deactivate SelectComponent
Loading

Sequence diagram for Select item click routed through DynamicElement

sequenceDiagram
    actor User
    participant Browser
    participant DynamicElement
    participant SelectComponent
    participant BlazorRenderer

    User->>Browser: Click dropdown item
    Browser->>DynamicElement: OnClick event
    DynamicElement->>SelectComponent: OnClickItem(item)
    activate SelectComponent
    SelectComponent->>SelectComponent: Update _defaultVirtualizedItemText
    SelectComponent->>SelectComponent: SelectedItemChanged(item)
    SelectComponent->>SelectComponent: StateHasChanged()
    SelectComponent-->>BlazorRenderer: Request re-render
    BlazorRenderer-->>Browser: Patch DOM with new selection
    deactivate SelectComponent
Loading

Updated class diagram for Select component with DynamicElement and EventCallback management

classDiagram
    class SimpleSelectBase_TValue_ {
    }

    class Select_TValue_ {
        - bool IsEditable
        - string _defaultVirtualizedItemText
        - EventCallback~ChangeEventArgs~ _onChangeEventCallback
        - SelectedItem SelectedItem
        - SelectedItem SelectedRow
        + void OnParametersSet()
        + Task OnClickItem(SelectedItem item)
        + Task SelectedItemChanged(SelectedItem item)
        + Task OnChange(ChangeEventArgs args)
        + SelectedItem GetItemByCurrentValue()
        + ValueTask DisposeAsync(bool disposing)
    }

    Select_TValue_ --|> SimpleSelectBase_TValue_
Loading

File-Level Changes

Change Details Files
Centralize and cache the editable input’s change callback so it can be safely cleared on disposal.
  • Introduce a private EventCallback field to store the change handler.
  • Initialize the callback in OnParametersSet only when the Select is editable, otherwise keep it empty.
  • Wire the input’s @onchange binding to the cached callback instead of directly to the handler method.
  • Reset the cached EventCallback to empty in DisposeAsync when disposing.
src/BootstrapBlazor/Components/Select/Select.razor.cs
src/BootstrapBlazor/Components/Select/Select.razor
Route item click handling through DynamicElement and ensure UI re-renders after selection.
  • Replace the div used for rendering selectable rows with DynamicElement while preserving click behavior and CSS class binding.
  • Call StateHasChanged after processing OnClickItem to force a re-render once an item is selected.
src/BootstrapBlazor/Components/Select/Select.razor.cs
src/BootstrapBlazor/Components/Select/Select.razor

Assessment against linked issues

Issue Objective Addressed Explanation
#7893 Update the Select component item rendering to use DynamicElement instead of a plain HTML element to ensure proper event callback handling/release.
#7893 Ensure the Select component’s change event callback is explicitly managed and released on disposal to avoid lingering event callbacks.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ArgoZhang ArgoZhang merged commit c2dcc2e into main Apr 19, 2026
6 checks passed
@ArgoZhang ArgoZhang deleted the feat-select branch April 19, 2026 06:44
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/BootstrapBlazor/Components/Select/Select.razor.cs" line_range="205" />
<code_context>

     private string _defaultVirtualizedItemText = "";

+    private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
+
     private SelectedItem? SelectedItem { get; set; }
</code_context>
<issue_to_address>
**issue (complexity):** Consider removing the stateful EventCallback field and explicit StateHasChanged call by handling editability and rendering declaratively or within the handler to keep the component logic simpler and more self-contained.

You can simplify this change by removing the stateful `EventCallback` field and its disposal, and by tightening the click handler.

### 1. Remove `_onChangeEventCallback` field and lifecycle wiring

The field and `OnParametersSet` / `DisposeAsync` logic are only used to conditionally wire `OnChange`. You can achieve the same behavior declaratively in the `.razor` file without extra mutable state or lifecycle coupling.

**Instead of:**

```csharp
// .razor.cs
private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;

protected override void OnParametersSet()
{
    base.OnParametersSet();

    PlaceHolder ??= Localizer[nameof(PlaceHolder)];
    NoSearchDataText ??= Localizer[nameof(NoSearchDataText)];
    DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectDropdownIcon);
    ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon);

    _onChangeEventCallback = IsEditable
        ? EventCallback.Factory.Create<ChangeEventArgs>(this, OnChange)
        : EventCallback<ChangeEventArgs>.Empty;
}

protected override ValueTask DisposeAsync(bool disposing)
{
    if (disposing)
    {
        _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
    }

    return base.DisposeAsync(disposing);
}
```

Bind directly in the `.razor`:

```razor
<input
    @onchange="(IsEditable
        ? EventCallback.Factory.Create<ChangeEventArgs>(this, OnChange)
        : EventCallback<ChangeEventArgs>.Empty)" />
```

Then you can delete:

```csharp
private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
```

and the `DisposeAsync` override entirely. The component instance is short‑lived; clearing a value-type callback in `DisposeAsync` brings no real benefit.

If you prefer to keep the binding simple, another option is to short‑circuit inside the handler:

```csharp
private Task OnChange(ChangeEventArgs e)
{
    if (!IsEditable)
    {
        return Task.CompletedTask;
    }

    // existing logic...
}
```

and then use a straightforward binding:

```razor
<input @onchange="OnChange" />
```

Either pattern removes the extra field and lifecycle complexity while preserving behavior.

### 2. Reconsider explicit `StateHasChanged` in `OnClickItem`

`OnClickItem` currently forces a render:

```csharp
private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }

    StateHasChanged();
}
```

If `SelectedItemChanged` flows into `CurrentValue` / parameter updates that already trigger a render (which is common in Blazor forms), the explicit `StateHasChanged()` is redundant and can be removed:

```csharp
private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }
}
```

If you’ve hit a specific scenario where a manual refresh is required (e.g., async external state not bound to the component), consider adding a brief comment so future readers understand why it’s needed:

```csharp
private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }

    // Explicit refresh required because <reason>, this is not triggered by normal binding.
    StateHasChanged();
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


private string _defaultVirtualizedItemText = "";

private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider removing the stateful EventCallback field and explicit StateHasChanged call by handling editability and rendering declaratively or within the handler to keep the component logic simpler and more self-contained.

You can simplify this change by removing the stateful EventCallback field and its disposal, and by tightening the click handler.

1. Remove _onChangeEventCallback field and lifecycle wiring

The field and OnParametersSet / DisposeAsync logic are only used to conditionally wire OnChange. You can achieve the same behavior declaratively in the .razor file without extra mutable state or lifecycle coupling.

Instead of:

// .razor.cs
private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;

protected override void OnParametersSet()
{
    base.OnParametersSet();

    PlaceHolder ??= Localizer[nameof(PlaceHolder)];
    NoSearchDataText ??= Localizer[nameof(NoSearchDataText)];
    DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectDropdownIcon);
    ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon);

    _onChangeEventCallback = IsEditable
        ? EventCallback.Factory.Create<ChangeEventArgs>(this, OnChange)
        : EventCallback<ChangeEventArgs>.Empty;
}

protected override ValueTask DisposeAsync(bool disposing)
{
    if (disposing)
    {
        _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;
    }

    return base.DisposeAsync(disposing);
}

Bind directly in the .razor:

<input
    @onchange="(IsEditable
        ? EventCallback.Factory.Create<ChangeEventArgs>(this, OnChange)
        : EventCallback<ChangeEventArgs>.Empty)" />

Then you can delete:

private EventCallback<ChangeEventArgs> _onChangeEventCallback = EventCallback<ChangeEventArgs>.Empty;

and the DisposeAsync override entirely. The component instance is short‑lived; clearing a value-type callback in DisposeAsync brings no real benefit.

If you prefer to keep the binding simple, another option is to short‑circuit inside the handler:

private Task OnChange(ChangeEventArgs e)
{
    if (!IsEditable)
    {
        return Task.CompletedTask;
    }

    // existing logic...
}

and then use a straightforward binding:

<input @onchange="OnChange" />

Either pattern removes the extra field and lifecycle complexity while preserving behavior.

2. Reconsider explicit StateHasChanged in OnClickItem

OnClickItem currently forces a render:

private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }

    StateHasChanged();
}

If SelectedItemChanged flows into CurrentValue / parameter updates that already trigger a render (which is common in Blazor forms), the explicit StateHasChanged() is redundant and can be removed:

private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }
}

If you’ve hit a specific scenario where a manual refresh is required (e.g., async external state not bound to the component), consider adding a brief comment so future readers understand why it’s needed:

private async Task OnClickItem(SelectedItem item)
{
    if (!item.IsDisabled)
    {
        _defaultVirtualizedItemText = item.Text;
        await SelectedItemChanged(item);
    }

    // Explicit refresh required because <reason>, this is not triggered by normal binding.
    StateHasChanged();
}

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (54f2347) to head (99b5837).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7894   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          764       764           
  Lines        34177     34200   +23     
  Branches      4703      4705    +2     
=========================================
+ Hits         34177     34200   +23     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Select to use DynamicElement for option rows and adjusts event callback handling to better ensure event callbacks are released/managed correctly (per #7893).

Changes:

  • Introduces a cached @onchange EventCallback that is only wired when IsEditable is enabled, and clears it on disposal.
  • Replaces option-row <div @onclick> rendering with <DynamicElement OnClick=...> for row click handling.
  • Triggers a re-render after item selection to reflect updated selection state.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/BootstrapBlazor/Components/Select/Select.razor.cs Adds cached onchange callback setup/cleanup and forces re-render after selection.
src/BootstrapBlazor/Components/Select/Select.razor Uses _onChangeEventCallback for @onchange and switches row rendering to DynamicElement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 396 to 400
await SelectedItemChanged(item);
}

StateHasChanged();
}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnClickItem now calls StateHasChanged(), but ConfirmSelectedItem also calls StateHasChanged() after awaiting OnClickItem. This means selecting via keyboard Enter (JS-invoked) will trigger two renders. Consider removing the extra StateHasChanged() from ConfirmSelectedItem so the selection path renders only once.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(Select): use DynamicElement ensure release event callback

2 participants