Blazor Component not updating after StateHasChanged() - c#

I have a Blazor Server-Side page that has the following:
#foreach (var s in OSTs)
{
<div class="alert" style="height: 30px; background:#s.Color; color: #s.TextColor;">
<div class="form-row">
<div class="col-sm-3"><strong>#s.Stage</strong><br />#s.Description</div>
<OpportunitiesStageDiv MyEditOst="#s" UpdateIt="AddOrEditStage"></OpportunitiesStageDiv>
<div class="badge badge-primary" #onclick="() => MoveItUp(s)" style="cursor: pointer; width: 6rem;"><i class=" oi oi-arrow-top"></i> Move Up</div>
<div class="badge badge-primary" #onclick="() => MoveItDown(s)" style="cursor: pointer; width: 6rem;"><i class="oi oi-arrow-bottom"></i> Move Down</div>
<div class="badge badge-primary">#s.Id - #s.StageOrder</div>
</div>
</div>
}
The Component OpportunitiesStagesDiv is as follows:
<div class="badge badge-primary" #onclick="() => EditStage(MyEditOst.Id)" style="cursor: pointer; width: 6rem;">Edit Stage</div>
<Modal #ref="#EditModal">
<Title>Edit Opportunity Stage</Title>
<Body>
<EditOpportunityStage EditOSt="#MyEditOst" Complete="AddOrEditStage"></EditOpportunityStage>
</Body>
<Footer>
<button type="button" class="btn btn-secondary" data-dismiss="modal" #onclick="() => EditModal.Close()">Close</button>
</Footer>
</Modal>
#code {
[Parameter]
public OpportunityStagesTemplate MyEditOst { get; set; }
private Modal EditModal = new Modal();
[Parameter]
public EventCallback<int> UpdateIt { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
}
private void EditStage(int SId)
{
EditModal.Open();
}
private void AddOrEditStage(int Completed)
{
if (Completed == 99)
{
}
EditModal.Close();
}
}
The MoveItUp(s) and MoveItDown(s) are functions that move the main div element in the foreach loop either up or down based on the order parameter in the query from the database. This works perfectly. What is strange is that once a DIV element is moved up or down, the edit function #onclick="() => EditStage(MyEditOst.Id)" in the component reflects as if it is the previous element.
For reference the working MoveItUp and MoveItDown functions:
private void MoveItUp(OpportunityStagesTemplate MyEditOst)
{
if (MyEditOst.StageOrder > 1)
{
OpportunitiesStagesTemplateGateway OSTGw = new OpportunitiesStagesTemplateGateway();
OpportunityStagesTemplate PreviousOst = OSTGw.GetStageByOIdAndStageOrder(MyEditOst.OId, MyEditOst.StageOrder - 1);
PreviousOst.StageOrder = MyEditOst.StageOrder;
MyEditOst.StageOrder -= 1;
OSTGw.UpdateOpportunityStage(PreviousOst);
OSTGw.UpdateOpportunityStage(MyEditOst);
ExecuteTheGetStages(LastId);
StateHasChanged();
}
}
private void MoveItDown(OpportunityStagesTemplate MyEditOst)
{
OpportunitiesStagesTemplateGateway OSTGw = new OpportunitiesStagesTemplateGateway();
int ct = OSTGw.GetCountByOId(MyEditOst.OId);
if (ct > MyEditOst.StageOrder)
{
OpportunityStagesTemplate NextOst = OSTGw.GetStageByOIdAndStageOrder(MyEditOst.OId, MyEditOst.StageOrder + 1);
NextOst.StageOrder = MyEditOst.StageOrder;
MyEditOst.StageOrder += 1;
OSTGw.UpdateOpportunityStage(NextOst);
OSTGw.UpdateOpportunityStage(MyEditOst);
ExecuteTheGetStages(LastId);
StateHasChanged();
}
}
Example I have a list as follows:
<div 1 edit moveup movedown>
<div 2 edit moveup movedown>
<div 3 edit moveup movedown>
When I move up one div
<div 1 edit moveup movedown>
<div 3 edit moveup movedown>
<div 2 edit moveup movedown>
and then click on the edit for now on line 2 for div 3, it brings up the edit as if was the div 2. It seems that the component still retains the position on the rendered UI as if DIV 2 is in on the second line and DIV 3 is still on the thirdline, although the move up and move down functions have updated the database and have updated the UI correctly to display the list in the order as it is moved. Any suggestions?
Below is sample rendered page:
Sample list picture

I have found a simple solution after careful reading of the Microsoft LifeCycle Documenttation. I found that if use the:
protected override void OnParametersSet()
{
...
}
the edit form on my modal displays the correct information. The OnParameterSet allows me to update any local component variables even if the original parameter that fed them on the OnInitialized() were set.
Thanks and I hope this answer helped all.

Related

Blazor How to get a Blazor component to not reload/re-render

I am building a dynamic Drop Down Navigation bar in Blazor. Now the problem is that when paging occurs the Navbar component reloads and the drop down dissapears (which is not what I want).
I know this is true because when I take the navigationManager.NavigateTo(route); out of the equation then it works as intended.
My MainLayout:
<div style="height: 100%; width: 100%; display: flex;">
<div style="height: 100%; width: 170px">
<NavigationMenu></NavigationMenu>
</div>
<div class="flex-child-expand">
#Body
</div>
</div>
NavigationMenu.razor
<div>
#foreach (var navButton in NavManager.MainNavButtons)
{
<div class="dropdown">
<button class="#navButton.StyleClassString" #onclick="#(() => OnButtonClicked(navButton, navButton.ButtonRoute))">#navButton.ButtonString</button>
<div class="dropdown-content">
#foreach (var button in navButton.SubSection)
{
<button class="#button.StyleClassString" #onclick="#(() => OnButtonClicked(navButton, button.ButtonRoute, button.ButtonString))">#button.ButtonString</button>
}
</div>
</div>
}
</div>
private void OnButtonClicked(NavManager.NavButton mainButtonPressed, string route, string buttonString = "")
{
if(buttonString == "")
{
foreach (var mainbtn in NavManager.MainNavButtons)
{
if (mainbtn.Section == mainButtonPressed.Section)
{
mainbtn.StyleClassString = ButtonActiveStyle.active;
}
else
{
mainbtn.StyleClassString = ButtonActiveStyle.normal;
}
//cleanup
foreach (var subButton in mainbtn.SubSection)
{
subButton.StyleClassString = ButtonActiveStyle.normal;
}
}
if(mainButtonPressed.SubSection.Count > 0)
{
mainButtonPressed.SubSection[0].StyleClassString = ButtonActiveStyle.active;
}
}
else
{
foreach (var mainbtn in NavManager.MainNavButtons)
{
if (mainbtn.Section == mainButtonPressed.Section)
{
mainbtn.StyleClassString = ButtonActiveStyle.active;
}
else
{
mainbtn.StyleClassString = ButtonActiveStyle.normal;
}
foreach (var subButton in mainbtn.SubSection)
{
if (subButton.ButtonString == buttonString)
{
subButton.StyleClassString = ButtonActiveStyle.active;
}
else
{
subButton.StyleClassString = ButtonActiveStyle.normal;
}
}
}
}
GoToPage(route);
}
private void GoToPage(string route)
{
navigationManager.NavigateTo(route);
}
*Sorry for bad indentation.
So is there a way to make the NavigationMenu.razor component from not rendering or reloading it's state when I call navigationManager.NavigateTo(route);?
To avoid a component to auto reload, you should override the ShouldRender method, and make it always return false.
However, you should check your resulting HTML. It seems that the page that you are navigating into does not inherit MainLayout.
This means that it will overwrite the
<div style="height: 100%; width: 100%; display: flex;">
<div style="height: 100%; width: 170px">
<NavigationMenu></NavigationMenu>
</div>
<div class="flex-child-expand">
#Body
</div>
</div>
portion for whatever the page contains, even if you return false in the ShouldRender.
A state change in the NavigationMenu component should not make it disappear.
NavigateTo(route) loads an entire page afresh specified by the 'route' address.
Layouts are specified at a page level. When you navigate to an address, the layout is initialized again and its UI state is reset. This means that all your dropdown expansions, formatting changes etc are lost. For example, in your case, the following CSS assignment is lost:
subButton.StyleClassString = ButtonActiveStyle.normal;
StyleClassString members of subButton(s) will be reset to the initial value (is it null?)
Therefore, the only way you can make sure that the dropdown persists its state, is if you store it somewhere.
You can achieve it in two ways:
Read it from the current URL
Store it as a state somewhere in the memory and read it in OnInitialized (complex and I won't really recommend)

Use Javascript addEventListener within Blazor component

I have a Blazor component which is rendered server-side. And I would like to have some collapsible divs inside of it. However since the code is server rendered the Javascript is not executed therefore the parts cannot collapse.
Here is the code inside my script.js file :
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.maxHeight){
content.style.maxHeight = null;
} else if(window.matchMedia("(max-width:1440px)")){
// content.style.maxHeight = content.scrollHeight + "px";
content.style.maxHeight = "20vh";
}
else {
content.style.maxHeight = "50vh";
}
});
}
Here is my main.cshtml file :
<component type="typeof(Main)" render-mode="Server" />
<script src="~/js/script.js" type="text/javascript"></script>
And finally my Main component with the collapsible parts :
#using Microsoft.AspNetCore.Components;
#using Microsoft.AspNetCore.Components.Web;
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
<div class="collapsible">
<label for="tutu">HEADER</label>
<div id="mybtn" class="btn-rch"></div>
</div>
<div class="tutu content flex-column">
<p>CONTENT HIDDEN IN COLLAPSE</p>
</div>
#code {
}
If I use render-mode="Static" instead of render-mode="Server" it works, but since my component will have event inside of it is not a possibility for me. How can I, with the use of JSInterop for example, call my JS script to make my div collapse ?
You can do all this in Blazor. Below is a simplistic working example of what I think you are trying to achieve.
This is a collapsible div component.
CollapseDiv.razor
<div #onclick="Collapse" style="cursor:pointer;" >
<h2>#Label</h2>
</div>
#if (!Collapsed)
{
<div>#ChildContent</div>
}
#code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment Label { get; set; }
bool Collapsed;
void Collapse(MouseEventArgs e)
{
Collapsed = !Collapsed;
}
}
And this is the page to demo it:
Collapse.razor
#page "/collapse"
<h3>Collapse Test Page</h3>
<CollapseDiv>
<Label>I'm Collapsible</Label>
<ChildContent>
I'm the collapsed content!
</ChildContent>
</CollapseDiv>
<br />
<br />
<CollapseDiv>
<Label>I'm Collapsible Too</Label>
<ChildContent>
More collapsed content!
</ChildContent>
</CollapseDiv>
#code {
}
The key here is: Forget manipulating the DOM with Javascript, build components.
You should be able to adopt this to fit your needs.

Insert a new section/component on drag and drop with Blazor

I am trying to make a new section/div/component to appear in the droparea upon the drop action.
For example, I make two blazor components <Calculator/> and <Counter /> which should be put in a dropzone's div depending on which one was dragged. So far I have two draggable elements:
#page "/"
<!-- draggable items-->
<ul>
<li draggable="true" #ondragstart="OnDragStart">drag Counter</li>
<li draggable="true" #ondragstart="OnDragStart">drag Calculator</li>
...
</ul>
Then I have multiple divs and among them dropzone divs. Depending in which zone I drop it, there the new component should be rendered:
<!-- drop zones-->
<div class="bg-primary">
<div class="row one">Test</div>
<div dropzone="move" class="row space"
#ondrop="OnDrop" ondragover="event.preventDefault();"></div>
<div class="row two"></div>
<div dropzone="move1" class="row space"
#ondrop="OnDrop" ondragover="event.preventDefault();"></div>
<div class="row three">Test</div>
</div>
My code is not doing much yet, only indicating what action is taking place:
<div>#DragStatus</div>
#code {
public string DragStatus = "test...";
public void OnDragStart()
{
DragStatus = "started";
}
public void OnDrop()
{
DragStatus = "dropped";
}
}
How would I indicate which of two elements was taken/dragged?
I have found a solution. It is more functional than professional. Just to give an idea.
On drag start event you can pass a variable of what is being dragged to a function:
<li draggable="true" #ondragstart='(() => OnDragStart("Counter"))'>drag Counter</li>
<li draggable="true" #ondragstart='(() => OnDragStart("Calculator"))'>drag Calculator</li>
Then we need to know where this item is being dropped:
<div dropzone="move1" class="row space"
#ondrop='(() => OnDrop("first"))' ondragover="event.preventDefault();">B</div>
<div class="row two"></div>
From above we get two values {first, calculator} so it can be stored into tuple:
public List<Tuple<string, string>> myitems { get; set; }
= new List<Tuple<string, string>>();
public string place { get; set; } = "";
public string DragStatus = "test...";
Two functions will save this data only when an item is both dragged and dropped:
public void OnDragStart(string taken_a)
{
DragStatus = taken_a; //what is being dragged
}
public void OnDrop(string place_a)
{
place = place_a;
var item = new Tuple<string, string>(place, DragStatus);
myitems.Add(item);
}
Finally we can loop through a list of tuples inside dropzone div:
<div dropzone="move1" class="row space"
#ondrop='(() => OnDrop("first"))' ondragover="event.preventDefault();">
#foreach (var item in myitems)
{
#if (item.Item1 == "first")
{
#if (item.Item2 == "Calculator")
{
<Calculator />
}
#if (item.Item2 == "Counter")
{
<Counter />
}
}
}
</div>
It may work for some small data sets, because you need to put such loops under each dropzone.
Otherwise, one of the professional methods seems to be render trees. A good resource on it:
https://chrissainty.com/building-components-via-rendertreebuilder/

How to access for each loop html properties to code behind?

i'm trying to access these data in code behind file. if i change these html tag to asp tags i cannot retrieve data from for each loop. here i want actual sales price and the product size which are generated by for each loop to save these properties in database. So is there any possible ways to solve these problem??
aspx page
<%
foreach (Com.Idk.Application.ImaraResPOS.Entities.ProductSize psize in psList)
{
%>
<div class="col-sm-12 text-center type">
<div class="circle bg">
<img src="images/Meal-100.png">
</div>
<div class="btn-align-size">
<button class="btn btn-primary ladda-button cd-add-to-cart" data-price="<%=psize.SalesPrice %>" data-name="<%= psize.SizeDef.Name %>" data-product-image="images/3.jpg" data-style="expand-right" id="Breadtype_btn" data-dismiss="modal" data-toggle="modal" data-target="#bread_type">
<%= psize.SizeDef.Name %>
<img src="images/Buy-30.png" style="height: 30px; width: 30px">
<h3 class="hide">
<%= psize.Id%>
</h3>
</button>
</div>
</div>
<%
}
%>
code behind
private void InsertProductSizeToSale()
{
string sizeID = Request.QueryString["size_id"].ToString();
Com.Idk.Application.ImaraResPOS.Entities.ProductSize prid = new Com.Idk.Application.ImaraResPOS.Entities.ProductSize();
psList.Select(Global.sess, "ProductSize","where Product_ID="+ prid);
if (psList.Count > 0)
{
Hashtable parameterList = new Hashtable();
OfferDetailList odList = new OfferDetailList();
parameterList.Clear();
parameterList.Add("productSizeId", sizeID );
parameterList.Add("comboId", null);
parameterList.Add("currentDate", DateTimeUtil.GetFormattedString(((Sale)index.saleid).Date));
parameterList.Add("currentTime", DateTimeUtil.GetFormatedTimeString(DateTime.Now));
parameterList.Add("day", Const.GetDay(((Sale)index.saleid).Date.DayOfWeek));
odList.SelectUsingSP(Global.sess, "SqlProGetOfferDetail", parameterList);
if (odList.Count == 0)
{
//want to retrive psize properties here
// here i'm getting an error
SqlProInsertProductSizeToSale(prid, sizeID, psize.SalesPrice, psize.SalesPrice);
}
else if (odList.Count > 0)
{ SqlProInsertProductSizeToSale(selectedProduct, selectedProductSize, odSelection.GetSelectedProductSizePrice(), selectedProductSize.SalesPrice);
}
}
}
}
I am not sure why you wish to get the data from the page. Since you are populating the page from some entity objects, why not just get the data from the entity objects directly?
Codebehind
foreach (Com.Idk.Application.ImaraResPOS.Entities.ProductSize p in psList)
{
SqlProInsertProductSizeToSale(p.prid, p.sizeID, p.SalesPrice, p.SalesPrice);
}

Javascript - Check checkbox if Multiple Checkboxes selected

I was wondering if someone could help me with some javascript as I'm quite unfamilar with it and unfortunately have been tasked with writing a function for tomorrow, and I appear to be failing miserably.
In my MVC application, I have a View where the user can select multiple outlets within a particular groupHeader. Already written was SelectAll, and DeselectAll javascript functions to select all (or deselect all) outlets within a groupHeader, however I am unsure how to use these functions within other functions.
I need to limit the existing functionality which will only allow the user to select the groupHeader, and this should select all the outlets within that group. Unfortunately this part of the application affects other parts so the underlying functionality must remain the same.
What I would ideally like is to have javascript to do the following:
If the groupHeader checkbox is checked, call the selectAll function.
If the groupHeader checkbox is unchecked, call the deselectAll function.
As the selections need to be remembered, which would be figured out from the controller, it would also be necessary to have the following functions:
On page load, if all outlets are checked in particular section, check the groupHeader checkbox.
On page load, if all outlets are unchecked in particular section, uncheck the groupHeader checkbox.
Here is the view:
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.1.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
<script src="/Scripts/FormHelpers.js" type="text/javascript"></script>
<script type="text/javascript">
function selectAll(sectionId) {
toggle(sectionId, "checked");
}
function deselectAll(sectionId) {
toggle(sectionId, null);
}
function toggle(sectionId, checked) {
$('[section$=' + sectionId + ']').each(function () { $(this).attr('checked', checked); });
}
</script>
<div>
<% int i = 0; %>
<% Html.BeginForm(); %>
<% { %>
<% foreach (App.Web.Models.OutletGroup g in Model.Groups) %>
<% { %>
<div style="width:700px;">
<div style="border-bottom: 1px solid;">
<div style="font-weight: bold; font-size: larger; width: 300px; float: left;">
<input type="checkbox" id="GrpHdr" /> <%: g.GroupHeader%>
</div>
<div style="line-height: 18px; vertical-align: middle; width: 250px; float: left;">
<a id="select" href="javascript:selectAll(<%: i %>)" <%: ViewData["GROUP_ALL_SELECTED_" + g.GroupHeader] %>>
Select All</a> / <a id="deselect" href="javascript:deselectAll(<%: i %>)" <%: ViewData["GROUP_ALL_SELECTED_" + g.GroupHeader] %>>
Deselect All</a>
</div>
<div style="clear: both;">
</div>
</div>
</div>
<div style="margin-left: 10px; margin-top: 10px;">
<% foreach (App.Data.Outlet outlet in g.Outlets) %>
<% { %>
<div style="float: left; line-height: 18px; padding: 2px; margin: 2px; vertical-align: middle;
border: 1px solid grey; width: 282px;">
<input type="checkbox" section="<%: i %>" name="OUTLET_<%: outlet.OutletID %>" <%: ViewData["OUTLET_" + outlet.OutletID] %>
style="vertical-align: middle; padding-left: 5px;" />
<%= Html.TrimTextToLength(outlet.Name)%>
</div>
<% } %>
</div>
<div style="clear: both; margin-bottom: 5px;">
</div>
<% i++; %>
<% } %>
<br />
<br />
<div class="buttonFooter">
<input type="submit" value="Update" />
</div>
<div style="clear: both;">
</div>
<% } %>
</div>
</asp:Content>
Here is the controller code also:
public class OutletsController : Controller
{
public ActionResult Index()
{
// Get all the outets and group them up.
//
ModelContainer ctn = new ModelContainer();
var groups = ctn.Outlets.GroupBy(o => o.Header);
OutletViewModel model = new OutletViewModel();
foreach (var group in groups)
{
OutletGroup oGroup = new OutletGroup()
{
GroupHeader = group.Key,
};
model.Groups.Add(oGroup);
}
foreach (var group in model.Groups)
{
group.Outlets = ctn.Outlets.Where(o => o.Header == group.GroupHeader).ToList();
}
// Get the existing details and check the necessary boxes (only read undeleted mappings).
//
var currentOutlets = ctn.UserOutlets.Where(uo => uo.UserID == UserServices.CurrentUserId && !uo.Deleted);
foreach (var outlet in currentOutlets)
{
ViewData["OUTLET_" + outlet.OutletID] = "checked='checked'";
}
return View(model);
}
[HttpPost]
public ActionResult Index(FormCollection formValues)
{
// Update the existing settings.
//
ModelContainer ctn = new ModelContainer();
var outlets = ctn.UserOutlets.Where(uo => uo.UserID == UserServices.CurrentUserId);
foreach (var outlet in outlets)
{
outlet.Deleted = true;
outlet.UpdatedDate = DateTime.Now;
outlet.UpdatedBy = UserServices.CurrentUserId;
}
// Save all the selected Outlets.
//
foreach (string o in formValues.Keys)
{
if (o.StartsWith("OUTLET_"))
{
UserOutlet uo = new UserOutlet();
uo.UserID = UserServices.CurrentUserId;
uo.OutletID = int.Parse(o.Substring("OUTLET_".Length));
uo.CreatedDate = DateTime.Now;
uo.CreatedBy = UserServices.CurrentUserId;
ctn.UserOutlets.AddObject(uo);
}
}
ctn.SaveChanges();
return RedirectToAction("Index");
}
}
I'd be very grateful if anyone could offer some help, or point me in the right direction.
Thanks!
EDIT:
Edited the javascript to include the following as suggested by Tejs:
$('.GrpHdr').each(function()
{
var elements = $(this).find('input[name|="OUTLET_"]');
var checkboxCount = elements.filter(':checked').length;
if (checkboxCount == elements.length)
$('.GrpHdr').attr('checked', this.checked);
else if (checkboxCount == 0)
$('.GrpHdr').attr('checked', !this.checked);
});
However I can't seem to get this to work for me. Can anyone see what's going wrong?
First, you need to change the GrpHdr checkbox to use a class or something; currently, it looks like you generate multiple checkboxes with the same Id which is never good. Assuming you change it to a class like so:
<input type="checkbox" class="GrpHdr" />
Then you can write something like this to check the checked status:
$('.GrpHdr').each(function()
{
var elements = $(this).find('input[name|="OUTPUT_"]');
var checkboxCount = elements.filter(':checked').length;
if(checkboxCount == elements.length)
// All Checked, Do Some Logic
else if(checkboxCount == 0)
// None checked, do some logic
else
// Some Checked and some not checked
});

Categories

Resources