blazor StateHasChanged not working in a loop - c#

I have a Blazor Server app. I dynamically add a custom control (which I created in a Razor Class Library) to my page, like this:
<div class="container ">
<div class="row ">
<div class="col-12">
<label>#p_section.question</label>
</div>
</div>
#foreach (object new_control_model in p_section.list_of_control_models)
{
DisaggregateControlModel new_disag_model = (DisaggregateControlModel)new_control_model;
<DisaggregateControl #ref="myComponents[new_disag_model.id]" model="new_disag_model">
</DisaggregateControl>
}
</div>
This add the control to my dictionary, which I can access.
#code {
private Dictionary<string, object> myComponents = new Dictionary<string, object>();
}
In the custom control, I have a method that sets a bool, which allows me to display or hide a Div. In the web component that has this code, I want to iterate over all the objects in myComponents and either turn on or off the div display. I do that like this:
foreach (string id in some_list_of_ids){
//find the object in myComponents list
object found_obj = myComponents.FirstOrDefault(x => x.Key == id).Value;
//cast to my custom control
DisaggregateControl myControl = (DisaggregateControl)found_obj;
// based on property, determine if I should show the div or not
if(myControl.some_vale >0){
//show
myControl.showDiv(true);
}else{
myControl.showDiv(false);
}
}
//updated all the controls, so update the page
StateHasChanged();
If I debug and walk through, I can see that the code works. The correct divs are shown/hidden. Until the code reaches StateHasChanged(), and then all the divs are hidden. If I remove StateHasChanged then the code also does not work (the divs are not shown, when they should be).
I am not sure what the issue is or how to best handle this?

Turns out that it was an issue with my variable. In the custom library, I showed/hid the div by setting a bool value. But, I inadvertently made the bool static. Once I changed that, it worked fine. I assume because the variable was static, once I changed it for one control, it got changed for all controls.

Related

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 can I handle onclick event for multiple items and keep AuthorizedView

I have a razor component which displays a menu:
<div class="menu">
Home
Report
<AuthorizedView Roles="admin">
Admin Page
</AuthorizedView>
</div>
In the above example, the main(home) page is active by default. Once the user clicks on any of the items, the active class needs to be removed from any previous item and assigned to the newly clicked item.
What is the best way to do that in a blazor server app?
Here is the current work around I am using:
<div class="menu">
<a href="/"
class="item #(IsHomeActive?"active":"")"
#onclick="()=> { ClearActive(); IsHomeActive=true; }"> Home </a>
<a href="/report"
class="item #(IsReportActive?"active":"")"
#onclick="() => { ClearActive(); IsReportActive = true;} ">Report</a>
<AuthorizedView Roles="admin">
<a href="/admin"
class="item #(IsAdminActive?"active":"")"
#onclick="() => { ClearActive(); IsAdminActive = true;}">Admin Page</a>
</AuthorizedView>
</div>
#code {
bool IsHomeActive = true;
bool IsReportActive = false;
bool IsAdminActive = false;
private void ClearActive() {
IsHomeActive = false;
IsReportActive = false;
IsAdminActive = false;
}
}
This solution works fine, but it doesn't scale well. If I need to add more items to the menu, I will have to remember to added a boolean for the new item and remember to include it in ClearActive().
I though about creating a special data structure (an array, or list) and then have the code iterate through all items, this way I will only need to add one item to the array and then the code will generate the required html for the element, and also will handle the click event and automatically clear all active classes from the array and assign it to the clicked item in the array. But that approach will sacrifice the ability to use <AuthorizedView> on special items in the array..

How to pass an ID parameter to Blazorise modal on button click, from one component to another?

I'm new and trying to formulate this as best as I can. There's so much new concepts to get a hold of! Please tell me if I'm unclear.
I'm making a Todo-app as an exercise. On page load I get todo items from the database and iterate them in a foreach-loop, each todo is assigned an X-button for removal. This was easy, but for modifying a todo text I want an update button that brings up Blazorise's modal popup, to use it's input field.
My problem is that I don't get how to pass each todo item's id into this modal, for each button that is. For the remove button I could use the foreach loop's "todoItem.Id" variable, but as the Blazorise modal is another component I can't.
Here's how my Index page looks:
#page "/"
#using TodoApp.App.Components
<section class="todo-container">
#if (TodoItems != null)
{
#foreach (var todoItem in TodoItems)
{
if (todoItem.IsDone == false)
{
<div class="todo-item">
<p>#todoItem.Text</p>
<UpdatePopup OnTodoItemUpdated="UpdateAndLoad"></UpdatePopup>
<Button class="remove-btn" Clicked="(() => RemoveTodoItem(todoItem.Id))">X</Button>
</div>
}
}
}
</section>
<Popup OnTodoItemAdded="UpdateAndLoad"></Popup>
This button is inside the modal itself: <Button Clicked="#ShowModal">...</Button>, and I would've wanted to do the same thing as with the remove button, adding something like () => UpdateTodoItem(todoItem.Id).
How to get the ID from each item in the Index component and add this to each button in the Modal component?
There are multiple problems with your solution. First, you're creating a modal for each todo item (UpdatePopup) which is very inefficient. You should instead create just one update modal and use it for all the items.
#page "/"
#using TodoApp.App.Components
<section class="todo-container">
#if (TodoItems != null)
{
#foreach (var todoItem in TodoItems)
{
if (todoItem.IsDone == false)
{
<div class="todo-item">
<p>#todoItem.Text</p>
<Button class="update-btn" Clicked="(() => updatePopupRef.Show(todoItem))">Edit</Button>
<Button class="remove-btn" Clicked="(() => RemoveTodoItem(todoItem.Id))">X</Button>
</div>
}
}
}
</section>
<UpdatePopup #ref="#updatePopupRef" OnTodoItemUpdated="UpdateAndLoad" />
<Popup OnTodoItemAdded="UpdateAndLoad" />
#code{
UpdatePopup updatePopupRef;
}
And then in UpdatePopup you have something like this:
void Show(TodoItem item)
{
this.Item = item; // use this to bind item values to input fields
modalRef.Show(); // you also need to have modalRef set with #ref attribute
}
and in razor
<TextEdit #bind-Text="#Item.Name" />
PS. I haven't tested this code but you should have an overall idea :)

Increment Value on Model using Ajax and Razor

I'm looping through a collection on my model using Razor to display it. As an example:
#foreach(var item in myCollection)
{
<span id='item-#item.Id'>#item.Quantity</span>
<button type='button' onclick='updateQuantity(#item.Quantity+1);'>Add One</button>
}
In this example, updateQuantity performs an AJAX request, gets the new quantity back, and updates item-#item.Id with the new quantity. However, because #item.Quantity is pulled from the model (passed in via the page's GET method, #item.Quantity is never updated with the new value until the page is reloaded.
My question is: How can I make sure I'm always using the latest value, without having to reload the page?
Change the button inside your foreach loop like this:
#foreach(var item in myCollection)
{
<span id='item-#item.Id'>#item.Quantity</span>
<button id='updateButton' type='button'>Add One</button>
}
And, add this script to your View:
<script>
$("#updateButton").click(function() {
var quantity = $("#item-#item.Id").text();
updateQuantity(quantity);
})
</script>
And, in your updateQuantity() function, update the text inside the span after getting it back through Ajax.

Div elements on click fires for the first element only

I am listing my data in an ItemTemplate.Then inside the ItemTemplate, i have two div tags as follows:
<ItemTemplate>
<div id="contentdiv">
<h4 id="titleresult"><%# Server.HtmlEncode(Eval("Name").ToString())%></h4>
</div>
<div id="showclick" class=hideAll>
<p class="brief"><%# Server.HtmlEncode(Eval("LegalName").ToString())%></p>
<p class="brief"><%# Server.HtmlEncode(Eval("FirstName").ToString())%></p>
<p><%# Server.HtmlEncode(Eval("LastName").ToString())%></p>
</div>
</ItemTemplate>
Then i have the css to define the hideAll class so that when the page loads, the data in this div tag is hidden until the user clicks on the contentdiv link.
.hideAll { display:none }
.displayAll { display:block; top:0px}
Then finally i have the javascript part for firing the click event.
<script type="text/javascript">
function showResults(UserID) {
var contentdiv= document.getElementById('contentdiv');
var showclick = document.getElementById('showclick');
<%
long id =0;
DataAccess dataAccess = new DataAccess();
Data = dataAccess.GetCounterParty(id);
%>
var UserID = <%=dataAccess.GetCounterParty(id) %>
contentdiv.style.visibility = "visible";
$(showclick).removeClass('hideAll');
}
</script>
The UserID is the id of every element in the list. The problem is, the click affects only the first element no matter which other element i click on the list.
In html id is used to refer to one element.
If you use it multiple times the browser would default to the first element.
You should use a class selector. Something like:
$(".contentdiv").click(function(){
$(this).next().removeClass('hideAll');
});
Here is a working example. I used toggleClass though, it seems more appropriate to me.
An id is a unique identifier, you cannot have two or more things on the same page with the same identifier and expect things to work properly. Make your identifiers unique, and bind to the click event using a class selector instead.
you should use class instead of id, id are unique, which only exist in 1 page, class can exist in multple div
some idea for u
html
<div class="showclick hideAll">
script
$('.showclick').on('click', function(){
$(this).toggle(); //toggle to show or hide, can be any element u want to toggle instead of this
});

Categories