Blazor update UI inside component foreach loop - c#

I have Blazor components in a way similar to this:
Main.razor
#foreach (Vehicle vehicle in _vehicles)
{
if (vehicle.Visible)
{
<VehicleComponent Vehicle=#vehicle></VehicleComponent>
}
}
#code {
protected override void OnInitialized()
{
_vehicles = new List<Vehicles>();
// -> _vehicles being filled here
base.OnInitialized();
}
}
VehicleComponent.razor
#if (Vehicle != null)
{
<img src="#(Vehicle.src)"/>
<div id="#(Vehicle.Id)" tabindex="#(Vehicle.tabindex)">
<h3>#(Vehicle.text)</h3>
</div>
}
#code {
[Parameter] public Vehicle Vehicle { get; set; }
}
The problem is that all the VehicleComponents inside the loop are rendered after the loop is finished. But I want the component to completely render, before the next List item will be rendered.
I tried using StateHasChanged() after each item in Main.razor, but I got an infinite loop.
Is there a way I can render each component in the loop after each other (and update the UI)?

If you want the visual effect of 'gradually appearing' items:
protected override async Task OnInitializedAsync ()
{
var temp = new List<Vehicles>();
// -> _vehicles being filled here
_vehicles = new List<Vehicles>();
foreach (var vehicle in temp)
{
_vehicles.Add(vehicle);
StateHasChanged();
await task.Delay(500); // adjust the delay to taste
}
}
but be aware that this is an expensive (slow) way to show them.
Using #key won't improve this but it might help when you change the list later:
<VehicleComponent #key=vehicle Vehicle=#vehicle></VehicleComponent>

I think you are misunderstanding the Render process.
Your Razor code gets compiled into a C# class with your Razor compiled as a RenderFragment (a delegate) - you can see it in the project obj/Debug/Net5 folder structure. The component "Render" events queue this delegate onto the Renderer's Queue. The Renderer executes these delegates and applies changes to the Renderer's DOM. Any changes in the Renderer's DOM get passed to the browser to modify it's DOM.
Henk's answer adds the items slowly to the list. It adds a "Render" event through StateHasChanged and then yields through the Task.Delay to let the Renderer re-render the list on each iteration.

Related

Blazor causing all children to re-render (firstRender is true) instead of a differential update

I have a blazor element which has a CascadingValue.
<Microsoft.AspNetCore.Components.Forms.EditForm EditContext="#EditContext" Model="#Model" OnValidSubmit="#OnValidSubmitInternal" >
<Microsoft.AspNetCore.Components.Forms.ObjectGraphDataAnnotationsValidator />
<CascadingValue Value="#CurrentFormState" Name="FormState" >
#ChildContent(EditContext!)
</CascadingValue>
</Microsoft.AspNetCore.Components.Forms.EditForm>
Here is the relevant bits in my #code block.
private FormStateContext CurrentFormState { get; set; } = new();
private void SetState(FormState s)
CurrentFormState.State = s;
StateHasChanged();
}
private async Task OnValidSubmitInternal(Microsoft.AspNetCore.Components.Forms.EditContext context)
{
SetState(FormState.Submitting);
try
{
await OnValidSubmit.InvokeAsync();
SetState(FormState.Success);
}
catch (HttpProblemException e)
{
// [...] omitted code.
SetState(FormState.Error);
}
catch (Exception e)
{
// [...] omitted code.
SetState(FormState.Error);
}
finally
{
await Task.Delay(2000);
SetState(FormState.Idle);
}
}
The call to StateHasChanged() seems to render the whole sub-tree of ChildContent from scratch. That is, in OnAfterRenderAsync(bool firstRender), firstRender is true. Without calling StateHasChanged() the cascading value is never updated on descendant components which use it.
Why is my whole subtree being re-rendered when StateHasChanged() is called? No only is this poor performance, but it breaks other javascript interop libraries which mount to an actual element on the page. In my case, stripe.js isn't working because blazor is deleting and recreating the DOM elements instead of just updating via the normal diff.
I've tried adding an #key attribute on the EditForm, but the ChildContent still renders from scratch.
This seems to be a bug in Microsoft.AspNetCore.Components.Forms.EditForm when both EditContext and Model are passed into the element. I fixed this by doing something like the following:
#if (Model != null)
{
<Microsoft.AspNetCore.Components.Forms.EditForm Model="#Model" >
// ....
</Microsoft.AspNetCore.Components.Forms.EditForm>
}
else
{
<Microsoft.AspNetCore.Components.Forms.EditForm EditContext="#EditContext" >
// ...
</Microsoft.AspNetCore.Components.Forms.EditForm>
}
With this, my whole sub tree doesn't render from scratch again.

Is there a way to cache RenderFragment output?

Is it possible to cache the output of a RenderFragment in Blazor WebAssembly?
Specifically, this is to retain components shown intermittently without rendering them to the browser. With "rendering to the browser" here I mean outputting HTML to the browser.
I am trying to get this to work to improve performance in a library I am writing where two-dimensional data is shown in the browser. The resulting grid is virtualized to prevent having tens of thousands of elements in the DOM since having that many elements results in a degraded experience.
When virtualizing, elements outside the view are not rendered only to be rendered when they shift into view.
The caching mechanism should preserve the HTML output of Razor Components such that the components can be removed from and added to the DOM without having to be reinitialized and rerendered.
Currently, I have not found a way to achieve this.
A basic set-up to reproduce what I have tried so far is as follows:
create a Blazor WebAssembly project using the default template without hosting.
Add a Razor Component with the name ConsoleWriter.razor to Shared and set the contents as follows:
<h3>ConsoleWriter #Name</h3>
#code {
[Parameter]
public string? Name { get; set; }
protected override void OnInitialized() {
Console.WriteLine($"ConsoleWriter {this.Name} initialized");
}
protected override void OnAfterRender(bool firstRender) {
if(firstRender) {
Console.WriteLine($"ConsoleWriter {this.Name} rendered");
} else {
Console.WriteLine($"ConsoleWriter {this.Name} re-rendered");
}
}
// Trying to stop rerendering with ShouldRender.
// Does not stop the rendering in scenario's where the component has just been initialized.
protected override bool ShouldRender() => false;
}
Replace the contents of Index.razor with the following code:
#page "/"
<p>Components are showing: #showComponents</p>
<button #onclick="() => this.showComponents=!this.showComponents">Toggle</button>
#* Here the components are in the DOM but can be hidden from view, still bogging down the DOM if there are too many. *#
<div style="#(this.showComponents ? null : "display:none;")">
<ConsoleWriter Name="OnlyHidden" /> #* Does not intialize or rerender when showComponents is toggled *#
</div>
#* Here the components are not in the DOM at all when hidden, which is the intended scenario but this initializes the components every time they are shown. *#
#if(this.showComponents) {
<ConsoleWriter Name="Default" /> #* Intializes and rerenders when showComponents is toggled *#
#consoleWriter #* Intializes and rerenders when showComponents is toggled *#
<ConsoleWriter #key="consoleWriterKey" Name="WithKey" /> #* Intializes and rerenders when showComponents is toggled *#
}
#code {
private bool showComponents = false;
private object consoleWriterKey = new object();
private RenderFragment consoleWriter { get; } = builder => {
builder.OpenComponent<ConsoleWriter>(0);
builder.AddAttribute(1, nameof(ConsoleWriter.Name), "RenderFragment");
builder.CloseComponent();
};
}
When running the project and checking the browser console, you can see which components report being reinitialized or rerendered.
The components can be toggled by clicking the button.
Unfortunately, all of those being removed from and added to the DOM report back whenever being toggled to be shown despite their content never changing.
Does anyone have another idea how to approach this?

How do I access the code block within a Blazor RenderFragment?

I'm new to programming, C# Blazor, and RenderFragments in general, so forgive me if this is a basic question.
I need to set up a tabbed dialog interface where different tabs appear based on which menu buttons are clicked. I set up a DevExpress menu item which appears as a button:
<DxMenuItem Text="States" IconCssClass="homeMenuButtonStates" CssClass="menuItemSm" Click="OnStatesBrowseClick" />
When the user clicks it, the OnStatesBrowseClick function is called:
public void OnStatesBrowseClick()
{
tabName[tdiIndex] = "States";
TdiNewTab(tabName[tdiIndex]);
}
As you can see, its job is to assign a value to a member of a string array (tdiIndex was assigned "0"), and call the TdiNewTab method with argument tabName[tdiIndex] which evaluates to "States."
TdiNewTab has a switch statement which, for now, only has a case for "States."
public void TdiNewTab(string switchTabName) {
switch (switchTabName)
{
case "States":
renderStatesBrowsePage(tdiIndex);
break;
}
tdiIndex++;
}
In the event that "States" comes up, it points to renderStatesBrowsePage which should create a tab.
private RenderFragment renderStatesBrowsePage(int index)
{
deleteMe++;
RenderFragment item = tabsChildContent =>
{
deleteMe++;
tabsChildContent.OpenComponent<DxTabPage>(0);
tabsChildContent.AddAttribute(2, "Text", $"Tab # {index}");
tabsChildContent.AddAttribute(3, "ChildContent", (RenderFragment)((tabPageChildContent) =>
{
tabPageChildContent.OpenComponent<States>(0);
tabPageChildContent.CloseComponent();
}));
tabsChildContent.CloseComponent();
};
return item;
}
However, nothing happens. Using breakpoints, I determined that the block within "RenderFragment item = tabsChildContent =>" does not get executed. Indeed, when I put both "deleteMe++"s in there, only the first one incremented the variable.
I'm at a loss as to why the block won't execute. Any advice would be much appreciated!
In response to answer from Mister Magoo:
I'm now trying to call the RenderFragment from within my markup:
<DxMenuItem Text="States" IconCssClass="homeMenuButtonStates" CssClass="menuItemSm" Click="StatesBrowseClick">
#if (statesBrowse == 1)
{
#renderStatesBrowsePage(0);
}
</DxMenuItem>
#code
public void StatesBrowseClick()
{
int statesBrowse = 1;
}
However, nothing still happens. I made sure statesBrowse, which was initialized to "0" outside of the method, equaled 1 after the click. I also tried removing the #if statement (leaving just "#renderStatesBrowsePage(0);", and I got the same result.
Note: I'm using VS 2019, and in StatesBrowseClick, it tells me that "The variable 'statesBrowse' is assigned, but its value is never used." This seems inconsequential, though, as I have tried manually setting it to "1" and removing the "if" statement.
The block won't execute because there is nothing executing it.
renderStatesBrowsePage returns a RenderFragment that needs to be rendered - but you are discarding it here:
public void TdiNewTab(string switchTabName) {
switch (switchTabName)
{
case "States":
// This returns a RenderFragment but you are discarding it
renderStatesBrowsePage(tdiIndex);
break;
}
tdiIndex++;
}
It is hard to tell you what you should be doing without seeing more of your project, but generally a RenderFragment is rendered by adding it to the component markup
<div>some stuff</div>
<div>
#MyRenderFragment
</div>

Refresh blazor server child component from parent component by using StateHasChanged

Blazor relaod child component
One of my component is like below ( This is a child component in my case )
ListMetaData component / Child Component
<table>
bind the list of items from model to the table
</table>
#code {
List<MetaDataViewModel> model= new List<MetaDataViewModel>();
protected override async Task OnInitializedAsync()
{
await LoadMetaDataList();
}
public async Task LoadMetaDataList()
{
model= dbContextService.fetchFromDb();
}
public void Refresh()
{
StateHasChanged();
}
}
I have a parent component and it is like below
#page "/settings/metadata"
#inject IDialogService DialogService
#inject ISnackbar Snackbar
<MudGrid>
<MudItem >
<MudButton OnClick="#((e) => AddNewmetaDataEvent())"
Class="ma-1">Add new</MudButton>
</MudItem>
<MudItem xs="12" >
<ListMetaData #ref="_listComponent" ></ListMetaData>
</MudItem>
</MudGrid>
#code {
private ListMetaData _listComponent;
async Task AddNewmetaDataEvent()
{
var dialog = DialogService.Show<AddMetaDataDialog>("Add new MetaData");
var result = await dialog.Result;
if (!result.Cancelled)
{
Snackbar.Add("Please wait while the list is getting updated", Severity.Info);
_listComponent.Refresh();
}
else
{
Snackbar.Add("Dialog cancelled without performing any action", Severity.Normal);
}
}
}
From parent component on button click event a dialog is appearing and allowing user to add new item to the database.
On close of dialog i am trying to reload the list of items in child component
But the list is not getting updated for me even after using StateHasChanged(). I have to reload the page to see the modified list of items in the child component
I followed couple of answers from community How to refresh a blazor sub/child component within a main/parent component? but not getting what is not correct in my code
This should work (however see the notes)
#page "/settings/metadata"
#inject IDialogService DialogService
#inject ISnackbar Snackbar
<MudItem >
<MudButton OnClick="#((e) => AddNewmetaDataEvent())"
Class="ma-1">Add new</MudButton>
</MudItem>
#if (_updating)
{
<span>Updating</span>
}
else
{
<MudGrid>
<MudItem xs="12" >
<ListMetaData #ref="_listComponent" ></ListMetaData>
</MudItem>
</MudGrid>
}
#code {
private ListMetaData _listComponent;
private bool _updating;
async Task AddNewmetaDataEvent()
{
_updating = true; // forces UI dom refresh
var dialog = DialogService.Show<AddMetaDataDialog>("Add new MetaData");
var result = await dialog.Result;
if (!result.Cancelled)
{
Snackbar.Add("Please wait while the list is getting updated", Severity.Info);
}
else
{
Snackbar.Add("Dialog cancelled without performing any action", Severity.Normal);
}
_updating = false; // forces UI dom refresh
}
}
Try to avoid updating the child components by capturing the reference. I am aware that UI libraries like SyncFusion etc force this kind of #ref / Refresh() pattern. Avoid it as much as possible and instead pass on the data as parameter . That will save you from these type of manual refreshes.

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

Categories