How does ElementReference in Blazor work?

Viewed 4439

I'm building a dropdown list that filters based on input. When you click the down arrow it should focus on the first item in the list. The following is a simplified version of the issue I'm getting.

When you enter "a" in the input and then delete it, and afterwards press the down arrow, I get: The given key was not present in the dictionary. Can someone explain why this would happen? Why wouldn't the key be in the dictionary? What stopped it from being added?

<div class="parent" >
<div class="container">
    <input class="input" @oninput="FilterIdentifiers" @onkeydown="(e) => HandleKBEvents(e)"/>
    <div class="dropdown">
        @{
            int i = 1;
            
            foreach (var identifier in identifiersUI)
            {
                <div class="dropdown-item" @key="identifier" tabindex="@i" @ref="refs[identifier]">@identifier</div>

                i++;
            }
        }


    </div>
</div>
@code {
private List<string> identifiers = new List<string>
{
    "Aaa",
    "Aab",
    "Aba",
    "Aca",
    "Baa",
    "Bba"
};

private List<string> identifiersUI = new List<string>();

private Dictionary<string, ElementReference> refs = new Dictionary<string, ElementReference>();

protected override async Task OnInitializedAsync()
{
    identifiersUI = identifiers;
}

private void FilterIdentifiers(ChangeEventArgs e)
{
    string input = e.Value.ToString();
    refs.Clear();
    identifiersUI = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();
    
}

private void HandleKBEvents(KeyboardEventArgs e)
{
    if (e.Code == "ArrowDown")
    {
        refs[identifiersUI[0]].FocusAsync();

    }
}
}

When I change the FilterIdentifiers method to this it does work however. I saw this GitHub Blazor @ref and I thought I would try to only remove the keys that were filtered out to see if that made any difference...and it actually worked. Don't really understand why tho...

private void FilterIdentifiers(ChangeEventArgs e)
{
    string input = e.Value.ToString();
    var id = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();

    //remove removed elements from refs
    var elements = identifiersUI.Except(id);
    identifiersUI = id;

    foreach (var element in elements)
    {
        refs.Remove(element);
    }
}
2 Answers

Simply remove the refs.Clear() and let the Blazor UI manage refs.

private void FilterIdentifiers(ChangeEventArgs e)
{
   string input = e.Value.ToString();
   //refs.Clear();
identifiersUI = identifiers.Where(s => s.ToUpper().StartsWith(input.ToUpper())).ToList();
}

There's a lot going on under the hood when you use @ref with an HTML element rather than a Component object. When you clear refs manually, on the next render event, Blazor still thinks they're there, so doesn't add them again. Filtering out the a's removes the first value, hence the error. If you put break points in, you'll find you only have two elements in refs, the b's. Filter on b's and all appears to work, but in fact you only have four elements in refs - only the a's are present.

I would say this is due to the fact that the entire dictionary was cleared. And I presume you were hoping for Blazor to "re build" that dictionary every time the identifiersUI list was reset, while looping over it to redraw.

But Blazor's change tracking probably decided (and rightfully so) that those elements hadn't changed and left them alone. And it's especially keeping track of your dropdown-items, taking special care not fiddle too much with those elements who's identifier has not changed, since you have set the @key attribute.

Related