[Solved] ComboBox with Virtual Scrolling + ValueMapper: selected value text intermittently not displayed

2 Answers 24 Views
ComboBox
Next
Top achievements
Rank 1
Iron
Next asked on 18 Mar 2026, 12:05 PM
We have a Blazor Server application (.NET 10, Telerik UI for Blazor 13.0) where we use TelerikComboBox with OnRead, Virtual Scrolling, and ValueMapper. The selected value text intermittently fails to display. The ComboBox appears empty even though a value is selected.

The issue is more frequent on staging/production environments (higher SignalR latency), but it also happens on localhost — rarely, and typically on complex pages with heavy rendering (many components, multiple ComboBoxes, grids, etc.).


Component setup (simplified):

<div class="tooltip-target" title="@_selectedDescrizione">
    <TelerikComboBox OnRead="@LoadData"
                     TItem="MyEntity"
                     TValue="Guid?"
                     Value="@Value"
                     ValueChanged="@OnComboValueChanged"
                     ValueExpression="@ValueExpression"
                     TextField="@nameof(MyEntity.Nome)"
                     ValueField="@nameof(MyEntity.Id)"
                     Filterable="true"
                     FilterOperator="@StringFilterOperator.Contains"
                     PageSize="20"
                     ScrollMode="DropDownScrollMode.Virtual"
                     ItemHeight="40"
                     ValueMapper="@GetModelFromValue">
        <ComboBoxSettings>
            <ComboBoxPopupSettings Height="auto" MaxHeight="40vh" />
        </ComboBoxSettings>
        <ItemTemplate>
            @context.Nome
        </ItemTemplate>
    </TelerikComboBox>
</div>


OnRead — lazy loading with server-side filtering:

Standard OnRead handler that queries the database with the user's filter text and returns paginated results via ToDataSourceResultAsync:

private async Task LoadData(ComboBoxReadEventArgs args)
{
    var filterDescriptor = args.Request.Filters?.FirstOrDefault() as FilterDescriptor;
    string searchText = filterDescriptor?.Value?.ToString() ?? string.Empty;

    using var context = await ContextFactory.CreateDbContextAsync();

    IQueryable<MyEntity> query = context.Set<MyEntity>().AsNoTracking();

    if (!string.IsNullOrWhiteSpace(searchText))
    {
        var search = searchText.ToLower();
        query = query.Where(e => e.Nome.ToLower().Contains(search));
    }

    var result = await query
        .OrderBy(e => e.Nome)
        .ToDataSourceResultAsync(args.Request);

    args.Data = result.Data;
    args.Total = result.Total;
}


ValueMapper — resolves the selected value by ID:

Since we use OnRead with Virtual Scrolling, the pre-selected value may not be in the current page of data. The ValueMapper fetches the entity by ID so the ComboBox can display it. We also store the description as a side-effect for the wrapping tooltip:

private async Task<MyEntity> GetModelFromValue(Guid? selectedValue)
{
    if (!selectedValue.HasValue || selectedValue.Value == Guid.Empty)
        return null;

    return await ExecuteWithStateRefresh(async () =>
    {
        using var context = await ContextFactory.CreateDbContextAsync();

        var item = await context.Set<MyEntity>()
            .AsNoTracking()
            .FirstOrDefaultAsync(e => e.Id == selectedValue.Value);

        _selectedDescrizione = item?.Descrizione;

        return item;
    });
}


ExecuteWithStateRefresh — our workaround attempt:

We noticed that after the ValueMapper callback resolves asynchronously (invoked via JS Interop), Telerik does not trigger a re-render on its own. Without intervention, the resolved value frequently doesn't appear. We implemented a two-phase re-render mechanism in our base component class:

private bool _stateRefreshPending;

protected async Task<TResult> ExecuteWithStateRefresh<TResult>(Func<Task<TResult>> func)
{
    var result = await func();
    _stateRefreshPending = true;
    StateHasChanged(); // Batch 1: triggers render + OnAfterRenderAsync
    return result;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    if (_stateRefreshPending)
    {
        _stateRefreshPending = false;
        StateHasChanged(); // Batch 2: deferred render, after client acknowledged Batch 1
    }
}

The idea is that StateHasChanged() in Batch 1 travels to the client together with Telerik's JS Interop updates. After the client processes and acknowledges Batch 1, OnAfterRenderAsync fires and triggers Batch 2 — which should arrive after Telerik JS has finished updating the ComboBox internally. This improved reliability significantly, but did not fully solve the issue — the display text still occasionally fails to appear, especially on heavy pages or with network latency.


What we observe:
- The ValueMapper callback executes and returns the correct entity — confirmed by the _selectedDescrizione field being populated correctly (the wrapping <div> tooltip shows the correct description).
- Despite this, the ComboBox itself does not render the display text — it appears empty or shows the placeholder.
- Interacting with the ComboBox (e.g., opening the dropdown, clicking elsewhere) sometimes causes the value to suddenly appear.
- More frequent on staging (network latency), but also reproducible on localhost on complex/heavy pages.

Questions:
1. After the ValueMapper callback returns the resolved object, does the ComboBox process it synchronously or asynchronously on the JS side?
2. Is StateHasChanged() the intended mechanism to force the ComboBox to reflect the ValueMapper result, or is there a different recommended approach?

Environment:
- Telerik UI for Blazor 13.0
- .NET 10, Blazor Server
Douglas
Top achievements
Rank 1
commented on 20 Mar 2026, 12:51 PM

I am seeing the same behavior with the same environment setup. 

My workaround was to add a 100 ms task delay in the value mapper logic, which makes me think a race condition is occuring.

2 Answers, 1 is accepted

Sort by
0
Dimo
Telerik team
answered on 20 Mar 2026, 12:28 PM

Hello,

>> 1. After the ValueMapper callback returns the resolved object, does the ComboBox process it synchronously or asynchronously on the JS side?

We have an async MapSelectedItem() method that awaits your ValueMapper method and then awaits a JSInterop call that renders the selected item's text in the ComboBox <input />.

The above routine can start from:

  • OnParametersSetAsync of the ComboBox if the Value has changed. In this case the MapSelectedItem() method is awaited.
  • After the ComboBox OnRead handler executes if the Value has not changed. In this case the MapSelectedItem() method is discarded.

===

>> 2. Is StateHasChanged() the intended mechanism to force the ComboBox to reflect the ValueMapper result, or is there a different recommended approach?

That shouldn't be necessary.

===

There might be a specific scenario that causes a race condition. However, in order for us to troubleshoot and potentially fix it, we will have to reproduce it on our side.

Here is a runnable test page with some Console.Writeline() and Task.Delay() statements inside. No matter what delays I set or where do I set the ComboBox Value, the selected Value displays successfully no matter if the service returns data or not. So, can you update this example to reproduce the problem or send a similar one for a review?

@using Telerik.DataSource
@using Telerik.DataSource.Extensions

<p>ComboBox Value: @ComboBoxValue</p>

<TelerikComboBox Filterable="true"
                 FilterOperator="@StringFilterOperator.Contains"
                 ItemHeight="30"
                 OnRead="@OnComboBoxRead"
                 PageSize="20"
                 ScrollMode="@DropDownScrollMode.Virtual"
                 TextField="@nameof(MyEntity.Name)"
                 TItem="@MyEntity"
                 TValue="@(Guid?)"
                 @bind-Value="@ComboBoxValue"
                 ValueField="@nameof(MyEntity.Id)"
                 ValueMapper="@ComboBoxValueMapper"
                 Width="240px" />

@code{
    private Guid? ComboBoxValue { get; set; } // = Guid.ParseExact(MyService.HardCodedGuid, "D");

    private async Task OnComboBoxRead(ComboBoxReadEventArgs args)
    {
        Console.WriteLine("TelerikComboBox.OnRead Start");

        await Task.Delay(1);

        DataEnvelope<MyEntity> result = await MyService.GetItems(args.Request);

        args.Data = result.Data;
        args.Total = result.Total;

        await Task.Delay(1);

        Console.WriteLine("TelerikComboBox.OnRead End");
    }

    private async Task<MyEntity?> ComboBoxValueMapper(Guid? comboBoxValue)
    {
        Console.WriteLine("TelerikComboBox.ValueMapper Start");

        await Task.Delay(1);

        MyEntity? result = await MyService.GetItemFromValue(comboBoxValue);

        await Task.Delay(1);

        Console.WriteLine("TelerikComboBox.ValueMapper End");
        return result;
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(200);
        ComboBoxValue = Guid.ParseExact(MyService.HardCodedGuid, "D");

        await base.OnInitializedAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            //await Task.Delay(200);
            //ComboBoxValue = Guid.ParseExact(MyService.HardCodedGuid, "D");
        }

        await base.OnAfterRenderAsync(firstRender);
    }

    public static class MyService
    {
        public static string HardCodedGuid = "d8c94cf8-467c-4434-9162-fc3dcda82ceb"; 
        static List<MyEntity> AllData { get; set; } = new();

        public static async Task<DataEnvelope<MyEntity>> GetItems(DataSourceRequest request)
        {
            Console.WriteLine("MyService.GetItems Start");

            EnsureData();

            await Task.Delay(300);

            var result = await AllData.ToDataSourceResultAsync(request);
            DataEnvelope<MyEntity> dataToReturn = new DataEnvelope<MyEntity>
            {
                Data = result.Data.Cast<MyEntity>().ToList(),
                Total = result.Total
            };

            Console.WriteLine("MyService.GetItems End");

            return dataToReturn;
        }

        public static async Task<MyEntity?> GetItemFromValue(Guid? selectedValue)
        {
            Console.WriteLine("MyService.GetItemFromValue Start");

            EnsureData();

            await Task.Delay(100);

            MyEntity? result = AllData.FirstOrDefault(x => selectedValue == x.Id);

            Console.WriteLine("MyService.GetItemFromValue End");

            return result;
        }

        private static void EnsureData()
        {
            if (AllData.Count == 0)
            {
                AllData = Enumerable
                    .Range(1, 12345)
                    .Select(x => new MyEntity()
                        {
                            Id = Guid.NewGuid(),
                            Name = $"Name {x}"
                        })
                    .ToList();

                AllData[AllData.Count / 2] = new MyEntity()
                {
                    Id = Guid.ParseExact(HardCodedGuid, "D"),
                    Name = "Initial Value"
                };
            }
        }
    }

    public class DataEnvelope<T>
    {
        public int Total { get; set; }
        public List<T> Data { get; set; } = new();
    }

    public class MyEntity
    {
        public Guid Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }
}

 

Regards,
Dimo
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

0
Next
Top achievements
Rank 1
Iron
answered on 23 Mar 2026, 08:06 AM

This is a minimal page that reproduces what happens to us

@page "/debug/combobox-bug-repro"
@using Telerik.Blazor.Components
@using Telerik.DataSource
@using Telerik.DataSource.Extensions

<h3>ComboBox ValueMapper Bug Reproduction</h3>

<p>
    The ComboBox below has a pre-selected value (<strong>Sample Item 005</strong>).
    On most page loads, the input is empty. Reload the page to observe.
    Opening and closing the dropdown makes the value appear.
</p>

<TelerikComboBox OnRead="@OnReadHandler"
                 TItem="SampleItem"
                 TValue="Guid?"
                 Value="@SelectedValue"
                 ValueChanged="@((Guid? v) => SelectedValue = v)"
                 ValueExpression="@(() => SelectedValue)"
                 TextField="@nameof(SampleItem.Name)"
                 ValueField="@nameof(SampleItem.Id)"
                 Filterable="true"
                 FilterOperator="@StringFilterOperator.Contains"
                 ScrollMode="DropDownScrollMode.Virtual"
                 ItemHeight="36"
                 PageSize="20"
                 ValueMapper="@ValueMapperHandler"
                 Placeholder="Select an item..."
                 ShowClearButton="true">
    <ComboBoxSettings>
        <ComboBoxPopupSettings Height="auto" MaxHeight="40vh" />
    </ComboBoxSettings>
</TelerikComboBox>

@code {

    private Guid? SelectedValue { get; set; }
    private List<SampleItem> _allItems = new();

    protected override void OnInitialized()
    {
        _allItems = Enumerable.Range(1, 15).Select(i => new SampleItem
        {
            Id = new Guid($"00000000-0000-0000-0000-{i:D12}"),
            Name = $"Sample Item {i:D3}"
        }).ToList();

        SelectedValue = _allItems[4].Id; // pre-select "Sample Item 005"
    }

    private async Task OnReadHandler(ComboBoxReadEventArgs args)
    {
        await Task.Yield(); // minimal async yield — triggers the race condition

        var query = _allItems.AsQueryable();

        var filter = args.Request.Filters?.FirstOrDefault() as FilterDescriptor;
        var search = filter?.Value?.ToString() ?? string.Empty;
        if (!string.IsNullOrWhiteSpace(search))
            query = query.Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase));

        var result = await query.ToDataSourceResultAsync(args.Request);
        args.Data = result.Data;
        args.Total = result.Total;
    }

    private async Task<SampleItem> ValueMapperHandler(Guid? value)
    {
        if (!value.HasValue || value.Value == Guid.Empty)
            return null;

        await Task.Yield(); // minimal async yield — triggers the race condition

        return _allItems.FirstOrDefault(x => x.Id == value.Value);
    }

    private sealed class SampleItem
    {
        public Guid Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }
}

 

And this is your snipped edited

@page "/debug/combobox-bug-telerik"
@using Telerik.DataSource
@using Telerik.DataSource.Extensions

@*
    ============================================================
    MODIFICATIONS FROM ORIGINAL TELERIK SNIPPET TO REPRODUCE BUG
    ============================================================

    This file is based on the sample provided by Dimo (Telerik Support).
    Three changes were made to reliably trigger the race condition:

    1. OnInitializedAsync → OnInitialized (synchronous)
       ORIGINAL: await Task.Delay(200); ComboBoxValue = Guid.Parse(...);
       MODIFIED: ComboBoxValue = Guid.Parse(...);  // set immediately, no delay
       WHY: In real apps, the Value is passed as a [Parameter] from the parent
            component — it's already set when the ComboBox first renders.
            The original 200ms delay masked the bug by allowing OnAfterRenderAsync
            to run before OnParametersSetAsync, setting _isRendered = true early.

    2. Task.Delay(1) → Task.Yield() in OnRead and ValueMapper handlers
       ORIGINAL: await Task.Delay(1);
       MODIFIED: await Task.Yield();
       WHY: Task.Yield() guarantees a sync context yield without a timer delay.
            The bug occurs when async operations complete FAST (before _isRendered
            is set to true in OnAfterRenderAsync). Task.Delay(1) may or may not
            yield depending on timer resolution. Task.Yield() always yields.

    3. Task.Delay(300/100) → Task.Yield() in MyService methods
       ORIGINAL: await Task.Delay(300); / await Task.Delay(100);
       MODIFIED: await Task.Yield();
       WHY: Long delays (100-300ms) MASK the bug because the ValueMapper completes
            AFTER OnAfterRenderAsync sets _isRendered = true, allowing UpdateJsValue
            to fire. With minimal yields, the ValueMapper completes BEFORE
            _isRendered = true → UpdateJsValue is skipped → input stays empty.

    ROOT CAUSE: In ComboBoxBase.MapSelectedItem(), the guard
        if (_isRendered && hasItemChanged) { await UpdateJsValue(...); }
    skips the JS update when _isRendered is false. During OnParametersSetAsync,
    _isRendered is not yet true (set in OnAfterRenderAsync). If the ValueMapper
    completes fast enough to finish during OnParametersSetAsync, UpdateJsValue
    is never called, and InitComboBox (which runs once) sends null to the JS widget.
    ============================================================
*@

<h3>ComboBox ValueMapper Bug — Based on Telerik Support Snippet</h3>

<p>ComboBox value: <strong>@(ComboBoxValue?.ToString() ?? "(null)")</strong></p>
<p>Expected text: <strong>Initial Value</strong></p>

<TelerikComboBox Filterable="true"
                 FilterOperator="@StringFilterOperator.Contains"
                 ItemHeight="30"
                 OnRead="@OnComboBoxRead"
                 PageSize="20"
                 ScrollMode="@DropDownScrollMode.Virtual"
                 TextField="@nameof(MyEntity.Name)"
                 TItem="MyEntity"
                 TValue="Guid?"
                 @bind-Value="@ComboBoxValue"
                 ValueField="@nameof(MyEntity.Id)"
                 ValueMapper="@ComboBoxValueMapper"
                 Width="240px" />

<hr />
<p><em>If the input above is empty, the bug has reproduced. Open and close the dropdown to see the value appear.</em></p>

@code {
    private Guid? ComboBoxValue { get; set; }

    private async Task OnComboBoxRead(ComboBoxReadEventArgs args)
    {
        // Using Task.Yield() instead of Task.Delay(1) to guarantee a sync context yield
        // This is the key to reproducing the race condition reliably
        await Task.Yield();

        DataEnvelope<MyEntity> result = await MyService.GetItems(args.Request);

        args.Data = result.Data;
        args.Total = result.Total;
    }

    private async Task<MyEntity?> ComboBoxValueMapper(Guid? comboBoxValue)
    {
        await Task.Yield();

        MyEntity? result = await MyService.GetItemFromValue(comboBoxValue);

        return result;
    }

    protected override void OnInitialized()
    {
        // Set value synchronously — same as receiving it as a Parameter from parent
        ComboBoxValue = Guid.ParseExact(MyService.HardCodedGuid, "D");
    }

    // ========== Inline service (no external dependencies) ==========

    public static class MyService
    {
        public static string HardCodedGuid = "d8c94cf8-467c-4434-9162-fc3dcda82ceb";
        static List<MyEntity> AllData { get; set; } = new();

        public static async Task<DataEnvelope<MyEntity>> GetItems(DataSourceRequest request)
        {
            EnsureData();

            // Minimal yield — simulates async data access
            await Task.Yield();

            var result = await AllData.ToDataSourceResultAsync(request);
            DataEnvelope<MyEntity> dataToReturn = new DataEnvelope<MyEntity>
            {
                Data = result.Data.Cast<MyEntity>().ToList(),
                Total = result.Total
            };

            return dataToReturn;
        }

        public static async Task<MyEntity?> GetItemFromValue(Guid? selectedValue)
        {
            EnsureData();

            await Task.Yield();

            MyEntity? result = AllData.FirstOrDefault(x => selectedValue == x.Id);

            return result;
        }

        private static void EnsureData()
        {
            if (AllData.Count == 0)
            {
                AllData = Enumerable
                    .Range(1, 12345)
                    .Select(x => new MyEntity()
                    {
                        Id = Guid.NewGuid(),
                        Name = $"Name {x}"
                    })
                    .ToList();

                AllData[AllData.Count / 2] = new MyEntity()
                {
                    Id = Guid.ParseExact(HardCodedGuid, "D"),
                    Name = "Initial Value"
                };
            }
        }
    }

    public class DataEnvelope<T>
    {
        public int Total { get; set; }
        public List<T> Data { get; set; } = new();
    }

    public class MyEntity
    {
        public Guid Id { get; set; }
        public string Name { get; set; } = string.Empty;
    }
}

 

Regards

Dimo
Telerik team
commented on 23 Mar 2026, 12:19 PM

Thanks for the code examples. I reproduced the problem and logged a public bug report on your behalf. I also awarded you some Telerik points.

I hope it's possible for you to delay the Value setting, the ValueMapper execution, or the ComboBox rendering as a workaround. Please excuse us for the extra effort.

Tags
ComboBox
Asked by
Next
Top achievements
Rank 1
Iron
Answers by
Dimo
Telerik team
Next
Top achievements
Rank 1
Iron
Share this question
or