Scott Perry
Scott Perry

Reputation: 75

Blazor WASM - StateHasChanged() Not Updating UI

I'm using Blazor WASM on .NET6. I'm trying to filter names of links on a mega menu.

I have my reactive filter which is a html input, and based on the text entered in the input, if any text is matching a link name, the matching part of the link name is highlighted in red. It is working properly, except that it requires a mouse click (anywhere) or hitting the enter key after the text is entered in the filter to update the UI with the highlighted text. I tried raising the "onkeypress" event on the input, that calls the "RefreshUI" method that invokes StateHasChanged() asyncronously (I will provide a small sample below).

I put the classes in the razor file so that you can see a working sample (I do put them in separate class files). GetMenuData() was altered here to provide a smaller, bogus set of data. The real one just consumes JSON from a web api and converts it into the object.

I verified that the event gets raised and StateHasChanged() is called, but I'm not understanding why the DOM is not updating. Any help would be greatly appreciated.

@page "/counter"
@using System.Text.RegularExpressions

<PageTitle>Counter</PageTitle>

<ul>
  <li id="filter" class="show">
    <div class="form-group fg--search">
        <input type="search" class="input" placeholder="Search for Menu Items..." @bind="filterTerm" @onkeypress="RefreshUI">
    </div>
  </li>
  <li>
    <a href="#">Business Metrics</a>
    <ul class="business-metrics flexbox">
        <li class="col3">
            @if (menuItems != null)
            {
                foreach (SubMenu sm in menuItems.Where(x => x.MenuText == "Business Metrics").Select(x => x.Submenus).FirstOrDefault())
                {
                    <div class="menu-div">
                        <ul>
                            <li><h6><a href="#">@sm.SubmenuText</a></h6></li>
                            @foreach (IntranetMenuData imd in sm.SubmenuLinks)
                            {
                                <li><a href="imd.LinkUrl">@((MarkupString)FilterMenu(imd.LinkTitle))</a></li>
                            }
                        </ul>
                    </div>
                }
            }
        </li>
    </ul>
  </li>
</ul>

@code {
  private string filterTerm;
  private List<TopMenuHeading> menuItems;

  protected override void OnInitialized()
  {
    GetMenuData();
    base.OnInitialized();
  }

  private string FilterMenu(string link)
  {
    if (!string.IsNullOrWhiteSpace(link) && !string.IsNullOrWhiteSpace(filterTerm))
    {
        Match matchedText = Regex.Match(link, @"(?i:" + filterTerm + @")");
        if (matchedText.Success)
        {
            return link.Replace(matchedText.Value, "<span style=\"color: #e8112d;text-decoration: underline;font-weight: 600;\">" + matchedText.Value + "</span>", StringComparison.InvariantCultureIgnoreCase);
        }
        else
        {
            return link;
        }
    }
    else
    {
        return link;
    }
  }

  private void GetMenuData()
  {
    IntranetMenuData imd1 = new IntranetMenuData();
    IntranetMenuData imd2 = new IntranetMenuData();
    List<IntranetMenuData> mItems = new List<IntranetMenuData>();
    SubMenu sub1 = new SubMenu();
    List<SubMenu> subMenus = new List<SubMenu>();
    TopMenuHeading tmh = new TopMenuHeading();
    List<TopMenuHeading> topMenuHeadings = new List<TopMenuHeading>();

    imd1.LinkTitle = "Capacity Utilization";
    imd2.LinkTitle = "Relieved Hours";
    mItems.Add(imd1);
    mItems.Add(imd2);

    sub1.SubmenuText = "Capacity";
    sub1.SubmenuLinks = mItems;
    subMenus.Add(sub1);

    tmh.MenuText = "Business Metrics";
    tmh.Submenus = subMenus;
    topMenuHeadings.Add(tmh);

    menuItems = topMenuHeadings;
  }

  private async Task RefreshUI()
  {
    await InvokeAsync(() => StateHasChanged());
  }

  public class IntranetBookmarks
  {
    public long EmployeeNumber { get; set; }
    public string Submenu { get; set; }
    public string Text { get; set; }
    public string Url { get;set; }
  }

  public class IntranetMenuData
  {
    public long LinkMenuId { get; set; }
    public string LinkOnClick { get; set; }
    public string LinkTitle { get; set; }
    public string LinkUrl { get; set; }
  }

  public class SubMenu
  {
    public List<IntranetMenuData> SubmenuLinks { get; set; }
    public string SubmenuText { get; set; }

    public SubMenu()
    {
        SubmenuLinks = new List<IntranetMenuData>();
    }
  }

  public class TopMenuHeading
  {
    public string MenuText { get; set; }
    public List<SubMenu> Submenus { get; set; }

    public TopMenuHeading()
    {
        Submenus = new List<SubMenu>();
    }
  }
}

Upvotes: 1

Views: 891

Answers (2)

Cyrille Con Morales
Cyrille Con Morales

Reputation: 967

I use @bind-value so everytime you set a valkue in result the input value changes

<input @onchange="filter()"/>
<input @bind-value="@result"/>

private string result {get;set;}

void filter(){
 //Do something and set result=output
}

Upvotes: 0

Neil W
Neil W

Reputation: 9257

Your keypress is not triggering a 'Bind'. It just asks the DOM to refresh, but your filterTerm value will not have changed.

The default binding happens with the 'onchange' event. This happens when you press enter or the input loses focus.

You want binding for the 'oninput' event, which will occur every time the content of the input changes:

<input type="search" 
       class="input" 
       placeholder="Search for Menu Items..." 
       @bind="filterTerm" 
       @bind:event="oninput">

As this causes an oninput event to be triggered, and the Blazor engine automatically triggers a DOM refresh after a UI event is handled, you should be able to remove your onkeypress handler as a force refresh is not required.

Upvotes: 1

Related Questions