1

My planned design is to have a home page with 4 buttons. When a user clicks any of these buttons it adds an item to the navigation. I used this singleton to manage state

public class StateManager
{
    public event Action NewPlanAdded;
    public void AddPlan()
    {
        NewPlanAdded?.Invoke();
    }
}

I injected this singleton into the child component

@inject StateManager StateManager

<button class="btn btn-primary" @onclick="CreateNewPlan">Create New Plan</button>

@code {
    private void CreateNewPlan()
    {
        StateManager.AddPlan();
    }
}

And the navigation component

@inject StateManager StateManager

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        @foreach (var item in Items)
        {
            <li class="nav-item px-3">
                <NavLink class="nav-link" href="@item.Item1" >
                    <span class="@item.Item2" aria-hidden="true"></span> @item.Item3
                </NavLink>
            </li>
        }        
    </ul>
</div>

@code {
    IList<Tuple<string, string, string>> Items { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        StateManager.NewPlanAdded += NewPlanAdded;
        Items = new List<Tuple<string, string, string>>
        {
            new Tuple<string, string, string>("", "oi oi-home", "Home")
        };
    }

    public void Dispose()
    {
        StateManager.NewPlanAdded -= NewPlanAdded;
    }

    private void NewPlanAdded()
    {
        Items.Add(new Tuple<string, string, string>("plan", "oi oi-plus", "Plan (unsaved)"));
    }
}

The problem is that the new list items don't appear until the navigation component is hidden and then displayed again. How do I get the foreach to refresh when the collection changes?

Adam
  • 1,613
  • 1
  • 16
  • 24

1 Answers1

2

You should to inform your component that state has been changed executing InvokeAsync(StateHasChanged):

private void NewPlanAdded()
{
    Items.Add(new Tuple<string, string, string>("plan", "oi oi-plus", "Plan (unsaved)"));
    InvokeAsync(StateHasChanged);  //<--- this line
}

Realize that, in your scenario, just to invoke StateHasChanged() it's not enough, you should to do it from right thread (using InvokeAsync)

dani herrera
  • 39,746
  • 4
  • 87
  • 153
  • I'm seeing several examples showing `InvokeAsync(() => StateHasChanged());`. Why is your method better? – JHBonarius Feb 15 '21 at 14:05
  • 1
    @JHBonarius, is the same. InvokeAsync takes as argument a function and StateHasChanged is a function. You can send StateHasChanged as parameter or make a new anonymous function `()=>StateHasChanged()` and send the anonymous function. – dani herrera Feb 15 '21 at 14:09
  • 1
    @JHBonarius it's the same thing with fewer characters, makes it a bit easier to read. – Adam Feb 15 '21 at 14:10
  • Thanks! I'm integrating this into my code as we type, so less characters = better ;) leaves me more characters to type in SO comments :P – JHBonarius Feb 15 '21 at 14:11
  • by the way I'm still not sure I should `await` `InvokeAsync` or not... thinking on what's the worst that could happen if I just fire and forget... Maybe a concurrency exception when a second thread calls it when the first call is not finished yet? – JHBonarius Feb 15 '21 at 14:14
  • Hi, interesting question on the `await`. I believe the "simple" answer is that as `NewPlanAdded` is a fire and forget, you should only await if you have stuff after the await that needs the awaited action to complete. The not so simple answer depends on what the awaited method does! – MrC aka Shaun Curtis Feb 15 '21 at 21:03