Search code examples
c#dictionaryblazorref

How does ElementReference in Blazor work?


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);
    }
}

Solution

  • 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.